This commit is contained in:
matthewalanpenning 2023-06-04 15:52:38 -04:00
parent d5c8fe5668
commit bdc878e1b6
38 changed files with 815 additions and 394 deletions

View file

@ -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

View file

@ -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/<endpoint>', view_func=containers.api_containers_e
api.add_url_rule('/servers/<endpoint>', view_func=servers.api_servers_endpoint, methods=['GET', 'POST'])
api.add_url_rule('/server/<endpoint>', view_func=server.api_server_endpoint)
api.add_url_rule('/certificates/<endpoint>', view_func=certificates.api_certificates_endpoint, methods=['GET', 'POST'])
api.add_url_rule('/cluster-groups/<endpoint>', view_func=cluster_groups.api_cluster_groups_endpoint, methods=['GET', 'POST'])
api.add_url_rule('/cluster-members/<endpoint>', view_func=cluster_members.api_cluster_members_endpoint, methods=['GET', 'POST'])
api.add_url_rule('/images/<endpoint>', view_func=images.api_images_endpoint, methods=['GET', 'POST'])
api.add_url_rule('/networks/<endpoint>', view_func=networks.api_networks_endpoint, methods=['GET', 'POST'])

View file

@ -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',

View file

@ -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

View file

@ -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

View file

@ -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():

View file

@ -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]"; };
}));

View file

@ -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));

View file

