[PATCH v4] [Wok] Bug fix #147: Block authentication request after too many failures
by ramonn@linux.vnet.ibm.com
From: Ramon Medeiros <ramonn(a)linux.vnet.ibm.com>
To prevent brute force attack, creates a mechanism to allow 3 tries
first. After that, a timeout will start and will be added 30 seconds for
each failed try in a row.
Signed-off-by: Ramon Medeiros <ramonn(a)linux.vnet.ibm.com>
---
Changes:
v4:
Use API.json for input validation
v3:
Improve error handling on login page
v2:
Set timeout by user, ip and session id. This will avoid trouble with
users using the same ip, like NAT.
src/wok/API.json | 25 +++++++++++++++++-
src/wok/i18n.py | 5 +++-
src/wok/root.py | 69 +++++++++++++++++++++++++++++++++++++++++-------
ui/js/src/wok.login.js | 19 ++++++++-----
ui/pages/i18n.json.tmpl | 5 +++-
ui/pages/login.html.tmpl | 6 ++---
6 files changed, 106 insertions(+), 23 deletions(-)
diff --git a/src/wok/API.json b/src/wok/API.json
index 8965db9..3f7bfd7 100644
--- a/src/wok/API.json
+++ b/src/wok/API.json
@@ -2,5 +2,28 @@
"$schema": "http://json-schema.org/draft-03/schema#",
"title": "Wok API",
"description": "Json schema for Wok API",
- "type": "object"
+ "type": "object",
+ "properties": {
+ "wokroot_login": {
+ "type": "object",
+ "properties": {
+ "username": {
+ "description": "Username",
+ "required": true,
+ "type": "string",
+ "minLength": 1,
+ "error": "WOKAUTH0003E"
+ },
+ "password": {
+ "description": "Password",
+ "required": true,
+ "type": "string",
+ "minLength": 1,
+ "error": "WOKAUTH0006E"
+ }
+ },
+ "additionalProperties": false,
+ "error": "WOKAUTH0007E"
+ }
+ }
}
diff --git a/src/wok/i18n.py b/src/wok/i18n.py
index 935c9c1..5ad5e57 100644
--- a/src/wok/i18n.py
+++ b/src/wok/i18n.py
@@ -40,8 +40,11 @@ messages = {
"WOKAUTH0001E": _("Authentication failed for user '%(username)s'. [Error code: %(code)s]"),
"WOKAUTH0002E": _("You are not authorized to access Wok. Please, login first."),
- "WOKAUTH0003E": _("Specify %(item)s to login into Wok."),
+ "WOKAUTH0003E": _("Specify username to login into Wok."),
+ "WOKAUTH0004E": _("You have failed to login in too much attempts. Please, wait for %(seconds)s seconds to try again."),
"WOKAUTH0005E": _("Invalid LDAP configuration: %(item)s : %(value)s"),
+ "WOKAUTH0006E": _("Specify password to login into Wok."),
+ "WOKAUTH0007E": _("You need to specify username and password to login into Wok."),
"WOKLOG0001E": _("Invalid filter parameter. Filter parameters allowed: %(filters)s"),
"WOKLOG0002E": _("Creation of log file failed: %(err)s"),
diff --git a/src/wok/root.py b/src/wok/root.py
index 080b7f0..9f6b7b3 100644
--- a/src/wok/root.py
+++ b/src/wok/root.py
@@ -1,7 +1,7 @@
#
# Project Wok
#
-# Copyright IBM Corp, 2015-2016
+# Copyright IBM Corp, 2015-2017
#
# Code derived from Project Kimchi
#
@@ -21,7 +21,9 @@
import cherrypy
import json
+import re
import os
+import time
from distutils.version import LooseVersion
from wok import auth
@@ -30,8 +32,8 @@ from wok.i18n import messages
from wok.config import paths as wok_paths
from wok.control import sub_nodes
from wok.control.base import Resource
-from wok.control.utils import parse_request
-from wok.exception import MissingParameter
+from wok.control.utils import parse_request, validate_params
+from wok.exception import UnauthorizedError, WokException
from wok.reqlogger import log_request
@@ -48,7 +50,8 @@ class Root(Resource):
super(Root, self).__init__(model)
self._handled_error = ['error_page.400', 'error_page.404',
'error_page.405', 'error_page.406',
- 'error_page.415', 'error_page.500']
+ 'error_page.415', 'error_page.500',
+ 'error_page.403', 'error_page.401']
if not dev_env:
self._cp_config = dict([(key, self.error_production_handler)
@@ -146,6 +149,7 @@ class WokRoot(Root):
self.domain = 'wok'
self.messages = messages
self.extends = None
+ self.failed_logins = {}
# set user log messages and make sure all parameters are present
self.log_map = ROOT_REQUESTS
@@ -153,24 +157,71 @@ class WokRoot(Root):
@cherrypy.expose
def login(self, *args):
+ def _raise_timeout(user_id):
+ length = self.failed_logins[user_ip_sid]["count"]
+ timeout = (length - 3) * 30
+ details = e = UnauthorizedError("WOKAUTH0004E",
+ {"seconds": timeout})
+ log_request(code, params, details, method, 403)
+ raise cherrypy.HTTPError(403, e.message)
details = None
method = 'POST'
code = self.getRequestMessage(method, 'login')
try:
params = parse_request()
+ validate_params(params, self, "login")
username = params['username']
password = params['password']
- except KeyError, item:
- details = e = MissingParameter('WOKAUTH0003E', {'item': str(item)})
- log_request(code, params, details, method, 400)
- raise cherrypy.HTTPError(400, e.message)
+ except WokException, e:
+ details = e
+ status = e.getHttpStatusCode()
+ raise cherrypy.HTTPError(status, e.message)
+
+ # get authentication info
+ remote_ip = cherrypy.request.remote.ip
+ session_id = str(cherrypy.session.originalid)
+ user_ip_sid = re.escape(username + remote_ip + session_id)
+
+ # check for repetly
+ count = self.failed_logins.get(user_ip_sid, {"count": 0}).get("count")
+ if count > 3:
+
+ # verify if timeout is still valid
+ last_try = self.failed_logins[user_ip_sid]["time"]
+ if time.time() < (last_try + ((count - 3) * 30)):
+ _raise_timeout(user_ip_sid)
+ else:
+ self.failed_logins.pop(user_ip_sid)
try:
status = 200
user_info = auth.login(username, password)
+
+ # user logged sucessfuly: reset counters
+ if self.failed_logins.get(user_ip_sid) != None:
+ self.failed_logins.pop(user_ip_sid)
except cherrypy.HTTPError, e:
- status = e.status
+
+ # store time and prevent too much tries
+ if self.failed_logins.get(user_ip_sid) == None:
+ self.failed_logins[user_ip_sid] = {"time": time.time(),
+ "ip": remote_ip,
+ "session_id": session_id,
+ "username": username,
+ "count": 1}
+ else:
+ # tries take more than 30 seconds between each one: do not
+ # increase count
+ if (time.time() -
+ self.failed_logins[user_ip_sid]["time"]) < 30:
+
+ self.failed_logins[user_ip_sid]["time"] = time.time()
+ self.failed_logins[user_ip_sid]["count"] += 1
+
+ # more than 3 fails: raise error
+ if self.failed_logins[user_ip_sid]["count"] > 3:
+ _raise_timeout(user_ip_sid)
raise
finally:
log_request(code, params, details, method, status)
diff --git a/ui/js/src/wok.login.js b/ui/js/src/wok.login.js
index 666a339..9e2a392 100644
--- a/ui/js/src/wok.login.js
+++ b/ui/js/src/wok.login.js
@@ -19,6 +19,10 @@
*/
wok.login_main = function() {
"use strict";
+ var i18n;
+ wok.getI18n(function(i18nObj){
+ i18n = i18nObj;
+ }, false, "i18n.json", true);
// verify if language is available
var selectedLanguage = wok.lang.get();
@@ -50,7 +54,8 @@ wok.login_main = function() {
var query = window.location.search;
var error = /.*error=(.*?)(&|$)/g.exec(query);
if (error && error[1] === "sessionTimeout") {
- $("#messSession").show();
+ $("#errorArea").html(i18n["WOKAUT0001E"]);
+ $("#errorArea").show();
}
var userNameBox = $('#username');
@@ -82,13 +87,13 @@ wok.login_main = function() {
window.location.replace(window.location.pathname.replace(/\/+login.html/, '') + next_url);
}, function(jqXHR, textStatus, errorThrown) {
if (jqXHR.responseText == "") {
- $("#messUserPass").hide();
- $("#missServer").show();
- } else {
- $("#missServer").hide();
- $("#messUserPass").show();
+ $("#errorArea").html(i18n["WOKAUT0002E"]);
+ $("#errorArea").show();
+ } else if ((jqXHR.responseJSON != undefined) &&
+ ! (jqXHR.responseJSON["reason"] == undefined)) {
+ $("#errorArea").html(jqXHR.responseJSON["reason"]);
+ $("#errorArea").show();
}
- $("#messSession").hide();
$("#logging").hide();
$("#login").show();
});
diff --git a/ui/pages/i18n.json.tmpl b/ui/pages/i18n.json.tmpl
index ba29532..4329ad0 100644
--- a/ui/pages/i18n.json.tmpl
+++ b/ui/pages/i18n.json.tmpl
@@ -1,7 +1,7 @@
#*
* Project Wok
*
- * Copyright IBM Corp, 2014-2016
+ * Copyright IBM Corp, 2014-2017
*
* Code derived from Project Kimchi
*
@@ -39,6 +39,9 @@
"WOKHOST6001M": "$_("Max:")",
+ "WOKAUT0001E": "$_("Session timeout, please re-login.")",
+ "WOKAUT0002E": "$_("Server unreachable")",
+
"WOKSETT0001M": "$_("Application")",
"WOKSETT0002M": "$_("User")",
"WOKSETT0003M": "$_("Request")",
diff --git a/ui/pages/login.html.tmpl b/ui/pages/login.html.tmpl
index f5a4b2d..6f967cf 100644
--- a/ui/pages/login.html.tmpl
+++ b/ui/pages/login.html.tmpl
@@ -1,7 +1,7 @@
#*
* Project Wok
*
- * Copyright IBM Corp, 2014-2016
+ * Copyright IBM Corp, 2014-2017
*
* Code derived from Project Kimchi
*
@@ -104,9 +104,7 @@
<div class="container">
<div id="login-window" class="login-area row">
<div class="err-area">
- <div id="messUserPass" class="alert alert-danger" style="display: none;">$_("The username or password you entered is incorrect. Please try again.")</div>
- <div id="messSession" class="alert alert-danger" style="display: none;">$_("Session timeout, please re-login.")</div>
- <div id="missServer" class="alert alert-danger" style="display: none;">$_("Server unreachable.")</div>
+ <div id="errorArea" class="alert alert-danger" style="display: none;"></div>
</div>
<form id="form-login" class="form-horizontal" method="post">
<div class="form-group">
--
2.10.1 (Apple Git-78)
7 years, 9 months
[PATCH v3] [WoK 0/2] /config/plugins API implementation
by dhbarboza82@gmail.com
From: Daniel Henrique Barboza <danielhb(a)linux.vnet.ibm.com>
v3:
- atomic backend commit
- using regex on parser
- 'test_mode' is now being retrieved by wok config
- plug-in dependencies are now being fetched
- plug-ins are now being enabled/disabled the cherrypy tree
v2:
- added User Log capabilities on /config/plugins/enable|disable actions
- added 'enable=' as a valid entry in the parsing of the conf file
This patch set implements the '/config/plugins' API.
The idea of this API is to replace the current '/plugins' API while
adding new attributes in their return values:
- enabled: true if the plug-in is enabled, false otherwise
- depends: list of all the plug-ins that this plug-in depends on
- is_dependency_of: list of all plug-in that depends on this plug-in
This backend is capable of enabling/disabling the plugi-ns using the
API /config/plugins/*name*/enable|disable. Please check the commit
messages of each patch for further details.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Daniel Henrique Barboza (2):
/config/plugins API: backend changes
/config/plugins: changing existing UI calls
docs/API/config.md | 32 +++++++
docs/API/plugins.md | 13 ---
src/wok/config.py.in | 5 +-
src/wok/control/config.py | 31 ++++++-
src/wok/control/plugins.py | 29 ------
src/wok/i18n.py | 4 +
src/wok/model/plugins.py | 40 ++++++--
src/wok/server.py | 56 ++---------
src/wok/utils.py | 227 +++++++++++++++++++++++++++++++++++++++++++--
tests/test_api.py | 59 ++++++++++++
tests/test_utils.py | 75 ++++++++++++++-
ui/js/src/wok.api.js | 4 +-
ui/js/src/wok.logos.js | 11 ++-
ui/js/src/wok.main.js | 10 +-
14 files changed, 476 insertions(+), 120 deletions(-)
delete mode 100644 docs/API/plugins.md
delete mode 100644 src/wok/control/plugins.py
--
2.9.3
7 years, 9 months
[RFC] Validating entries at login
by Ramon Medeiros
Propose: valid strings username and password with API.json
Issue: validator is not recognizing method
Just saw that src/wok/control/utils.py has a method validate_params,
that reads API and validate the output of request_params. I've added
this changes to code:
diff --git a/src/wok/API.json b/src/wok/API.json
index 8965db9..3faa31b 100644
--- a/src/wok/API.json
+++ b/src/wok/API.json
@@ -2,5 +2,24 @@
"$schema": "http://json-schema.org/draft-03/schema#",
"title": "Wok API",
"description": "Json schema for Wok API",
- "type": "object"
+ "type": "object",
+ "properties": {
+ "login": {
+ "type": "object",
+ "properties": {
+ "username": {
+ "description": "Username",
+ "required": true,
+ "type": "string",
+ "error": "WOKAUTH0003E"
+ },
+ "password": {
+ "description": "Password",
+ "required": true,
+ "type": "string",
+ "error": "WOKAUTH0003E"
+ }
+ }
+ }
+ }
}
diff --git a/src/wok/root.py b/src/wok/root.py
index e4cecae..55e1886 100644
--- a/src/wok/root.py
+++ b/src/wok/root.py
@@ -32,7 +32,7 @@ from wok.i18n import messages
from wok.config import paths as wok_paths
from wok.control import sub_nodes
from wok.control.base import Resource
-from wok.control.utils import parse_request
+from wok.control.utils import parse_request, validate_params
from wok.exception import MissingParameter, UnauthorizedError
from wok.reqlogger import log_request
@@ -170,6 +170,8 @@ class WokRoot(Root):
try:
params = parse_request()
+ validate_params(params, self, "login")
username = params['username']
password = params['password']
except KeyError, item:
Debugging the code, i just saw that the action_name passed to
validate_params (login) is not found by the validator:
/root/WOK/src/wok/control/utils.py(109)validate_params()
-> validator.validate(request)
(Pdb) s
--Call--
> /usr/lib/python2.7/site-packages/jsonschema/validators.py(121)validate()
-> def validate(self, *args, **kwargs):
(Pdb) n
> /usr/lib/python2.7/site-packages/jsonschema/validators.py(122)validate()
-> for error in self.iter_errors(*args, **kwargs):
(Pdb) s
--Call--
>
/usr/lib/python2.7/site-packages/jsonschema/validators.py(78)iter_errors()
-> def iter_errors(self, instance, _schema=None):
(Pdb) n
>
/usr/lib/python2.7/site-packages/jsonschema/validators.py(79)iter_errors()
-> if _schema is None:
(Pdb)
>
/usr/lib/python2.7/site-packages/jsonschema/validators.py(80)iter_errors()
-> _schema = self.schema
(Pdb)
>
/usr/lib/python2.7/site-packages/jsonschema/validators.py(82)iter_errors()
-> scope = _schema.get(u"id")
(Pdb)
>
/usr/lib/python2.7/site-packages/jsonschema/validators.py(83)iter_errors()
-> if scope:
(Pdb) print _schema
{u'$schema': u'http://json-schema.org/draft-03/schema#', u'type':
u'object', u'description': u'Json schema for Wok API', u'properties':
{u'wokroot_login': {u'type': u'object', u'properties': {u'username':
{u'required': True, u'type': u'string', u'description': u'Username',
u'error': u'WOKAUTH0003E'}, u'password': {u'required': True, u'type':
u'string', u'description': u'Password', u'error': u'WOKAUTH0003E'}}}},
u'title': u'Wok API'}
(Pdb) n
>
/usr/lib/python2.7/site-packages/jsonschema/validators.py(85)iter_errors()
-> try:
(Pdb)
>
/usr/lib/python2.7/site-packages/jsonschema/validators.py(86)iter_errors()
-> ref = _schema.get(u"$ref")
How i can know the correct one?
7 years, 9 months
[PATCH] [Kimchi 0/3] Multiple string truncation issue fixed
by rajgupta@linux.vnet.ibm.com
From: Rajat Gupta <rajat.triumph(a)gmail.com>
Rajat Gupta (3):
Fixed for Truncation occurs in edit a guest panel console row
Fixed Truncation appeared on Virtualization->add network of Japanese
language
Fixed truncation for Guest Interface GUI OVS network/interface scroll
bar
ui/css/src/modules/_edit-guests.scss | 2 +-
ui/css/src/modules/_network.scss | 8 ++++++++
ui/js/src/kimchi.guest_edit_main.js | 4 +++-
3 files changed, 12 insertions(+), 2 deletions(-)
--
2.1.0
7 years, 9 months
[PATCH V2] [Kimchi] Fix for the kimchi #1102
by jkatta@linux.vnet.ibm.com
From: Jayavardhan Katta <jkatta(a)linux.vnet.ibm.com>
Fix given to the kimchi plugin to avoid multiple continuos error message to the log
in case of VMs created outside of WoK.
Signed-off-by: Jayavardhan Katta <jkatta(a)linux.vnet.ibm.com>
---
model/vms.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/model/vms.py b/model/vms.py
index 7729af0..e7ed7c2 100644
--- a/model/vms.py
+++ b/model/vms.py
@@ -1300,7 +1300,7 @@ class VMModel(object):
with self.objstore as session:
try:
- extra_info = session.get('vm', dom.UUIDString())
+ extra_info = session.get('vm', dom.UUIDString(), True)
except NotFoundError:
extra_info = {}
icon = extra_info.get('icon')
--
2.7.4
7 years, 9 months
[PATCH V2] [Wok] Fix to the WoK for the Kimchi issue #1102
by jkatta@linux.vnet.ibm.com
From: Jayavardhan Katta <jkatta(a)linux.vnet.ibm.com>
As a fix to the Kimchi issue #1102, some fix needs to be given for the WoK framework as well.
Signed-off-by: Jayavardhan Katta <jkatta(a)linux.vnet.ibm.com>
---
src/wok/objectstore.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/wok/objectstore.py b/src/wok/objectstore.py
index a571e5f..c46aaf0 100644
--- a/src/wok/objectstore.py
+++ b/src/wok/objectstore.py
@@ -52,7 +52,7 @@ class ObjectStoreSession(object):
objects.sort(key=lambda (_, obj): obj[sort_key])
return [ident for ident, _ in objects]
- def get(self, obj_type, ident):
+ def get(self, obj_type, ident, ignore_missing=False):
c = self.conn.cursor()
res = c.execute('SELECT json FROM objects WHERE type=? AND id=?',
(obj_type, ident))
@@ -60,7 +60,10 @@ class ObjectStoreSession(object):
jsonstr = res.fetchall()[0][0]
except IndexError:
self.conn.rollback()
- raise NotFoundError("WOKOBJST0001E", {'item': ident})
+ data = {"":""}
+ jsonstr = json.dumps(data)
+ if not ignore_missing:
+ raise NotFoundError("WOKOBJST0001E", {'item': ident})
return json.loads(jsonstr)
def get_object_version(self, obj_type, ident):
--
2.7.4
7 years, 9 months
[PATCH v5][Kimchi] Issue# 979 - Change boot order UI
by bianca@linux.vnet.ibm.com
From: Bianca Carvalho <bianca(a)linux.vnet.ibm.com>
This patch adds the UI portion for supporting changing the guest boot order
via edit guest panel. This was based off of what Samuel had prototyped.
Issue found in backend during test:
When updating the VM even with just changing the name, the bootorder gets reset to 'hd' only.
I tested this using the curl command and confirmed that indeed it does get reset to one entry only.
Issue written to address this: https://github.com/kimchi-project/kimchi/issues/1012
Signed-off-by: Bianca Carvalho <bianca(a)linux.vnet.ibm.com>
---
ui/css/kimchi.css | 61 ++++++++++++++++++++++-
ui/css/src/modules/_edit-guests.scss | 60 +++++++++++++++++++++-
ui/js/src/kimchi.guest_edit_main.js | 97 +++++++++++++++++++++++++++++++++++-
ui/pages/guest-edit.html.tmpl | 26 ++++++++--
4 files changed, 238 insertions(+), 6 deletions(-)
diff --git a/ui/css/kimchi.css b/ui/css/kimchi.css
index fff3279..dec47fa 100644
--- a/ui/css/kimchi.css
+++ b/ui/css/kimchi.css
@@ -1,7 +1,7 @@
/*
* Project Kimchi
*
- * Copyright IBM Corp, 2015-2016
+ * Copyright IBM Corp, 2015-2017
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1557,6 +1557,65 @@ body.wok-gallery {
overflow: visible;
}
+ul {
+ cursor: default;
+}
+
+.boot-order {
+ display: block;
+ width: 85%;
+ font-size: 14px;
+ line-height: 1.42857;
+ color: #444;
+ overflow: hidden;
+ background-color: #fff;
+ background-image: none;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+ -o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+ transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+}
+
+.boot-order:focus,
+.boot-order.focus {
+ border-color: #66afe9;
+ outline: 0;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+}
+
+.boot-order > li {
+ cursor: move;
+ /* fallback if grab cursor is unsupported */
+ cursor: grab;
+ cursor: -moz-grab;
+ cursor: -webkit-grab;
+ border-left: 0;
+ border-right: 0;
+}
+
+.boot-order > li:first-child {
+ border-top: 0;
+}
+
+.boot-order > li:last-child {
+ border-bottom: 0;
+}
+
+.boot-order > li.ui-sortable-helper {
+ cursor: grabbing;
+ cursor: -moz-grabbing;
+ cursor: -webkit-grabbing;
+ border: 1px solid #ccc !important;
+}
+
+.boot-order li i {
+ text-align: right;
+}
+
/* Add Template Modal Window */
.templates-modal .modal-dialog {
width: 1100px;
diff --git a/ui/css/src/modules/_edit-guests.scss b/ui/css/src/modules/_edit-guests.scss
index 25d4d65..c8cc122 100644
--- a/ui/css/src/modules/_edit-guests.scss
+++ b/ui/css/src/modules/_edit-guests.scss
@@ -1,7 +1,7 @@
//
// Project Kimchi
//
-// Copyright IBM Corp, 2015-2016
+// Copyright IBM Corp, 2015-2017
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -429,3 +429,61 @@
#form-guest-storage-add .form-section .field {
overflow: visible;
}
+
+ul {
+ cursor: default;
+}
+
+.boot-order {
+ display: block;
+ width: 85%;
+ font-size: 14px;
+ line-height: 1.42857;
+ color: #444;
+ overflow: hidden;
+ background-color: #fff;
+ background-image: none;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+ -o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+ transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
+}
+
+.boot-order:focus,
+.boot-order.focus {
+ border-color: #66afe9;
+ outline: 0;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
+}
+
+.boot-order > li {
+ cursor: move; /* fallback if grab cursor is unsupported */
+ cursor: grab;
+ cursor: -moz-grab;
+ cursor: -webkit-grab;
+ border-left: 0;
+ border-right: 0;
+}
+
+.boot-order > li:first-child {
+ border-top: 0;
+}
+
+.boot-order > li:last-child {
+ border-bottom: 0;
+}
+
+.boot-order > li.ui-sortable-helper {
+ cursor: grabbing;
+ cursor: -moz-grabbing;
+ cursor: -webkit-grabbing;
+ border: 1px solid #ccc !important;
+}
+
+.boot-order li i {
+ text-align: right;
+}
diff --git a/ui/js/src/kimchi.guest_edit_main.js b/ui/js/src/kimchi.guest_edit_main.js
index b47d293..dfb2ca3 100644
--- a/ui/js/src/kimchi.guest_edit_main.js
+++ b/ui/js/src/kimchi.guest_edit_main.js
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
kimchi.guest_edit_main = function() {
var authType;
var formTargetId;
@@ -26,6 +27,7 @@ kimchi.guest_edit_main = function() {
var networkOptions = "";
clearTimeout(kimchi.vmTimeout);
+ var bootOrderOptions = [];
$('#modalWindow').on('hidden.bs.modal', function() {
kimchi.setListVMAutoTimeout();
@@ -45,7 +47,6 @@ kimchi.guest_edit_main = function() {
var submitForm = function(event) {
- // tap map, "general": 0, "storage": 1, "interface": 2, "permission": 3, "password": 4
var submit_map = {
0: generalSubmit,
3: permissionSubmit,
@@ -963,6 +964,85 @@ kimchi.guest_edit_main = function() {
});
};
+ var setupBootOrder = function(guest) {
+ var guestBootOrder = guest['bootorder'];
+ var dev = ["cdrom", "hd", "network"];
+ var excludedDev = dev.filter(function(e){return this.indexOf(e)<0;},guestBootOrder);
+
+ $('#myList button').prop('disabled', true);
+ $('#myList').empty();
+ $.each(guestBootOrder, function(index, value) {
+ item = $.parseHTML("<li class='list-group-item' data-value=" + value + "><input type='checkbox' class='wok-checkbox' id='checkbox-" + value + "' value='" + value + "'> <label class='check-all' for='checkbox-" + value + "'>" + value + "</label><button id='button-" + value + "' class='btn btn-link deleteBootOrderElem' style='float:right; margin-top: 3px; padding: 0;'><i class='fa fa-minus-circle'></i> Remove</button></li>");
+ $('#myList').append(item);
+ $('#checkbox-' + value).prop('disabled', true);
+ $('#checkbox-' + value).prop('checked', true);
+ $('#button-' + value).prop('disabled', false);
+ });
+ if (excludedDev) {
+ $.each(excludedDev, function(index, value) {
+ item = $.parseHTML("<li class='list-group-item' data-value=" + value + "><input type='checkbox' class='wok-checkbox' id='checkbox-" + value + "' value='" + value + "'> <label class='check-all' for='checkbox-" + value + "'>" + value + "</label><button id='button-" + value + "' class='btn btn-link deleteBootOrderElem' style='float:right; margin-top: 3px; padding: 0;'><i class='fa fa-minus-circle'></i> Remove</button></li>");
+ $('#myList').append(item);
+ });
+ }
+
+ if (guestBootOrder.length == 1) {
+ $('#myList button').prop('disabled', true);
+ }
+
+ $('.boot-order').sortable({
+ items: 'li',
+ cursor: 'move',
+ opacity: 0.6,
+ containment: "parent",
+ start: function(event, ui) {
+ $(this).addClass('focus');
+ },
+ stop: function(event, ui) {
+ $(this).removeClass('focus');
+ },
+ change: function(event, ui) {
+ // callback once started changing order
+ },
+ update: function(event, ui) {
+ // callback once finished order
+ $(saveButton).prop('disabled', false);
+ bootOrderOptions = [];
+ $("#myList li").each(function() {
+ bootOrderOptions.push($(this).attr("data-value").toLowerCase())
+ });
+ bootOrderOptions.forEach(function(entry) {
+ console.log(entry);
+ });
+ var data = {
+ bootorder: bootOrderOptions
+ };
+ kimchi.updateVM(kimchi.selectedGuest, data, function() {
+ // wok.window.close();
+ }, function(err) {
+ wok.message.error(err.responseJSON.reason,'#alert-modal-container');
+ });
+ }
+ });
+
+ $(".deleteBootOrderElem").on('click', function(evt) {
+ evt.preventDefault();
+ var item = $(this).parent().attr("data-value").toLowerCase();
+ $("#checkbox-" + item).prop('disabled', false);
+ $("#checkbox-" + item).prop('checked', false);
+ var index = guestBootOrder.indexOf(item);
+ if (index !== -1) {
+ guestBootOrder.splice(index, 1);
+ }
+ if (guestBootOrder.length == 1) {
+ $('#myList button').prop('disabled', true);
+ }
+ var data = {
+ bootorder: guestBootOrder
+ };
+ kimchi.updateVM(kimchi.selectedGuest, data, function() {});
+ });
+ };
+
var initContent = function(guest) {
guest['icon'] = guest['icon'] || 'plugins/kimchi/images/icon-vm.png';
$('#form-guest-edit-general').fillWithObject(guest);
@@ -1028,6 +1108,7 @@ kimchi.guest_edit_main = function() {
setupPermission();
setupPCIDevice();
setupSnapshot();
+ setupBootOrder(guest);
kimchi.init_processor_tab(guest.cpu_info, $(saveButton));
if ((kimchi.thisVMState === "running") || (kimchi.thisVMState === "paused")) {
@@ -1051,6 +1132,13 @@ kimchi.guest_edit_main = function() {
};
};
+ $('#form-guest-edit-general').removeClass('active');
+ $('#guest-edit-tabs > .wok-mask').show();
+ setTimeout(function() {
+ $('#form-guest-edit-general').addClass('active');
+ $('#guest-edit-tabs > .wok-mask').hide();
+ }, 500);
+
kimchi.retrieveVM(kimchi.selectedGuest, initContent);
var generalSubmit = function(event) {
@@ -1115,6 +1203,13 @@ kimchi.guest_edit_main = function() {
delete changedFields.memory.current;
}
}
+ console.log(changedFields);
+
+ checkedValue = [];
+ $("#myList input:checked").each(function() {
+ checkedValue.push($(this).attr("value"));
+ });
+ changedFields.bootorder = checkedValue;
kimchi.updateVM(kimchi.selectedGuest, changedFields, function() {
kimchi.listVmsAuto();
diff --git a/ui/pages/guest-edit.html.tmpl b/ui/pages/guest-edit.html.tmpl
index d8a482c..d67627a 100644
--- a/ui/pages/guest-edit.html.tmpl
+++ b/ui/pages/guest-edit.html.tmpl
@@ -1,7 +1,7 @@
#*
* Project Kimchi
*
- * Copyright IBM Corp, 2013-2016
+ * Copyright IBM Corp, 2013-2017
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,7 +30,7 @@
</div>
<div class="modal-body">
<span id="alert-modal-container"></span>
-<ul class="nav nav-tabs" role="tablist">
+ <ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#form-guest-edit-general" aria-controls="form-guest-edit-general" role="tab" data-id="form-guest-edit-general" data-toggle="tab">$_("General")</a></li>
<li role="presentation"><a href="#form-guest-edit-storage" aria-controls="form-guest-edit-storage" role="tab" data-id="form-guest-edit-storage" data-toggle="tab">$_("Storage")</a></li>
<li role="presentation"><a href="#form-guest-edit-interface" aria-controls="form-guest-edit-interface" role="tab" data-id="form-guest-edit-interface" data-toggle="tab">$_("Interface")</a></li>
@@ -40,6 +40,14 @@
<li role="presentation"><a href="#form-edit-processor" aria-controls="form-edit-processor" role="tab" data-id="form-edit-processor" data-toggle="tab">$_("Processor")</a></li>
</ul>
<div class="tab-content" id="guest-edit-tabs">
+ <div class="wok-mask" role="presentation" class="hidden">
+ <div class="wok-mask-loader-container">
+ <div class="wok-mask-loading">
+ <div class="wok-mask-loading-icon"></div>
+ <div class="wok-mask-loading-text">$_("Loading")...</div>
+ </div>
+ </div>
+ </div>
<form role="tabpanel" class="tab-pane active" id="form-guest-edit-general">
<div class="form-group">
<label for="guest-edit-id-textbox">$_("Name")</label>
@@ -68,6 +76,19 @@
<option value="virtio">$_("virtio")</option>
</select>
</div>
+ <div class="guest-edit-bootorder tab-pane" id="form-guest-edit-bootorder">
+ <div id="bootOrder">
+ <label for="guest-edit-boot-order-textbox">Boot Order</label>
+ <ul id="myList" class="list-group boot-order">
+ <li class="list-group-item" data-value="CDROM"><input type="checkbox" id="checkbox-cdrom" value="cdrom"> CD-ROM <button id="button-cdrom" class="btn btn-link deleteBootOrderElem"><i class="fa fa-minus-circle"></i> Remove</button></li>
+ <li class="list-group-item" data-value="HD"><input type="checkbox" id="checkbox-hd" value="hd"> HD <button id="button-hd" class="btn btn-link deleteBootOrderElem"><i class="fa fa-minus-circle"></i> Remove</button></li>
+ <li class="list-group-item" data-value="Network"><input type="checkbox" id="checkbox-network" value="network"> Network <button id="button-network" class="btn btn-link deleteBootOrderElem"><i class="fa fa-minus-circle"></i> Remove</button></li>
+ </ul>
+ <p class="help-block">
+ <i class="fa fa-info-circle"></i> $_("Select which items for boot order and grad them when needed to order them. At least one option must be selected.")</p>
+ </p>
+ </div>
+ </div>
</form>
<form role="tabpanel" class="tab-pane" id="form-guest-edit-storage">
<div class="btn-group action-area">
@@ -197,7 +218,6 @@
$_("Current CPU must be equal or lower than the Maximum CPU value. If a topology is set, it must be also be a multiple of the 'threads' value.")
</p>
</div>
-
<div id="guest-max-processor-panel" class="form-group">
<label for="guest-edit-max-processor-textbox">$_("Max CPU")</label>
<p id="settings-readonly-help" class="hidden">$_("Unable to edit maximum CPU or CPU topology when editing a running or paused virtual machine.")</p>
--
2.9.3
7 years, 9 months