From bdc878e1b64197bcfbc4ff6a65c14dde0bfd6d28 Mon Sep 17 00:00:00 2001 From: matthewalanpenning Date: Sun, 4 Jun 2023 15:52:38 -0400 Subject: [PATCH] v0.2.0 --- CHANGELOG.md | 7 + lxconsole/api/__init__.py | 2 + lxconsole/api/access_controls.py | 24 ++ lxconsole/api/cluster_groups.py | 118 ++++++ lxconsole/api/cluster_members.py | 63 ++++ lxconsole/routes.py | 5 + lxconsole/static/js/serializejson.js | 338 ------------------ lxconsole/static/js/sidebar.js | 3 + lxconsole/templates/access-controls.html | 4 +- lxconsole/templates/certificates.html | 2 +- lxconsole/templates/cluster-groups.html | 266 ++++++++++++++ lxconsole/templates/cluster-members.html | 72 +++- lxconsole/templates/container.html | 2 +- lxconsole/templates/containers.html | 9 +- lxconsole/templates/groups.html | 4 +- lxconsole/templates/images.html | 2 +- lxconsole/templates/login.html | 1 - lxconsole/templates/main.html | 3 +- .../templates/modals/cluster-groups.html | 171 +++++++++ .../templates/modals/cluster-members.html | 56 ++- lxconsole/templates/modals/containers.html | 3 +- lxconsole/templates/modals/main.html | 2 +- .../templates/modals/virtual-machines.html | 3 +- lxconsole/templates/network-acl.html | 2 +- lxconsole/templates/network-acls.html | 2 +- lxconsole/templates/networks.html | 2 +- lxconsole/templates/profiles.html | 2 +- lxconsole/templates/projects.html | 2 +- lxconsole/templates/register.html | 1 - lxconsole/templates/roles.html | 2 +- lxconsole/templates/sidebar.html | 8 + lxconsole/templates/simplestreams.html | 2 +- lxconsole/templates/storage-pools.html | 2 +- lxconsole/templates/storage-volumes.html | 2 +- lxconsole/templates/users.html | 8 +- lxconsole/templates/virtual-machine.html | 2 +- lxconsole/templates/virtual-machines.html | 9 +- roadmap.txt | 3 - 38 files changed, 815 insertions(+), 394 deletions(-) create mode 100644 lxconsole/api/cluster_groups.py delete mode 100644 lxconsole/static/js/serializejson.js create mode 100644 lxconsole/templates/cluster-groups.html create mode 100644 lxconsole/templates/modals/cluster-groups.html diff --git a/CHANGELOG.md b/CHANGELOG.md index d878919..caf7d2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 0.2.0 +- Added cluster groups +- Added evacuate and restore options for cluster members +- Added cluster members roles to tables +- Added cluster members actions +- Removed serializeJSON library in favor of jquery serialize + # 0.1.0 - Corrected image unit size, changing GiB to MiB - Added option filter for storage volumes by adding filter=custom to URL diff --git a/lxconsole/api/__init__.py b/lxconsole/api/__init__.py index f2c163c..ccd6059 100644 --- a/lxconsole/api/__init__.py +++ b/lxconsole/api/__init__.py @@ -2,6 +2,7 @@ from flask import Flask, Blueprint from . import server from . import servers from . import certificates +from . import cluster_groups from . import cluster_members from . import images from . import container @@ -34,6 +35,7 @@ api.add_url_rule('/containers/', view_func=containers.api_containers_e api.add_url_rule('/servers/', view_func=servers.api_servers_endpoint, methods=['GET', 'POST']) api.add_url_rule('/server/', view_func=server.api_server_endpoint) api.add_url_rule('/certificates/', view_func=certificates.api_certificates_endpoint, methods=['GET', 'POST']) +api.add_url_rule('/cluster-groups/', view_func=cluster_groups.api_cluster_groups_endpoint, methods=['GET', 'POST']) api.add_url_rule('/cluster-members/', view_func=cluster_members.api_cluster_members_endpoint, methods=['GET', 'POST']) api.add_url_rule('/images/', view_func=images.api_images_endpoint, methods=['GET', 'POST']) api.add_url_rule('/networks/', view_func=networks.api_networks_endpoint, methods=['GET', 'POST']) diff --git a/lxconsole/api/access_controls.py b/lxconsole/api/access_controls.py index a78dc3a..2902158 100644 --- a/lxconsole/api/access_controls.py +++ b/lxconsole/api/access_controls.py @@ -11,6 +11,7 @@ def privilege_check(privilege, server_id = 0): 'Auditor' : [ #'add_access_control', #'add_certificate', + #'add_cluster_group', #'add_cluster_member', #'add_group', #'add_server', @@ -32,6 +33,7 @@ def privilege_check(privilege, server_id = 0): #'add_storage_volume', #'add_user', #'attach_instance_profile', + #'change_cluster_member_state', #'change_instance_state', #'copy_instance', #'create_instance_backup', @@ -39,6 +41,7 @@ def privilege_check(privilege, server_id = 0): #'create_instance_snapshot', #'delete_access_control', #'delete_certificate', + #'delete_cluster_group', #'delete_cluster_member', #'delete_group', #'delete_image', @@ -88,6 +91,7 @@ def privilege_check(privilege, server_id = 0): 'is_cluster_member_enabled', 'list_access_controls', 'list_certificates', + 'list_cluster_groups', 'list_cluster_members', 'list_groups', 'list_servers', @@ -110,6 +114,7 @@ def privilege_check(privilege, server_id = 0): 'list_storage_volumes', 'list_users', 'load_certificate', + 'load_cluster_group', 'load_cluster_member', 'load_image', 'load_instance', @@ -129,6 +134,7 @@ def privilege_check(privilege, server_id = 0): #'restore_instance_snapshot', #'update_access_control', #'update_certificate', + #'update_cluster_group', #'update_cluster_member', #'update_group', #'update_server', @@ -147,6 +153,7 @@ def privilege_check(privilege, server_id = 0): 'User': [ #'add_access_control', #'add_certificate', + #'add_cluster_group', #'add_cluster_member', #'add_group', #'add_server', @@ -168,6 +175,7 @@ def privilege_check(privilege, server_id = 0): #'add_storage_volume', #'add_user', #'attach_instance_profile', + 'change_cluster_member_state', 'change_instance_state', 'copy_instance', 'create_instance_backup', @@ -175,6 +183,7 @@ def privilege_check(privilege, server_id = 0): 'create_instance_snapshot', #'delete_access_control', #'delete_certificate', + #'delete_cluster_group', #'delete_cluster_member', #'delete_group', #'delete_image', @@ -224,6 +233,7 @@ def privilege_check(privilege, server_id = 0): 'is_cluster_member_enabled', 'list_access_controls', 'list_certificates', + 'list_cluster_groups', 'list_cluster_members', 'list_groups', 'list_servers', @@ -246,6 +256,7 @@ def privilege_check(privilege, server_id = 0): 'list_storage_volumes', 'list_users', 'load_certificate', + 'load_cluster_group', 'load_cluster_member', 'load_image', 'load_instance', @@ -265,6 +276,7 @@ def privilege_check(privilege, server_id = 0): 'restore_instance_snapshot', #'update_access_control', #'update_certificate', + #'update_cluster_group', #'update_cluster_member', #'update_group', #'update_server', @@ -283,6 +295,7 @@ def privilege_check(privilege, server_id = 0): 'Operator': [ #'add_access_control', 'add_certificate', + 'add_cluster_group', 'add_cluster_member', #'add_group', 'add_server', @@ -304,6 +317,7 @@ def privilege_check(privilege, server_id = 0): 'add_storage_volume', #'add_user', 'attach_instance_profile', + 'change_cluster_member_state', 'change_instance_state', 'copy_instance', 'create_instance_backup', @@ -311,6 +325,7 @@ def privilege_check(privilege, server_id = 0): 'create_instance_snapshot', #'delete_access_control', 'delete_certificate', + 'delete_cluster_group', 'delete_cluster_member', #'delete_group', #'delete_image', @@ -360,6 +375,7 @@ def privilege_check(privilege, server_id = 0): 'is_cluster_member_enabled', 'list_access_controls', 'list_certificates', + 'list_cluster_groups', 'list_cluster_members', 'list_groups', 'list_servers', @@ -382,6 +398,7 @@ def privilege_check(privilege, server_id = 0): 'list_storage_volumes', 'list_users', 'load_certificate', + 'load_cluster_group', 'load_cluster_member', 'load_image', 'load_instance', @@ -401,6 +418,7 @@ def privilege_check(privilege, server_id = 0): 'restore_instance_snapshot', #'update_access_control', 'update_certificate', + 'update_cluster_group', 'update_cluster_member', #'update_group', 'update_server', @@ -419,6 +437,7 @@ def privilege_check(privilege, server_id = 0): 'Administrator': [ 'add_access_control', 'add_certificate', + 'add_cluster_group', 'add_cluster_member', 'add_group', 'add_server', @@ -440,6 +459,7 @@ def privilege_check(privilege, server_id = 0): 'add_storage_volume', 'add_user', 'attach_instance_profile', + 'change_cluster_member_state', 'change_instance_state', 'copy_instance', 'create_instance_backup', @@ -447,6 +467,7 @@ def privilege_check(privilege, server_id = 0): 'create_instance_snapshot', 'delete_access_control', 'delete_certificate', + 'delete_cluster_group', 'delete_cluster_member', 'delete_group', 'delete_image', @@ -496,6 +517,7 @@ def privilege_check(privilege, server_id = 0): 'is_cluster_member_enabled', 'list_access_controls', 'list_certificates', + 'list_cluster_groups', 'list_cluster_members', 'list_groups', 'list_servers', @@ -518,6 +540,7 @@ def privilege_check(privilege, server_id = 0): 'list_storage_volumes', 'list_users', 'load_certificate', + 'load_cluster_group', 'load_cluster_member', 'load_image', 'load_instance', @@ -537,6 +560,7 @@ def privilege_check(privilege, server_id = 0): 'restore_instance_snapshot', 'update_access_control', 'update_certificate', + 'update_cluster_group', 'update_cluster_member', 'update_group', 'update_server', diff --git a/lxconsole/api/cluster_groups.py b/lxconsole/api/cluster_groups.py new file mode 100644 index 0000000..4863218 --- /dev/null +++ b/lxconsole/api/cluster_groups.py @@ -0,0 +1,118 @@ +from flask import jsonify, request +import requests +from lxconsole import db +from lxconsole.models import Server +from flask_login import login_required +from lxconsole.api.access_controls import privilege_check + + +def get_client_crt(): + return 'certs/client.crt' + +def get_client_key(): + return 'certs/client.key' + +@login_required +def api_cluster_groups_endpoint(endpoint): + + if not privilege_check(endpoint, request.args.get('id')): + return jsonify({'data': [], 'metadata':[], 'error': 'not authorized', 'error_code': 403}) + + + if endpoint == 'add_cluster_group': + id = request.args.get('id') + project = request.args.get('project') + server = Server.query.filter_by(id=id).first() + url = 'https://' + server.addr + ':' + str(server.port) + '/1.0/cluster/groups?project=' + project + client_cert = get_client_crt() + client_key = get_client_key() + + if request.form.get('json'): + data = request.form.get('json') + results = requests.post(url, verify=server.ssl_verify, cert=(client_cert, client_key), data=data) + return jsonify(results.json()) + + data = {} + data.update({'name': request.form.get('name')}) + data.update({'description': request.form.get('description')}) + data.update({'members': request.form.getlist('members')}) + results = requests.post(url, verify=server.ssl_verify, cert=(client_cert, client_key), json=data) + + return jsonify(results.json()) + + + if endpoint == 'delete_cluster_group': + id = request.args.get('id') + project = request.args.get('project') + name = request.form.get('name') + server = Server.query.filter_by(id=id).first() + url = 'https://' + server.addr + ':' + str(server.port) + '/1.0/cluster/groups/' + name + '?project=' + project + client_cert = get_client_crt() + client_key = get_client_key() + results = requests.delete(url, verify=server.ssl_verify, cert=(client_cert, client_key)) + return jsonify(results.json()) + + + if endpoint == 'is_cluster_member_enabled': + id = request.args.get('id') + project = request.args.get('project') + server = Server.query.filter_by(id=id).first() + url = 'https://' + server.addr + ':' + str(server.port) + '/1.0/cluster?project=' + project + client_cert = get_client_crt() + client_key = get_client_key() + results = requests.get(url, verify=server.ssl_verify, cert=(client_cert, client_key)) + data = results.json()['metadata'] + return str(data['enabled']) + + + if endpoint == 'list_cluster_groups': + if api_cluster_groups_endpoint('is_cluster_member_enabled') == 'True': + id = request.args.get('id') + project = request.args.get('project') + server = Server.query.filter_by(id=id).first() + recursion = request.args.get('recursion') + if recursion == '1': + url = 'https://' + server.addr + ':' + str(server.port) + '/1.0/cluster/groups?recursion=1&project=' + project + else: + url = 'https://' + server.addr + ':' + str(server.port) + '/1.0/cluster/groups?project=' + project + client_cert = get_client_crt() + client_key = get_client_key() + results = requests.get(url, verify=server.ssl_verify, cert=(client_cert, client_key)) + return jsonify(results.json()) + data = { "metadata": []} + return jsonify(data) + + + if endpoint == 'load_cluster_group': + id = request.args.get('id') + project = request.args.get('project') + name = request.form.get('name') + server = Server.query.filter_by(id=id).first() + url = 'https://' + server.addr + ':' + str(server.port) + '/1.0/cluster/groups/' + name + '?project=' + project + client_cert = get_client_crt() + client_key = get_client_key() + results = requests.get(url, verify=server.ssl_verify, cert=(client_cert, client_key)) + return jsonify(results.json()) + + + if endpoint == 'update_cluster_group': + id = request.args.get('id') + project = request.args.get('project') + name = request.args.get('name') + server = Server.query.filter_by(id=id).first() + url = 'https://' + server.addr + ':' + str(server.port) + '/1.0/cluster/groups/' + name + '?project=' + project + client_cert = get_client_crt() + client_key = get_client_key() + + if request.form.get('json'): + data = request.form.get('json') + results = requests.put(url, verify=server.ssl_verify, cert=(client_cert, client_key), data=data) + return jsonify(results.json()) + + if request.form.get('name'): + data = {} + data.update({'name': request.form.get('name')}) + results = requests.post(url, verify=server.ssl_verify, cert=(client_cert, client_key), json=data) + return jsonify(results.json()) + return False + \ No newline at end of file diff --git a/lxconsole/api/cluster_members.py b/lxconsole/api/cluster_members.py index e9fb301..69ac683 100644 --- a/lxconsole/api/cluster_members.py +++ b/lxconsole/api/cluster_members.py @@ -18,6 +18,36 @@ def api_cluster_members_endpoint(endpoint): if not privilege_check(endpoint, request.args.get('id')): return jsonify({'data': [], 'metadata':[], 'error': 'not authorized', 'error_code': 403}) + + if endpoint == 'change_cluster_member_state': + id = request.args.get('id') + project = request.args.get('project') + name = request.form.get('name') + server = Server.query.filter_by(id=id).first() + url = 'https://' + server.addr + ':' + str(server.port) + '/1.0/cluster/members/' + name + '/state?project=' + project + client_cert = get_client_crt() + client_key = get_client_key() + data = {} + data.update({'action': request.form.get('action')}) + results = requests.post(url, verify=server.ssl_verify, cert=(client_cert, client_key), json=data) + return jsonify(results.json()) + + + if endpoint == 'delete_cluster_member': + id = request.args.get('id') + project = request.args.get('project') + name = request.form.get('name') + server = Server.query.filter_by(id=id).first() + if request.form.get('force') == 'true': + url = 'https://' + server.addr + ':' + str(server.port) + '/1.0/cluster/members/' + name + '?force=1&project=' + project + else: + url = 'https://' + server.addr + ':' + str(server.port) + '/1.0/cluster/members/' + name + '?project=' + project + client_cert = get_client_crt() + client_key = get_client_key() + results = requests.delete(url, verify=server.ssl_verify, cert=(client_cert, client_key)) + return jsonify(results.json()) + + if endpoint == 'is_cluster_member_enabled': id = request.args.get('id') project = request.args.get('project') @@ -47,3 +77,36 @@ def api_cluster_members_endpoint(endpoint): data = { "metadata": []} return jsonify(data) + + if endpoint == 'load_cluster_member': + id = request.args.get('id') + project = request.args.get('project') + name = request.form.get('name') + server = Server.query.filter_by(id=id).first() + url = 'https://' + server.addr + ':' + str(server.port) + '/1.0/cluster/members/' + name + '?project=' + project + client_cert = get_client_crt() + client_key = get_client_key() + results = requests.get(url, verify=server.ssl_verify, cert=(client_cert, client_key)) + return jsonify(results.json()) + + + if endpoint == 'update_cluster_member': + id = request.args.get('id') + project = request.args.get('project') + name = request.args.get('name') + server = Server.query.filter_by(id=id).first() + url = 'https://' + server.addr + ':' + str(server.port) + '/1.0/cluster/members/' + name + '?project=' + project + client_cert = get_client_crt() + client_key = get_client_key() + + if request.form.get('json'): + data = request.form.get('json') + results = requests.put(url, verify=server.ssl_verify, cert=(client_cert, client_key), data=data) + return jsonify(results.json()) + + if request.form.get('server_name'): + data = {} + data.update({'server_name': request.form.get('server_name')}) + results = requests.post(url, verify=server.ssl_verify, cert=(client_cert, client_key), json=data) + return jsonify(results.json()) + return False \ No newline at end of file diff --git a/lxconsole/routes.py b/lxconsole/routes.py index 0d6b549..41a9862 100644 --- a/lxconsole/routes.py +++ b/lxconsole/routes.py @@ -49,6 +49,11 @@ def home(): def certificates(): return render_template('certificates.html', page_title='Certificates', page_user_id=current_user.id, page_username=current_user.username,) +@app.route("/cluster-groups") +@login_required +def cluster_groups(): + return render_template('cluster-groups.html', page_title='Cluster Groups', page_user_id=current_user.id, page_username=current_user.username,) + @app.route("/cluster-members") @login_required def cluster_members(): diff --git a/lxconsole/static/js/serializejson.js b/lxconsole/static/js/serializejson.js deleted file mode 100644 index 80da2d8..0000000 --- a/lxconsole/static/js/serializejson.js +++ /dev/null @@ -1,338 +0,0 @@ -/*! - SerializeJSON jQuery plugin. - https://github.com/marioizquierdo/jquery.serializeJSON - version 3.2.1 (Feb, 2021) - - Copyright (c) 2012-2021 Mario Izquierdo - Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) - and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. -*/ -(function (factory) { - /* global define, require, module */ - if (typeof define === "function" && define.amd) { // AMD. Register as an anonymous module. - define(["jquery"], factory); - } else if (typeof exports === "object") { // Node/CommonJS - var jQuery = require("jquery"); - module.exports = factory(jQuery); - } else { // Browser globals (zepto supported) - factory(window.jQuery || window.Zepto || window.$); // Zepto supported on browsers as well - } - -}(function ($) { - "use strict"; - - var rCRLF = /\r?\n/g; - var rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i; - var rsubmittable = /^(?:input|select|textarea|keygen)/i; - var rcheckableType = /^(?:checkbox|radio)$/i; - - $.fn.serializeJSON = function (options) { - var f = $.serializeJSON; - var $form = this; // NOTE: the set of matched elements is most likely a form, but it could also be a group of inputs - var opts = f.setupOpts(options); // validate options and apply defaults - var typeFunctions = $.extend({}, opts.defaultTypes, opts.customTypes); - - // Make a list with {name, value, el} for each input element - var serializedArray = f.serializeArray($form, opts); - - // Convert the serializedArray into a serializedObject with nested keys - var serializedObject = {}; - $.each(serializedArray, function (_i, obj) { - - var nameSansType = obj.name; - var type = $(obj.el).attr("data-value-type"); - - if (!type && !opts.disableColonTypes) { // try getting the type from the input name - var p = f.splitType(obj.name); // "foo:string" => ["foo", "string"] - nameSansType = p[0]; - type = p[1]; - } - if (type === "skip") { - return; // ignore fields with type skip - } - if (!type) { - type = opts.defaultType; // "string" by default - } - - var typedValue = f.applyTypeFunc(obj.name, obj.value, type, obj.el, typeFunctions); // Parse type as string, number, etc. - - if (!typedValue && f.shouldSkipFalsy(obj.name, nameSansType, type, obj.el, opts)) { - return; // ignore falsy inputs if specified in the options - } - - var keys = f.splitInputNameIntoKeysArray(nameSansType); - f.deepSet(serializedObject, keys, typedValue, opts); - }); - return serializedObject; - }; - - // Use $.serializeJSON as namespace for the auxiliar functions - // and to define defaults - $.serializeJSON = { - defaultOptions: {}, // reassign to override option defaults for all serializeJSON calls - - defaultBaseOptions: { // do not modify, use defaultOptions instead - checkboxUncheckedValue: undefined, // to include that value for unchecked checkboxes (instead of ignoring them) - useIntKeysAsArrayIndex: false, // name="foo[2]" value="v" => {foo: [null, null, "v"]}, instead of {foo: ["2": "v"]} - - skipFalsyValuesForTypes: [], // skip serialization of falsy values for listed value types - skipFalsyValuesForFields: [], // skip serialization of falsy values for listed field names - - disableColonTypes: false, // do not interpret ":type" suffix as a type - customTypes: {}, // extends defaultTypes - defaultTypes: { - "string": function(str) { return String(str); }, - "number": function(str) { return Number(str); }, - "boolean": function(str) { var falses = ["false", "null", "undefined", "", "0"]; return falses.indexOf(str) === -1; }, - "null": function(str) { var falses = ["false", "null", "undefined", "", "0"]; return falses.indexOf(str) === -1 ? str : null; }, - "array": function(str) { return JSON.parse(str); }, - "object": function(str) { return JSON.parse(str); }, - "skip": null // skip is a special type used to ignore fields - }, - defaultType: "string", - }, - - // Validate and set defaults - setupOpts: function(options) { - if (options == null) options = {}; - var f = $.serializeJSON; - - // Validate - var validOpts = [ - "checkboxUncheckedValue", - "useIntKeysAsArrayIndex", - - "skipFalsyValuesForTypes", - "skipFalsyValuesForFields", - - "disableColonTypes", - "customTypes", - "defaultTypes", - "defaultType" - ]; - for (var opt in options) { - if (validOpts.indexOf(opt) === -1) { - throw new Error("serializeJSON ERROR: invalid option '" + opt + "'. Please use one of " + validOpts.join(", ")); - } - } - - // Helper to get options or defaults - return $.extend({}, f.defaultBaseOptions, f.defaultOptions, options); - }, - - // Just like jQuery's serializeArray method, returns an array of objects with name and value. - // but also includes the dom element (el) and is handles unchecked checkboxes if the option or data attribute are provided. - serializeArray: function($form, opts) { - if (opts == null) { opts = {}; } - var f = $.serializeJSON; - - return $form.map(function() { - var elements = $.prop(this, "elements"); // handle propHook "elements" to filter or add form elements - return elements ? $.makeArray(elements) : this; - - }).filter(function() { - var $el = $(this); - var type = this.type; - - // Filter with the standard W3C rules for successful controls: http://www.w3.org/TR/html401/interact/forms.html#h-17.13.2 - return this.name && // must contain a name attribute - !$el.is(":disabled") && // must not be disable (use .is(":disabled") so that fieldset[disabled] works) - rsubmittable.test(this.nodeName) && !rsubmitterTypes.test(type) && // only serialize submittable fields (and not buttons) - (this.checked || !rcheckableType.test(type) || f.getCheckboxUncheckedValue($el, opts) != null); // skip unchecked checkboxes (unless using opts) - - }).map(function(_i, el) { - var $el = $(this); - var val = $el.val(); - var type = this.type; // "input", "select", "textarea", "checkbox", etc. - - if (val == null) { - return null; - } - - if (rcheckableType.test(type) && !this.checked) { - val = f.getCheckboxUncheckedValue($el, opts); - } - - if (isArray(val)) { - return $.map(val, function(val) { - return { name: el.name, value: val.replace(rCRLF, "\r\n"), el: el }; - } ); - } - - return { name: el.name, value: val.replace(rCRLF, "\r\n"), el: el }; - - }).get(); - }, - - getCheckboxUncheckedValue: function($el, opts) { - var val = $el.attr("data-unchecked-value"); - if (val == null) { - val = opts.checkboxUncheckedValue; - } - return val; - }, - - // Parse value with type function - applyTypeFunc: function(name, strVal, type, el, typeFunctions) { - var typeFunc = typeFunctions[type]; - if (!typeFunc) { // quick feedback to user if there is a typo or missconfiguration - throw new Error("serializeJSON ERROR: Invalid type " + type + " found in input name '" + name + "', please use one of " + objectKeys(typeFunctions).join(", ")); - } - return typeFunc(strVal, el); - }, - - // Splits a field name into the name and the type. Examples: - // "foo" => ["foo", ""] - // "foo:boolean" => ["foo", "boolean"] - // "foo[bar]:null" => ["foo[bar]", "null"] - splitType : function(name) { - var parts = name.split(":"); - if (parts.length > 1) { - var t = parts.pop(); - return [parts.join(":"), t]; - } else { - return [name, ""]; - } - }, - - // Check if this input should be skipped when it has a falsy value, - // depending on the options to skip values by name or type, and the data-skip-falsy attribute. - shouldSkipFalsy: function(name, nameSansType, type, el, opts) { - var skipFromDataAttr = $(el).attr("data-skip-falsy"); - if (skipFromDataAttr != null) { - return skipFromDataAttr !== "false"; // any value is true, except the string "false" - } - - var optForFields = opts.skipFalsyValuesForFields; - if (optForFields && (optForFields.indexOf(nameSansType) !== -1 || optForFields.indexOf(name) !== -1)) { - return true; - } - - var optForTypes = opts.skipFalsyValuesForTypes; - if (optForTypes && optForTypes.indexOf(type) !== -1) { - return true; - } - - return false; - }, - - // Split the input name in programatically readable keys. - // Examples: - // "foo" => ["foo"] - // "[foo]" => ["foo"] - // "foo[inn][bar]" => ["foo", "inn", "bar"] - // "foo[inn[bar]]" => ["foo", "inn", "bar"] - // "foo[inn][arr][0]" => ["foo", "inn", "arr", "0"] - // "arr[][val]" => ["arr", "", "val"] - splitInputNameIntoKeysArray: function(nameWithNoType) { - var keys = nameWithNoType.split("["); // split string into array - keys = $.map(keys, function (key) { return key.replace(/\]/g, ""); }); // remove closing brackets - if (keys[0] === "") { keys.shift(); } // ensure no opening bracket ("[foo][inn]" should be same as "foo[inn]") - return keys; - }, - - // Set a value in an object or array, using multiple keys to set in a nested object or array. - // This is the main function of the script, that allows serializeJSON to use nested keys. - // Examples: - // - // deepSet(obj, ["foo"], v) // obj["foo"] = v - // deepSet(obj, ["foo", "inn"], v) // obj["foo"]["inn"] = v // Create the inner obj["foo"] object, if needed - // deepSet(obj, ["foo", "inn", "123"], v) // obj["foo"]["arr"]["123"] = v // - // - // deepSet(obj, ["0"], v) // obj["0"] = v - // deepSet(arr, ["0"], v, {useIntKeysAsArrayIndex: true}) // arr[0] = v - // deepSet(arr, [""], v) // arr.push(v) - // deepSet(obj, ["arr", ""], v) // obj["arr"].push(v) - // - // arr = []; - // deepSet(arr, ["", v] // arr => [v] - // deepSet(arr, ["", "foo"], v) // arr => [v, {foo: v}] - // deepSet(arr, ["", "bar"], v) // arr => [v, {foo: v, bar: v}] - // deepSet(arr, ["", "bar"], v) // arr => [v, {foo: v, bar: v}, {bar: v}] - // - deepSet: function (o, keys, value, opts) { - if (opts == null) { opts = {}; } - var f = $.serializeJSON; - if (isUndefined(o)) { throw new Error("ArgumentError: param 'o' expected to be an object or array, found undefined"); } - if (!keys || keys.length === 0) { throw new Error("ArgumentError: param 'keys' expected to be an array with least one element"); } - - var key = keys[0]; - - // Only one key, then it's not a deepSet, just assign the value in the object or add it to the array. - if (keys.length === 1) { - if (key === "") { // push values into an array (o must be an array) - o.push(value); - } else { - o[key] = value; // keys can be object keys (strings) or array indexes (numbers) - } - return; - } - - var nextKey = keys[1]; // nested key - var tailKeys = keys.slice(1); // list of all other nested keys (nextKey is first) - - if (key === "") { // push nested objects into an array (o must be an array) - var lastIdx = o.length - 1; - var lastVal = o[lastIdx]; - - // if the last value is an object or array, and the new key is not set yet - if (isObject(lastVal) && isUndefined(f.deepGet(lastVal, tailKeys))) { - key = lastIdx; // then set the new value as a new attribute of the same object - } else { - key = lastIdx + 1; // otherwise, add a new element in the array - } - } - - if (nextKey === "") { // "" is used to push values into the nested array "array[]" - if (isUndefined(o[key]) || !isArray(o[key])) { - o[key] = []; // define (or override) as array to push values - } - } else { - if (opts.useIntKeysAsArrayIndex && isValidArrayIndex(nextKey)) { // if 1, 2, 3 ... then use an array, where nextKey is the index - if (isUndefined(o[key]) || !isArray(o[key])) { - o[key] = []; // define (or override) as array, to insert values using int keys as array indexes - } - } else { // nextKey is going to be the nested object's attribute - if (isUndefined(o[key]) || !isObject(o[key])) { - o[key] = {}; // define (or override) as object, to set nested properties - } - } - } - - // Recursively set the inner object - f.deepSet(o[key], tailKeys, value, opts); - }, - - deepGet: function (o, keys) { - var f = $.serializeJSON; - if (isUndefined(o) || isUndefined(keys) || keys.length === 0 || (!isObject(o) && !isArray(o))) { - return o; - } - var key = keys[0]; - if (key === "") { // "" means next array index (used by deepSet) - return undefined; - } - if (keys.length === 1) { - return o[key]; - } - var tailKeys = keys.slice(1); - return f.deepGet(o[key], tailKeys); - } - }; - - // polyfill Object.keys to get option keys in IE<9 - var objectKeys = function(obj) { - if (Object.keys) { - return Object.keys(obj); - } else { - var key, keys = []; - for (key in obj) { keys.push(key); } - return keys; - } - }; - - var isObject = function(obj) { return obj === Object(obj); }; // true for Objects and Arrays - var isUndefined = function(obj) { return obj === void 0; }; // safe check for undefined values - var isValidArrayIndex = function(val) { return /^[0-9]+$/.test(String(val)); }; // 1,2,3,4 ... are valid array indexes - var isArray = Array.isArray || function(obj) { return Object.prototype.toString.call(obj) === "[object Array]"; }; -})); \ No newline at end of file diff --git a/lxconsole/static/js/sidebar.js b/lxconsole/static/js/sidebar.js index 4ea117a..12d8b19 100644 --- a/lxconsole/static/js/sidebar.js +++ b/lxconsole/static/js/sidebar.js @@ -30,6 +30,7 @@ function configureNavbarForServers(){ function configureSidebarForServers(){ $("#clusterMembersLinkSidebar").hide() $("#clusterGroupsLinkSidebar").hide() + $("#clusterGroupsLinkSidebar").hide() $("#instanceSidebarLinks").hide() $("#coreSidebarLinks").hide() $("#networkSidebarLinks").hide() @@ -38,6 +39,7 @@ function configureSidebarForServers(){ function populateSidebarLinks(){ $("#clusterMembersLinkSidebar").show() + $("#clusterGroupsLinkSidebar").show() $("#instanceSidebarLinks").show() $("#coreSidebarLinks").show() $("#networkSidebarLinks").show() @@ -60,6 +62,7 @@ function applySidebarLinks() { $("#networksLinkSidebar").attr("href", "networks?id=" + encodeURI(serverId) + "&project=" + encodeURI(project)); $("#storagePoolsLinkSidebar").attr("href", "storage-pools?id=" + encodeURI(serverId) + "&project=" + encodeURI(project)); $("#clusterMembersLinkSidebar").attr("href", "cluster-members?id=" + encodeURI(serverId) + "&project=" + encodeURI(project)); + $("#clusterGroupsLinkSidebar").attr("href", "cluster-groups?id=" + encodeURI(serverId) + "&project=" + encodeURI(project)); $("#projectsLinkSidebar").attr("href", "projects?id=" + encodeURI(serverId) + "&project=" + encodeURI(project)); $("#networkAclsLinkSidebar").attr("href", "network-acls?id=" + encodeURI(serverId) + "&project=" + encodeURI(project)); $("#operationsLinkSidebar").attr("href", "operations?id=" + encodeURI(serverId) + "&project=" + encodeURI(project)); diff --git a/lxconsole/templates/access-controls.html b/lxconsole/templates/access-controls.html index b691636..5031f8b 100644 --- a/lxconsole/templates/access-controls.html +++ b/lxconsole/templates/access-controls.html @@ -162,7 +162,7 @@ // Add access control function addAccessControl(){ console.log("Info: adding new group"); - data = $('#addForm').serializeJSON(); + data = $('#addForm').serialize(); $.post("../api/access-controls/add_access_control", data, function (data) { console.log(data); if (data.error_code >= 400){ @@ -192,7 +192,7 @@ // Update access control function updateAccessControl(){ console.log("Info: adding new access control"); - data = $('#editForm').serializeJSON(); + data = $('#editForm').serialize(); $.post("../api/access-controls/update_access_control", data, function (data) { console.log(data); if (data.error_code >= 400){ diff --git a/lxconsole/templates/certificates.html b/lxconsole/templates/certificates.html index e771bd8..55c6141 100644 --- a/lxconsole/templates/certificates.html +++ b/lxconsole/templates/certificates.html @@ -168,7 +168,7 @@ function addItem(){ console.log("Info: adding new certificate"); - data = $('#addForm').serializeJSON(); + data = $('#addForm').serialize(); $.post("../api/certificates/add_certificate?id="+serverId+"&project="+project, data, function (data) { console.log(data) if (data.error_code >= 400){ diff --git a/lxconsole/templates/cluster-groups.html b/lxconsole/templates/cluster-groups.html new file mode 100644 index 0000000..657bbff --- /dev/null +++ b/lxconsole/templates/cluster-groups.html @@ -0,0 +1,266 @@ +{% extends "main.html" %} + +{% block header %} +
+
+