@ -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){

View file

@ -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){

View file

@ -0,0 +1,266 @@
{% extends "main.html" %}
{% block header %}
<div class="row mb-2">
<div class="col-sm-6">
<h1>{{ page_title | safe }}</h1>
</div>
<div class="col-sm-6">
<a class="btn btn-outline-primary float-sm-right mr-4" href="#" data-toggle="modal" data-target="#addModal" title="Add Cluster Group" aria-hidden="true">
<i class="fas fa-plus fa-sm fa-fw"></i> Cluster Group
</a>
</div>
</div>
{% endblock header %}
{% block content %}
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Cluster Groups</h3>
<div class="card-tools">
<button type="button" class="btn btn-tool" onclick="reloadPageContent()" title="Refresh">
<i class="fas fa-sync"></i>
</button>
</div>
</div>
<div class="card-body">
<table class="table table-hover" id="myDataTable" width="100%" cellspacing="0">
</table>
</div>
</div>
</div>
{% endblock content %}
{% block modal %}
{% include 'modals/cluster-groups.html' %}
{% endblock modal %}
{% block script %}
<script>
var reloadTime = 10000;
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const serverId = urlParams.get('id');
const project = urlParams.get('project');
var editedClusterMember = ''
applySidebarStyles();
applySidebarLinks();
populateSidebarLinks();
populateNavbarLinks();
function reloadPageContent() {
//Clear the automatic page reload
clearTimeout(pageReloadTimeout);
//Reload the datatables content
$('#myDataTable').DataTable().ajax.reload(null, false);
//Set the automatic page reload
pageReloadTimeout = setTimeout(() => { reloadPageContent(); }, reloadTime);
}
function loadPageContent(){
//Display the current project
$("#selectedProject").text(project);
//Populate the Server dropdown
$.getJSON("../api/servers/list_servers?id="+serverId, function (data) {
data = data.data
for (var index = 0; index < data.length; index++) {
if (data[index].name == '')
optionText = data[index].addr
else
optionText = data[index].name
if (data[index].id == serverId)
$('#serverListNav').append('<option value="' + data[index].id + '" selected="selected">' + optionText + '</option>');
else
$('#serverListNav').append('<option value="' + data[index].id + '">' + optionText + '</option>');
}
})
//Populate the Project dropdown
$.getJSON("../api/projects/list_projects?id="+serverId+"&project="+project, function (data) {
data = data.metadata
for (var index = 0; index < data.length; index++) {
optionText = data[index].replace('/1.0/projects/','');
if (optionText == project)
$('#projectListNav').append('<option value="' + optionText + '" selected="selected">' + optionText + '</option>');
else
$('#projectListNav').append('<option value="' + optionText + '">' + optionText + '</option>');
}
})
// Configure Datatable
$('#myDataTable').DataTable({
ajax: {
url: "../api/cluster-groups/list_cluster_groups?id="+serverId+"&project=" + project + "&recursion=1",
dataType: "json",
dataSrc: "metadata",
contentType: "application/json"
},
columns: [
{ title: "Name", data: function (row, type, set) {
if (row.hasOwnProperty('name')) {
if (row.name)
return row.name
}
return '-'
},
},
{ title: "Description", data: function (row, type, set) {
if (row.hasOwnProperty('description')) {
if (row.description)
return row.description
}
return '-'
},
},
{ title: "Members", data: function (row, type, set) {
if (row.hasOwnProperty('members')) {
if (Object.keys(row.members).length > 0) {
arr = []
for (let i = 0; i < row.members.length; i++) {
arr.push(row.members[i]);
}
return arr.join(", ")
}
}
return '-'
},
},
{ title: "Actions", data: function (row, type, set) {
links = ''
if (row.hasOwnProperty('name')) {
links += '<a href="#" onclick=editItem(\''+row.name+'\')><i class="fas fa-edit fa-lg" style="color:#ddd" title="Edit" aria-hidden="true"></i></a>' +
'&nbsp' + '&nbsp' +
'<a href="#" onclick=confirmDeleteItem(\''+row.name+'\')><i class="fas fa-trash-alt fa-lg" style="color:#ddd" title="Delete" aria-hidden="true"></i></a>'
}
return links
},
},
],
order: [],
});
//Set reload page content
pageReloadTimeout = setTimeout(() => { reloadPageContent(); }, reloadTime);
}
function addItem(){
console.log("Info: adding new item");
data = $('#addForm').serialize();
$.post("../api/cluster-groups/add_cluster_group?id="+serverId+"&project="+project, data, function (data) {
console.log(data)
if (data.error_code >= 400){
alert(data.error);
}
//Sync type
reloadPageContent();
});
}
function confirmDeleteItem(name){
console.log("Info: confirming deletion of item " + name);
$("#deleteQuestionText").text("Are you sure you want to remove the cluster group " + name + "?");
$("#clusterGroup").val(name);
$("#deleteModal").modal('show');
}
function createItemUsingJSON(){
var json = $("#jsonCreateInput").val();
console.log("Info: adding new cluster group");
$.post("../api/cluster-groups/add_cluster_group?id="+serverId+"&project="+project, { json: json }, function (data) {
console.log(data);
if (data.error_code >= 400){
alert(data.error);
}
//Sync type
reloadPageContent();
});
}
function deleteItem(){
name = $("#clusterGroup").val();
console.log("Info: deleting item " + name);
$.post("../api/cluster-groups/delete_cluster_group?id=" + serverId + "&project=" + project, { name: name }, function (data) {
console.log(data);
if (data.error_code >= 400){
alert(data.error);
}
//Sync type
reloadPageContent();
});
}
function editItem(name){
editedClusterMember = name
console.log("Info: loading cluster member " + name);
$.post("../api/cluster-groups/load_cluster_group?id=" + serverId + "&project=" + project, { name: name }, function (data) {
console.log(data);
if (data.error_code >= 400){
alert(data.error);
}
$("#storagePoolNameEditInput").text("Name: " + name);
$("#jsonInput").val(JSON.stringify(data.metadata, null, 2));
$("#editModal").modal('show');
});
}
function renameItem(){
name = editedClusterMember
console.log("Info: renaming cluster group");
data = $('#renameForm').serialize();
$.post("../api/cluster-groups/update_cluster_group?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 group");
$.post("../api/cluster-groups/update_cluster_group?id=" + serverId + "&project=" + project + "&name=" + encodeURI(name), { json: updatedJSON }, function (data) {
console.log(data);
if (data.error_code >= 400){
alert(data.error);
}
//Sync type
reloadPageContent();
});
}
$(document).ready(function(){
//If id or project variables are missing redirect to servers page
if (!serverId || !project) {
window.location.href = 'servers';
}
else {
loadPageContent()
operationStatusCheck()
//Populate the Cluster Members Dropdown in Add Modal
$.getJSON("../api/cluster-members/list_cluster_members?id="+serverId+"&project="+project, function (data) {
data = data.metadata
for (var index = 0; index < data.length; index++) {
optionText = data[index].replace('/1.0/cluster/members/','');
$('#clusterMembersInput').append('<option value="' + optionText + '">' + optionText + '</option>');
}
})
}
});
</script>
{% endblock script %}

View file

@ -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(', <br />')
}
}
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 += '<a href="#" onclick=editItem(\''+row.name+'\')><i class="fas fa-edit fa-lg" style="color:#ddd" title="Edit" aria-hidden="true"></i></a>' +
// '&nbsp' + '&nbsp' +
// '<a href="#" onclick=confirmDeleteItem(\''+row.name+'\')><i class="fas fa-trash-alt fa-lg" style="color:#ddd" title="Delete" aria-hidden="true"></i></a>'
// }
// return links
// },
// },
{ title: "Actions", data: function (row, type, set) {
links = ''
if (row.hasOwnProperty('status')) {
links += '<a href="#" onclick=editItem(\''+row.server_name+'\')><i class="fas fa-edit fa-lg" style="color:#ddd" title="Edit" aria-hidden="true"></i></a>'
links += '&nbsp' + '&nbsp'
if (row.status == 'Online'){
links += '<a href="#" onclick=changeItemState(\''+row.server_name+'\',\'evacuate\')><i class="fas fa-sign-out-alt fa-lg" style="color:#ddd" title="Evacuate" aria-hidden="true"></i></a>'
links += '&nbsp' + '&nbsp'
}
if (row.status == 'Evacuated'){
links += '<a href="#" onclick=changeItemState(\''+row.server_name+'\',\'restore\')><i class="fas fa-sign-in-alt fa-lg" style="color:#ddd" title="Restore" aria-hidden="true"></i></a>'
links += '&nbsp' + '&nbsp'
}
links += '<a href="#" onclick=confirmDeleteItem(\''+row.server_name+'\')><i class="fas fa-trash-alt fa-lg" style="color:#ddd" title="Delete" aria-hidden="true"></i></a>'
}
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){

View file

@ -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){

View file

@ -116,6 +116,13 @@
$('#containerLocationInput').append('<option value="' + optionText + '">' + optionText + '</option>');
}
})
$.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('<option value="@' + optionText + '">@' + optionText + '</option>');
}
})
//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){

