From a5220eb24372c9645937b08cfd1d766417b7032c Mon Sep 17 00:00:00 2001 From: Peter Piela Date: Sun, 25 Sep 2016 18:48:05 -0400 Subject: [PATCH] Added support for editing Ironic nodes - Split out base-node from enroll-node and use it to implement edit-node - Enrollemnt no longer requires that all required attributes be specified - Edit-node functionality supports instance_info properties Change-Id: Ied1f21c8790f0d9dc3a238defa930181789a7281 Co-Authored-By: Beth Elwell --- ironic_ui/api/ironic.py | 13 + ironic_ui/api/ironic_rest_api.py | 12 +- .../ironic/base-node/base-node.controller.js | 303 ++++++++ .../base-node.html} | 114 ++- .../ironic/base-node/base-node.service.js | 673 ++++++++++++++++++ .../base-node.spec.js} | 4 +- .../ironic/edit-node/edit-node.controller.js | 184 +++++ .../ironic/edit-node/edit-node.service.js | 204 ++++++ .../enroll-node/enroll-node.controller.js | 283 +------- .../ironic/enroll-node/enroll-node.service.js | 648 +---------------- .../dashboard/admin/ironic/ironic.module.js | 1 + .../dashboard/admin/ironic/ironic.service.js | 37 +- .../node-details/node-details.controller.js | 14 + .../ironic/node-details/node-details.html | 4 + .../node-details/sections/configuration.html | 9 - .../node-details/sections/overview.html | 4 +- .../ironic/node-list/node-list.controller.js | 17 +- .../admin/ironic/node-list/node-list.html | 6 + 18 files changed, 1565 insertions(+), 965 deletions(-) create mode 100644 ironic_ui/static/dashboard/admin/ironic/base-node/base-node.controller.js rename ironic_ui/static/dashboard/admin/ironic/{enroll-node/enroll-node.html => base-node/base-node.html} (73%) create mode 100644 ironic_ui/static/dashboard/admin/ironic/base-node/base-node.service.js rename ironic_ui/static/dashboard/admin/ironic/{enroll-node/enroll-node.spec.js => base-node/base-node.spec.js} (96%) create mode 100644 ironic_ui/static/dashboard/admin/ironic/edit-node/edit-node.controller.js create mode 100644 ironic_ui/static/dashboard/admin/ironic/edit-node/edit-node.service.js diff --git a/ironic_ui/api/ironic.py b/ironic_ui/api/ironic.py index a893665c..3eb5c173 100755 --- a/ironic_ui/api/ironic.py +++ b/ironic_ui/api/ironic.py @@ -162,6 +162,19 @@ def node_delete(request, node_id): return ironicclient(request).node.delete(node_id) +def node_update(request, node_id, patch): + """Update a specified node. + + :param request: HTTP request. + :param node_id: The UUID of the node. + :param patch: Sequence of update operations + :return: node. + + http://docs.openstack.org/developer/python-ironicclient/api/ironicclient.v1.node.html#ironicclient.v1.node.NodeManager.update + """ + ironicclient(request).node.update(node_id, patch) + + def driver_list(request): """Retrieve a list of drivers. diff --git a/ironic_ui/api/ironic_rest_api.py b/ironic_ui/api/ironic_rest_api.py index 7a1fc3a4..32dd4ade 100755 --- a/ironic_ui/api/ironic_rest_api.py +++ b/ironic_ui/api/ironic_rest_api.py @@ -69,11 +69,21 @@ class Node(generic.View): """Get information on a specific node. :param request: HTTP request. - :param node_id: Node name or uuid. + :param node_id: Node id. :return: node. """ return ironic.node_get(request, node_id).to_dict() + @rest_utils.ajax(data_required=True) + def patch(self, request, node_id): + """Update an Ironic node + + :param request: HTTP request + :param node_uuid: Node uuid. + """ + patch = request.DATA.get('patch') + return ironic.node_update(request, node_id, patch) + @urls.register class Ports(generic.View): diff --git a/ironic_ui/static/dashboard/admin/ironic/base-node/base-node.controller.js b/ironic_ui/static/dashboard/admin/ironic/base-node/base-node.controller.js new file mode 100644 index 00000000..d07dbdcd --- /dev/null +++ b/ironic_ui/static/dashboard/admin/ironic/base-node/base-node.controller.js @@ -0,0 +1,303 @@ +/* + * Copyright 2016 Cray Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +(function() { + 'use strict'; + + /** + * Controller used to support operations on an Ironic node + */ + angular + .module('horizon.dashboard.admin.ironic') + .controller('BaseNodeController', BaseNodeController); + + BaseNodeController.$inject = [ + '$modalInstance', + 'horizon.app.core.openstack-service-api.ironic', + 'horizon.app.core.openstack-service-api.glance', + 'horizon.dashboard.admin.ironic.base-node.service', + 'horizon.dashboard.admin.ironic.validHostNamePattern', + '$log', + 'ctrl' + ]; + + function BaseNodeController($modalInstance, + ironic, + glance, + baseNodeService, + validHostNamePattern, + $log, + ctrl) { + ctrl.validHostNameRegex = new RegExp(validHostNamePattern); + ctrl.drivers = null; + ctrl.images = null; + ctrl.loadingDriverProperties = false; + // Object containing the set of properties associated with the currently + // selected driver + ctrl.driverProperties = null; + ctrl.driverPropertyGroups = null; + + ctrl.modalTitle = gettext("Node"); + ctrl.submitButtonTitle = gettext("Submit"); + ctrl.showInstanceInfo = false; + + // Node object suitable for Ironic api + ctrl.node = { + name: null, + driver: null, + driver_info: {}, + properties: {}, + extra: {} + }; + + /** + * @description Get the list of currently active Ironic drivers + * + * @return {void} + */ + ctrl._loadDrivers = function() { + return ironic.getDrivers().then(function(response) { + ctrl.drivers = response.data.items; + }); + }; + + /** + * @description Get the list of images from Glance + * + * @return {void} + */ + ctrl._getImages = function() { + glance.getImages().then(function(response) { + ctrl.images = response.data.items; + }); + }; + + /** + * @description Check whether a group contains required properties + * + * @param {DriverProperty[]} group - Property group + * @return {boolean} Return true if the group contains required + * properties, false otherwise + */ + function driverPropertyGroupHasRequired(group) { + var hasRequired = false; + for (var i = 0; i < group.length; i++) { + if (group[i].required) { + hasRequired = true; + break; + } + } + return hasRequired; + } + + /** + * @description Convert array of driver property groups to a string + * + * @param {array[]} groups - Array for driver property groups + * @return {string} Output string + */ + function driverPropertyGroupsToString(groups) { + var output = []; + angular.forEach(groups, function(group) { + var groupStr = []; + angular.forEach(group, function(property) { + groupStr.push(property.name); + }); + groupStr = groupStr.join(", "); + output.push(['[', groupStr, ']'].join("")); + }); + output = output.join(", "); + return ['[', output, ']'].join(""); + } + + /** + * @description Comaprison function used to sort driver property groups + * + * @param {DriverProperty[]} group1 - First group + * @param {DriverProperty[]} group2 - Second group + * @return {integer} Return: + * < 0 if group1 should precede group2 in an ascending ordering + * > 0 if group2 should precede group1 + * 0 if group1 and group2 are considered equal from ordering perpsective + */ + function compareDriverPropertyGroups(group1, group2) { + var group1HasRequired = driverPropertyGroupHasRequired(group1); + var group2HasRequired = driverPropertyGroupHasRequired(group2); + + if (group1HasRequired === group2HasRequired) { + if (group1.length === group2.length) { + return group1[0].name.localeCompare(group2[0].name); + } else { + return group1.length - group2.length; + } + } else { + return group1HasRequired ? -1 : 1; + } + return 0; + } + + /** + * @description Order driver properties in the form using the following + * rules: + * + * (1) Properties that are related to one another should occupy adjacent + * locations in the form + * + * (2) Required properties with no dependents should be located at the + * top of the form + * + * @return {void} + */ + ctrl._sortDriverProperties = function() { + // Build dependency graph between driver properties + var graph = new baseNodeService.Graph(); + + // Create vertices + angular.forEach(ctrl.driverProperties, function(property, name) { + graph.addVertex(name, property); + }); + + /* eslint-disable no-unused-vars */ + + // Create edges + angular.forEach(ctrl.driverProperties, + function(property, name) { + var activators = property.getActivators(); + if (activators) { + angular.forEach(activators, + function(unused, activatorName) { + graph.addEdge(name, activatorName); + }); + } + }); + + /* eslint-enable no-unused-vars */ + + // Perform depth-first-search to find groups of related properties + var groups = []; + graph.dfs( + function(vertexList, components) { + // Sort properties so that those with the largest number of + // immediate dependents are the top of the list + vertexList.sort(function(vertex1, vertex2) { + return vertex2.adjacents.length - vertex1.adjacents.length; + }); + + // Build component and add to list + var component = new Array(vertexList.length); + angular.forEach(vertexList, function(vertex, index) { + component[index] = vertex.data; + }); + components.push(component); + }, + groups); + groups.sort(compareDriverPropertyGroups); + + $log.debug("Found the following property groups: " + + driverPropertyGroupsToString(groups)); + return groups; + }; + + /** + * @description Get the properties associated with a specified driver + * + * @param {string} driverName - Name of driver + * @return {void} + */ + ctrl.loadDriverProperties = function(driverName) { + ctrl.node.driver = driverName; + ctrl.node.driver_info = {}; + + ctrl.loadingDriverProperties = true; + ctrl.driverProperties = null; + ctrl.driverPropertyGroups = null; + + return ironic.getDriverProperties(driverName).then(function(response) { + ctrl.driverProperties = {}; + angular.forEach(response.data, function(desc, property) { + ctrl.driverProperties[property] = + new baseNodeService.DriverProperty(property, + desc, + ctrl.driverProperties); + }); + ctrl.driverPropertyGroups = ctrl._sortDriverProperties(); + ctrl.loadingDriverProperties = false; + }); + }; + + /** + * @description Cancel the current node operation + * + * @return {void} + */ + ctrl.cancel = function() { + $modalInstance.dismiss('cancel'); + }; + + /** + * @desription Delete a node property + * + * @param {string} propertyName - Name of the property + * @return {void} + */ + ctrl.deleteProperty = function(propertyName) { + delete ctrl.node.properties[propertyName]; + }; + + /** + * @description Check whether the specified node property already exists + * + * @param {string} propertyName - Name of the property + * @return {boolean} True if the property already exists, + * otherwise false + */ + ctrl.checkPropertyUnique = function(propertyName) { + return !(propertyName in ctrl.node.properties); + }; + + /** + * @description Delete a node metadata property + * + * @param {string} propertyName - Name of the property + * @return {void} + */ + ctrl.deleteExtra = function(propertyName) { + delete ctrl.node.extra[propertyName]; + }; + + /** + * @description Check whether the specified node metadata property + * already exists + * + * @param {string} propertyName - Name of the metadata property + * @return {boolean} True if the property already exists, + * otherwise false + */ + ctrl.checkExtraUnique = function(propertyName) { + return !(propertyName in ctrl.node.extra); + }; + + /** + * @description Check whether a specified driver property is + * currently active + * + * @param {string} property - Driver property + * @return {boolean} True if the property is active, false otherwise + */ + ctrl.isDriverPropertyActive = function(property) { + return property.isActive(); + }; + } +})(); diff --git a/ironic_ui/static/dashboard/admin/ironic/enroll-node/enroll-node.html b/ironic_ui/static/dashboard/admin/ironic/base-node/base-node.html similarity index 73% rename from ironic_ui/static/dashboard/admin/ironic/enroll-node/enroll-node.html rename to ironic_ui/static/dashboard/admin/ironic/base-node/base-node.html index 424760bd..d67457d1 100644 --- a/ironic_ui/static/dashboard/admin/ironic/enroll-node/enroll-node.html +++ b/ironic_ui/static/dashboard/admin/ironic/base-node/base-node.html @@ -6,7 +6,7 @@ aria-label="Close"> - + - -
+ +
@@ -177,6 +177,63 @@
+ +
+
+ +
+ + Add New Instance Property: + + + + +
+
+
+ +
+
+
+ + {$ propertyName $} + + +
+ + + +
+
+
+
@@ -192,8 +249,8 @@ ng-repeat="property in propertyGroup | filter:ctrl.isDriverPropertyActive" ng-init="name = property.name; selectOptions = property.getSelectOptions()" - ng-class="{'has-error': enrollNodeForm.{$ name $}.$invalid && - enrollNodeForm.{$ name $}.$dirty}"> + ng-class="{'has-error': baseNodeForm.{$ name $}.$invalid && + baseNodeForm.{$ name $}.$dirty}">