{{ page_title | safe }}

+
+ +
+{% endblock header %} + +{% block content %} +
+
+
+

Cluster Groups

+
+ +
+
+
+ +
+
+
+
+{% endblock content %} + +{% block modal %} + {% include 'modals/cluster-groups.html' %} +{% endblock modal %} + +{% block script %} + +{% endblock script %} \ No newline at end of file diff --git a/lxconsole/templates/cluster-members.html b/lxconsole/templates/cluster-members.html index 0a77ffe..37a3bed 100644 --- a/lxconsole/templates/cluster-members.html +++ b/lxconsole/templates/cluster-members.html @@ -119,6 +119,15 @@ return '-' }, }, + { title: "Roles", data: function (row, type, set) { + if (row.hasOwnProperty('roles')) { + if (row.roles.length > 0){ + return row.roles.join(',
') + } + } + return '-' + }, + }, { title: "URL", data: function (row, type, set) { if (row.hasOwnProperty('url')) { if (row.url) @@ -144,16 +153,24 @@ }, }, - // { title: "Actions", data: function (row, type, set) { - // links = '' - // if (row.hasOwnProperty('server_name')) { - // links += '' + - // ' ' + ' ' + - // '' - // } - // return links - // }, - // }, + { title: "Actions", data: function (row, type, set) { + links = '' + if (row.hasOwnProperty('status')) { + links += '' + links += ' ' + ' ' + if (row.status == 'Online'){ + links += '' + links += ' ' + ' ' + } + if (row.status == 'Evacuated'){ + links += '' + links += ' ' + ' ' + } + links += '' + } + return links + }, + }, ], order: [], @@ -166,7 +183,7 @@ function addItem(){ console.log("Info: adding new item"); - data = $('#addForm').serializeJSON(); + data = $('#addForm').serialize(); $.post("../api/cluster-members/add_cluster_member?id="+serverId+"&project="+project, data, function (data) { console.log(data) if (data.error_code >= 400){ @@ -177,6 +194,22 @@ }); } + function changeItemState(name, action){ + console.log("Info: confirming " + action + " action of cluster member " + name); + if (confirm("Are you sure you want to " + action + " cluster member " + name + "?") == true) { + console.log("Info: " + name + " " + action + " action started"); + $.post("../api/cluster-members/change_cluster_member_state?id=" + serverId + "&project=" + project, { name: name, action: action }, function (data) { + console.log(data); + if (data.error_code >= 400){ + alert(data.error); + } + //Aync type + setTimeout(() => { reloadPageContent(); }, 2000); + operationStatusCheck() + }); + } + } + function confirmDeleteItem(name){ console.log("Info: confirming deletion of item " + name); $("#deleteQuestionText").text("Are you sure you want to remove " + name + " from the cluster?"); @@ -228,10 +261,25 @@ }); } + function renameItem(){ + name = editedClusterMember + console.log("Info: renaming cluster group"); + data = $('#renameForm').serialize(); + $.post("../api/cluster-members/update_cluster_member?id=" + serverId + "&project=" + project + "&name=" + encodeURI(name), data, function (data) { + console.log(data) + if (data.error_code >= 400){ + alert(data.error); + } + //Sync type + reloadPageContent(); + }); + } + + function updateItem(){ name = editedClusterMember var updatedJSON = $("#jsonInput").val(); - console.log("Info: updating cluster membere"); + console.log("Info: updating cluster member"); $.post("../api/cluster-members/update_cluster_member?id=" + serverId + "&project=" + project + "&name=" + encodeURI(name), { json: updatedJSON }, function (data) { console.log(data); if (data.error_code >= 400){ diff --git a/lxconsole/templates/container.html b/lxconsole/templates/container.html index 7b10022..1ee7a66 100644 --- a/lxconsole/templates/container.html +++ b/lxconsole/templates/container.html @@ -3125,7 +3125,7 @@ // Update the instance from the edit instance modal form function updateInstanceForm(){ console.log("Info: updating instance " + instance); - data = $('#editForm').serializeJSON(); + data = $('#editForm').serialize(); $.post("../api/container/update_instance?id=" + serverId + "&project=" + project + "&instance=" + encodeURI(instance), data, function (data) { console.log(data); if (data.error_code >= 400){ diff --git a/lxconsole/templates/containers.html b/lxconsole/templates/containers.html index 36f2482..f83fa0b 100644 --- a/lxconsole/templates/containers.html +++ b/lxconsole/templates/containers.html @@ -116,6 +116,13 @@ $('#containerLocationInput').append(''); } }) + $.getJSON("../api/cluster-groups/list_cluster_groups?id="+serverId+"&project="+project, function (data) { + data = data.metadata + for (var index = 0; index < data.length; index++) { + optionText = data[index].replace('/1.0/cluster/groups/',''); + $('#containerLocationInput').append(''); + } + }) //Populate the modal Image dropdown $.getJSON("../api/images/list_images?id="+serverId+"&project="+project+"&recursion=1", function (data) { @@ -264,7 +271,7 @@ // Add instance function addItem(){ console.log("Info: adding new container"); - data = $('#addForm').serializeJSON(); + data = $('#addForm').serialize(); $.post("../api/containers/add_instance?id="+serverId+"&project="+project, data, function (data) { console.log(data); if (data.error_code >= 400){ diff --git a/lxconsole/templates/groups.html b/lxconsole/templates/groups.html index a7ae9e8..b853424 100644 --- a/lxconsole/templates/groups.html +++ b/lxconsole/templates/groups.html @@ -104,7 +104,7 @@ // Add group function addGroup(){ console.log("Info: adding new group"); - data = $('#addForm').serializeJSON(); + data = $('#addForm').serialize(); $.post("../api/groups/add_group", data, function (data) { console.log(data); if (data.error_code >= 400){ @@ -133,7 +133,7 @@ // Update group function updateGroup(){ console.log("Info: adding new group"); - data = $('#editForm').serializeJSON(); + data = $('#editForm').serialize(); $.post("../api/groups/update_group", data, function (data) { console.log(data); if (data.error_code >= 400){ diff --git a/lxconsole/templates/images.html b/lxconsole/templates/images.html index 02755fb..cbf8e2d 100644 --- a/lxconsole/templates/images.html +++ b/lxconsole/templates/images.html @@ -254,7 +254,7 @@ function addItem(){ console.log("Info: downloading image"); - data = $('#addForm').serializeJSON(); + data = $('#addForm').serialize(); $.post("../api/images/add_image?id="+serverId+"&project="+project, data, function (data) { console.log(data); if (data.error_code >= 400){ diff --git a/lxconsole/templates/login.html b/lxconsole/templates/login.html index 28449df..cecf608 100644 --- a/lxconsole/templates/login.html +++ b/lxconsole/templates/login.html @@ -147,7 +147,6 @@ - diff --git a/lxconsole/templates/main.html b/lxconsole/templates/main.html index 209af42..ac03206 100644 --- a/lxconsole/templates/main.html +++ b/lxconsole/templates/main.html @@ -40,7 +40,7 @@
- Version 0.1.0 + Version 0.2.0
Copyright © 2020-Present Penning Labs. All rights reserved.
@@ -60,7 +60,6 @@ - diff --git a/lxconsole/templates/modals/cluster-groups.html b/lxconsole/templates/modals/cluster-groups.html new file mode 100644 index 0000000..b2f9638 --- /dev/null +++ b/lxconsole/templates/modals/cluster-groups.html @@ -0,0 +1,171 @@ + + + + + + + + \ No newline at end of file diff --git a/lxconsole/templates/modals/cluster-members.html b/lxconsole/templates/modals/cluster-members.html index 0af21c6..fd80f59 100644 --- a/lxconsole/templates/modals/cluster-members.html +++ b/lxconsole/templates/modals/cluster-members.html @@ -82,21 +82,55 @@ diff --git a/lxconsole/templates/modals/containers.html b/lxconsole/templates/modals/containers.html index e8a5812..867d116 100644 --- a/lxconsole/templates/modals/containers.html +++ b/lxconsole/templates/modals/containers.html @@ -257,9 +257,10 @@
- +
+