View file

@ -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){

View file

@ -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){

View file

@ -147,7 +147,6 @@
<script src="../static/plugins/datatables-responsive/js/dataTables.responsive.min.js"></script>
<script src="../static/plugins/datatables-responsive/js/responsive.bootstrap4.min.js"></script>
<script src="../static/plugins/jquery-knob/jquery.knob.min.js"></script>
<script src="../static/js/serializejson.js"></script>
<!-- AdminLTE App -->
<script src="../static/dist/js/adminlte.min.js"></script>

View file

@ -40,7 +40,7 @@
<footer class="main-footer">
<div class="float-right d-none d-sm-block">
Version 0.1.0
Version 0.2.0
</div>
Copyright &copy; 2020-Present <a href="https://penninglabs.com">Penning Labs</a>. All rights reserved.
</footer>
@ -60,7 +60,6 @@
<script src="../static/plugins/datatables-responsive/js/dataTables.responsive.min.js"></script>
<script src="../static/plugins/datatables-responsive/js/responsive.bootstrap4.min.js"></script>
<script src="../static/plugins/jquery-knob/jquery.knob.min.js"></script>
<script src="../static/js/serializejson.js"></script>
<!-- AdminLTE App -->
<script src="../static/dist/js/adminlte.min.js"></script>

View file

@ -0,0 +1,171 @@
<!-- Add Modal-->
<div class="modal fade" id="addModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Add Cluster Group</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="form-tab" data-toggle="tab" href="#form" role="tab" aria-controls="form" aria-selected="true">Form</a>
</li>
<li class="nav-item">
<a class="nav-link" id="json-tab" data-toggle="tab" href="#json" role="tab" aria-controls="json" aria-selected="false">JSON</a>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="form" role="tabpanel" aria-labelledby="form-tab">
<form id="addForm">
<br />
<div class="row">
<label class="col-3 col-form-label text-right">Name: <span class="text-danger">*</span></label>
<div class="col-7">
<div class="form-group">
<input type="text" class="form-control" placeholder="" name="name">
</div>
</div>
<div class="col-1">
<i class="far fa-sm fa-question-circle" title='(Required) - Enter in the name of the cluster group.'></i>
</div>
</div>
<div class="row">
<label class="col-3 col-form-label text-right">Description: </label>
<div class="col-7">
<div class="form-group">
<input type="text" class="form-control" placeholder="" name="description">
</div>
</div>
<div class="col-1">
<i class="far fa-sm fa-question-circle" title='Enter in a description for the cluster group.'></i>
</div>
</div>
<div class="row">
<label class="col-3 col-form-label text-right">Cluster Members: </label>
<div class="col-7">
<div class="form-group">
<select id="clusterMembersInput" class="select form-control" multiple name="members">
</select>
</div>
</div>
<div class="col-1">
<i class="far fa-sm fa-question-circle" title='Select cluster members to add to this cluster group. Use Ctrl key for multiple select'></i>
</div>
</div>
</form>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
<a class="btn btn-primary" href="#" onclick="addItem()" data-dismiss="modal">Submit</a>
</div>
</div>
<div class="tab-pane fade" id="json" role="tabpanel" aria-labelledby="json-tab">
<br />
<div class="row">
<div class="col-12">
<div class="form-group text-right">
<pre>
<textarea name="json" class="form-control" id="jsonCreateInput" rows="16" placeholder="Enter JSON data"></textarea>
</pre>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
<a class="btn btn-primary" href="#" onclick="createItemUsingJSON()" data-dismiss="modal">Submit</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Modal-->
<div class="modal fade" id="editModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Edit Cluster Group</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="json-edit-tab" data-toggle="tab" href="#json-edit" role="tab" aria-controls="json-edit" aria-selected="false">JSON</a>
</li>
<li class="nav-item">
<a class="nav-link" id="rename-edit-tab" data-toggle="tab" href="#rename-edit" role="tab" aria-controls="rename-edit" aria-selected="true">Rename</a>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="json-edit" role="tabpanel" aria-labelledby="json-edit-tab">
<br />
<div class="row">
<label class="col-4 col-form-label" id="itemNameEditInput"></label>
<div class="col-12">
<div class="form-group text-right">
<pre>
<textarea name="json" class="form-control" id="jsonInput" rows="16" ></textarea>
</pre>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
<a class="btn btn-primary" href="#" onclick="updateItem()" data-dismiss="modal">Submit</a>
</div>
</div>
<div class="tab-pane fade" id="rename-edit" role="tabpanel" aria-labelledby="rename-edit-tab">
<form id="renameForm">
<br />
<div class="row">
<label class="col-2 col-form-label text-right">Name:</label>
<div class="col-8">
<div class="form-group">
<input type="text" class="form-control" placeholder="" name="name">
</div>
</div>
<div class="col-1">
<i class="far fa-sm fa-question-circle" title='Enter in the new name of the cluster group.'></i>
</div>
</div>
</form>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
<a class="btn btn-primary" href="#" onclick="renameItem()" data-dismiss="modal">Submit</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Delete Modal-->
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Delete Cluster Group</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="mb-2" id="deleteQuestionText">
Are you sure you want to delete this cluster group?
</div>
<input type="hidden" id="clusterGroup" class="form-control" name="server_name">
</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
<a class="btn btn-primary" href="#" onclick="deleteItem()" data-dismiss="modal">Yes</a>
</div>
</div>
</div>
</div>

View file

@ -82,6 +82,17 @@
</button>
</div>
<div class="modal-body">
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="json-edit-tab" data-toggle="tab" href="#json-edit" role="tab" aria-controls="json-edit" aria-selected="false">JSON</a>
</li>
<li class="nav-item">
<a class="nav-link" id="rename-edit-tab" data-toggle="tab" href="#rename-edit" role="tab" aria-controls="rename-edit" aria-selected="true">Rename</a>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="json-edit" role="tabpanel" aria-labelledby="json-edit-tab">
<br />
<div class="row">
<label class="col-4 col-form-label" id="itemNameEditInput"></label>
<div class="col-12">
@ -92,12 +103,35 @@
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
<a class="btn btn-primary" href="#" onclick="updateItem()" data-dismiss="modal">Submit</a>
</div>
</div>
<div class="tab-pane fade" id="rename-edit" role="tabpanel" aria-labelledby="rename-edit-tab">
<form id="renameForm">
<br />
<div class="row">
<label class="col-2 col-form-label text-right">Name:</label>
<div class="col-8">
<div class="form-group">
<input type="text" class="form-control" placeholder="" name="server_name">
</div>
<span>After renaming a cluster member, restart the LXD service on the host to clear cached name information.</span>
</div>
<div class="col-1">
<i class="far fa-sm fa-question-circle" title='Enter in the new name of the cluster member. Host alias will apply rename changes after LXD services have restarted on host.'></i>
</div>
</div>
</form>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
<a class="btn btn-primary" href="#" onclick="renameItem()" data-dismiss="modal">Submit</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -257,9 +257,10 @@
</div>
</div>
<div class="col-1">
<i class="far fa-sm fa-question-circle" title='Select the LXD cluster member to deploy this container on. Default: none'></i>
<i class="far fa-sm fa-question-circle" title='Select the LXD cluster member or @group to deploy this container on. Default: none'></i>
</div>
</div>
<hr class="mb-2">
<nav>
<div class="nav nav-pills justify-content-center" id="nav-tab" role="tablist">

View file

@ -13,7 +13,7 @@
<div class="col-12">
<p>Lxconsole is an open source management console providing a web-based user interface capable of managing multiple LXD servers from a single location.</p>
<p>
<strong>Version</strong>: <span id="versionNumber">v0.1.0</span> <br />
<strong>Version</strong>: <span id="versionNumber">v0.2.0</span> <br />
<strong>License</strong>: AGPL-3.0 <br />
<strong>URL</strong>: https://lxconsole.com <br />
</p>

View file

@ -257,9 +257,10 @@
</div>
</div>
<div class="col-1">
<i class="far fa-sm fa-question-circle" title='Select the LXD cluster member to deploy this instance on. Default: none'></i>
<i class="far fa-sm fa-question-circle" title='Select the LXD cluster member or @group to deploy this container on. Default: none'></i>
</div>
</div>
<hr class="mb-2">
<nav>
<div class="nav nav-pills justify-content-center" id="nav-tab" role="tablist">

View file

@ -340,7 +340,7 @@
function addItem(){
console.log("Info: adding new network-acl");
data = $('#addForm').serializeJSON();
data = $('#addForm').serialize();
$.post("../api/network-acl/add_network_acl?id="+serverId+"&project="+project+"&acl="+acl, data, function (data) {
console.log(data)
if (data.error_code >= 400){

View file

@ -175,7 +175,7 @@
function addItem(){
console.log("Info: adding new network-acl");
data = $('#addForm').serializeJSON();
data = $('#addForm').serialize();
$.post("../api/network-acls/add_network_acl?id="+serverId+"&project="+project, data, function (data) {
console.log(data)
if (data.error_code >= 400){

View file

@ -190,7 +190,7 @@
function addItem(){
console.log("Info: adding new network");
data = $('#addForm').serializeJSON();
data = $('#addForm').serialize();
$.post("../api/networks/add_network?id="+serverId+"&project="+project, data, function (data) {
console.log(data)
if (data.error_code >= 400){

View file

@ -153,7 +153,7 @@
function addItem(){
console.log("Info: adding new profile");
data = $('#addForm').serializeJSON();
data = $('#addForm').serialize();
$.post("../api/profiles/add_profile?id="+serverId+"&project="+project, data, function (data) {
console.log(data)
if (data.error_code >= 400){

View file

@ -207,7 +207,7 @@
function addItem(){
console.log("Info: adding new project");
data = $('#addForm').serializeJSON();
data = $('#addForm').serialize();
$.post("../api/projects/add_project?id="+serverId+"&project="+project, data, function (data) {
console.log(data)
if (data.error_code >= 400){

View file

@ -165,7 +165,6 @@
<script src="../static/plugins/datatables-responsive/js/dataTables.responsive.min.js"></script>
<script src="../static/plugins/datatables-responsive/js/responsive.bootstrap4.min.js"></script>
<script src="../static/plugins/jquery-knob/jquery.knob.min.js"></script>
<script src="../static/js/serializejson.js"></script>
<!-- AdminLTE App -->
<script src="../static/dist/js/adminlte.min.js"></script>

View file

@ -108,7 +108,7 @@
// Add user
function addRole(){
console.log("Info: adding new role");
data = $('#addForm').serializeJSON();
data = $('#addForm').serialize();
$.post("../api/roles/add_role", data, function (data) {
console.log(data);
if (data.error_code >= 400){

View file

@ -36,6 +36,14 @@
</p>
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="clusterGroupsLinkSidebar" href="cluster-groups" style="display: none;">
<i id="clusterGroupsIcon" class="nav-icon fas fa-layer-group"></i>
<p id="clusterGroupsSpan">
Cluster Groups
</p>
</a>
</li>
</ul>
<ul id="instanceSidebarLinks" class="nav nav-pills nav-sidebar flex-column user-panel py-2" style="display: none;" data-widget="treeview" role="menu" data-accordion="false">

View file

@ -135,7 +135,7 @@
function addItem(){
console.log("Info: adding new simplestreams repo");
data = $('#addForm').serializeJSON();
data = $('#addForm').serialize();
$.post("../api/simplestreams/add_simplestream", data, function (data) {
console.log(data)
if (data.error_code >= 400){

View file

@ -174,7 +174,7 @@
function addItem(){
console.log("Info: adding new storage pool");
data = $('#addForm').serializeJSON();
data = $('#addForm').serialize();
$.post("../api/storage-pools/add_storage_pool?id="+serverId+"&project="+project, data, function (data) {
console.log(data)
if (data.error_code >= 400){

View file

@ -202,7 +202,7 @@
function addItem(){
console.log("Info: adding new storage volume");
data = $('#addForm').serializeJSON();
data = $('#addForm').serialize();
$.post("../api/storage-volumes/add_storage_volume?id="+serverId+"&project="+project+"&pool="+pool, data, function (data) {
console.log(data)
if (data.error_code >= 400){

View file

@ -133,7 +133,7 @@
// Add user
function addUser(){
console.log("Info: adding new user");
data = $('#addForm').serializeJSON();
data = $('#addForm').serialize();
$.post("../api/users/add_user", data, function (data) {
console.log(data);
if (data.error_code >= 400){
@ -182,7 +182,7 @@
// Update user
function updateUser(){
console.log("Info: adding new user");
data = $('#editForm').serializeJSON();
data = $('#editForm').serialize();
$.post("../api/users/update_user", data, function (data) {
console.log(data);
if (data.error_code >= 400){
@ -196,7 +196,7 @@
// Update user group
function updateUserGroup(){
console.log("Info: editing user group");
data = $('#editUserGroupsForm').serializeJSON();
data = $('#editUserGroupsForm').serialize();
$.post("../api/users/update_user", data, function (data) {
console.log(data);
if (data.error_code >= 400){
@ -210,7 +210,7 @@
// Update user password
function updateUserPassword(){
console.log("Info: editing user password");
data = $('#editUserPasswordForm').serializeJSON();
data = $('#editUserPasswordForm').serialize();
$.post("../api/users/update_user", data, function (data) {
console.log(data);
if (data.error_code >= 400){

View file

@ -2832,7 +2832,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/virtual-machine/update_instance?id=" + serverId + "&project=" + project + "&instance=" + encodeURI(instance), data, function (data) {
console.log(data);
if (data.error_code >= 400){

View file

@ -116,6 +116,13 @@
$('#virtualMachineLocationInput').append('<option value="' + optionText + '">' + optionText + '</option>');
}
})
$.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('<option value="@' + optionText + '">@' + optionText + '</option>');
}
})
//Populate the modal Image dropdown
$.getJSON("../api/images/list_images?id="+serverId+"&project="+project+"&recursion=1", function (data) {
@ -260,7 +267,7 @@
// Add instance
function addItem(){
console.log("Info: adding new virtual machine");
data = $('#addForm').serializeJSON();
data = $('#addForm').serialize();
$.post("../api/virtual-machines/add_instance?id="+serverId+"&project="+project, data, function (data) {
console.log(data);
if (data.error_code >= 400){

View file

@ -1,6 +1,3 @@
# 0.2.0
- Add cluster groups
# 0.3.0
- Add network-zones
- Add network-forwards