From 0c4c948324f18cde62b4e51532047b08a9e0ef7a Mon Sep 17 00:00:00 2001 From: Peter Piela Date: Fri, 3 Feb 2017 18:46:59 -0500 Subject: [PATCH] Add support for editing Ironic network ports The port table in node-detais/configuration tab has been updated to include an "Edit port" action for each port. Closes-Bug: #1648563 Change-Id: I04ec8904dc67f98ff9f0d94a7fa46618cfba956c --- ironic_ui/api/ironic.py | 16 +++ ironic_ui/api/ironic_rest_api.py | 16 +++ .../ironic/edit-port/edit-port.controller.js | 126 ++++++++++++++++++ .../ironic/edit-port/edit-port.service.js | 53 ++++++++ .../dashboard/admin/ironic/ironic.module.js | 3 +- .../dashboard/admin/ironic/ironic.service.js | 27 ++++ .../node-details/node-details.controller.js | 15 +++ .../node-details.controller.spec.js | 44 +++++- .../node-details/sections/configuration.html | 19 ++- 9 files changed, 306 insertions(+), 13 deletions(-) create mode 100644 ironic_ui/static/dashboard/admin/ironic/edit-port/edit-port.controller.js create mode 100644 ironic_ui/static/dashboard/admin/ironic/edit-port/edit-port.service.js diff --git a/ironic_ui/api/ironic.py b/ironic_ui/api/ironic.py index bcaf98e1..590c4494 100755 --- a/ironic_ui/api/ironic.py +++ b/ironic_ui/api/ironic.py @@ -17,6 +17,7 @@ from django.conf import settings from ironicclient import client +from ironicclient.v1 import resource_fields as res_fields from horizon.utils.memoized import memoized # noqa @@ -227,3 +228,18 @@ def port_delete(request, port_uuid): :return: Port """ return ironicclient(request).port.delete(port_uuid) + + +def port_update(request, port_id, patch): + """Update a specified port. + + :param request: HTTP request. + :param node_id: The uuid of the port. + :param patch: Sequence of update operations + :return: port. + + http://docs.openstack.org/developer/python-ironicclient/api/ironicclient.v1.port.html#ironicclient.v1.port.PortManager.update + """ + port = ironicclient(request).port.update(port_id, patch) + return dict([(f, getattr(port, f, '')) + for f in res_fields.PORT_DETAILED_RESOURCE.fields]) diff --git a/ironic_ui/api/ironic_rest_api.py b/ironic_ui/api/ironic_rest_api.py index 2dd6d0e4..8ae5c73d 100755 --- a/ironic_ui/api/ironic_rest_api.py +++ b/ironic_ui/api/ironic_rest_api.py @@ -123,6 +123,22 @@ class Ports(generic.View): return ironic.port_delete(request, params) +@urls.register +class Port(generic.View): + + url_regex = r'ironic/ports/(?P[0-9a-f-]+)$' + + @rest_utils.ajax(data_required=True) + def patch(self, request, port_id): + """Update an Ironic port + + :param request: HTTP request + :param port_id: Port id. + """ + patch = request.DATA.get('patch') + return ironic.port_update(request, port_id, patch) + + @urls.register class StatesPower(generic.View): diff --git a/ironic_ui/static/dashboard/admin/ironic/edit-port/edit-port.controller.js b/ironic_ui/static/dashboard/admin/ironic/edit-port/edit-port.controller.js new file mode 100644 index 00000000..7f05b4d3 --- /dev/null +++ b/ironic_ui/static/dashboard/admin/ironic/edit-port/edit-port.controller.js @@ -0,0 +1,126 @@ +/* + * 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'; + + var UNABLE_TO_UPDATE_CONNECTIVITY_ATTR_MSG = gettext("This field is disabled because a port cannot have any connectivity attributes (pxe_enabled, local_link_connection, portgroup_id) updated unless its associated node is in an enroll, inspecting, mangeable state; or in maintenance mode."); // eslint-disable-line max-len + + /** + * Controller used to edit a specified node port + */ + angular + .module('horizon.dashboard.admin.ironic') + .controller('EditPortController', EditPortController); + + EditPortController.$inject = [ + '$rootScope', + '$controller', + '$uibModalInstance', + '$log', + '$q', + 'horizon.app.core.openstack-service-api.ironic', + 'horizon.dashboard.admin.ironic.events', + 'horizon.dashboard.admin.ironic.update-patch.service', + 'port', + 'node' + ]; + + function EditPortController($rootScope, + $controller, + $uibModalInstance, + $log, + $q, + ironic, + ironicEvents, + updatePatchService, + port, + node) { + var ctrl = this; + $controller('BasePortController', + {ctrl: ctrl, + $uibModalInstance: $uibModalInstance}); + + ctrl.modalTitle = gettext("Edit Port"); + ctrl.submitButtonTitle = gettext("Update Port"); + + var cannotEditConnectivityAttr = + !(node.maintenance || (node.provision_state === "enroll" || + node.provision_state === "inspecting" || + node.provision_state === "manageable")); + + // Initialize form fields + ctrl.port.address = port.address; + + ctrl.pxeEnabled.value = port.pxe_enabled ? 'True' : 'False'; + if (cannotEditConnectivityAttr) { + ctrl.pxeEnabled.disabled = true; + ctrl.pxeEnabled.info = UNABLE_TO_UPDATE_CONNECTIVITY_ATTR_MSG; + } + + angular.forEach( + ['port_id', 'switch_id', 'switch_info'], + function(prop) { + if (angular.isDefined(port.local_link_connection[prop])) { + ctrl.localLinkConnection[prop].value = + port.local_link_connection[prop]; + } + }); + + if (cannotEditConnectivityAttr) { + ctrl.localLinkConnection.$setDisabled( + true, + UNABLE_TO_UPDATE_CONNECTIVITY_ATTR_MSG); + } + + ctrl.port.extra = angular.copy(port.extra); + + /** + * Apply updates to the port being edited + * + * @return {void} + */ + ctrl.updatePort = function() { + var patcher = new updatePatchService.UpdatePatch(); + + $log.info("Updating port " + JSON.stringify(port)); + + patcher.buildPatch(port.address, ctrl.port.address, "/address"); + patcher.buildPatch(port.pxe_enabled ? 'True' : 'False', + ctrl.pxeEnabled.value, + "/pxe_enabled"); + patcher.buildPatch(port.local_link_connection, + ctrl.localLinkConnection.$toPortAttr(), + "/local_link_connection"); + patcher.buildPatch(port.extra, ctrl.port.extra, "/extra"); + + var patch = patcher.getPatch(); + $log.info("patch = " + JSON.stringify(patch.patch)); + if (patch.status === updatePatchService.UpdatePatch.status.OK) { + ironic.updatePort(port.uuid, patch.patch).then(function(port) { + $rootScope.$emit(ironicEvents.EDIT_PORT_SUCCESS); + $uibModalInstance.close(port); + }); + } else { + toastService.add('error', + gettext('Unable to create port update patch.')); + } + }; + + ctrl.submit = function() { + ctrl.updatePort(); + }; + } +})(); diff --git a/ironic_ui/static/dashboard/admin/ironic/edit-port/edit-port.service.js b/ironic_ui/static/dashboard/admin/ironic/edit-port/edit-port.service.js new file mode 100644 index 00000000..2d1dcb5f --- /dev/null +++ b/ironic_ui/static/dashboard/admin/ironic/edit-port/edit-port.service.js @@ -0,0 +1,53 @@ +/* + * Copyright 2017 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'; + + angular + .module('horizon.dashboard.admin.ironic') + .factory('horizon.dashboard.admin.ironic.edit-port.service', + editPortService); + + editPortService.$inject = [ + '$uibModal', + 'horizon.dashboard.admin.ironic.basePath' + ]; + + function editPortService($uibModal, basePath) { + var service = { + modal: modal + }; + + function modal(port, node) { + var options = { + controller: 'EditPortController as ctrl', + backdrop: 'static', + resolve: { + port: function() { + return port; + }, + node: function() { + return node; + } + }, + templateUrl: basePath + '/base-port/base-port.html' + }; + return $uibModal.open(options).result; + } + + return service; + } +})(); diff --git a/ironic_ui/static/dashboard/admin/ironic/ironic.module.js b/ironic_ui/static/dashboard/admin/ironic/ironic.module.js index 19c9bd26..493e1cda 100755 --- a/ironic_ui/static/dashboard/admin/ironic/ironic.module.js +++ b/ironic_ui/static/dashboard/admin/ironic/ironic.module.js @@ -52,7 +52,8 @@ DELETE_NODE_SUCCESS:'horizon.dashboard.admin.ironic.DELETE_NODE_SUCCESS', EDIT_NODE_SUCCESS:'horizon.dashboard.admin.ironic.EDIT_NODE_SUCCESS', CREATE_PORT_SUCCESS:'horizon.dashboard.admin.ironic.CREATE_PORT_SUCCESS', - DELETE_PORT_SUCCESS:'horizon.dashboard.admin.ironic.DELETE_PORT_SUCCESS' + DELETE_PORT_SUCCESS:'horizon.dashboard.admin.ironic.DELETE_PORT_SUCCESS', + EDIT_PORT_SUCCESS:'horizon.dashboard.admin.ironic.EDIT_PORT_SUCCESS' }; $provide.constant('horizon.dashboard.admin.ironic.events', events); } diff --git a/ironic_ui/static/dashboard/admin/ironic/ironic.service.js b/ironic_ui/static/dashboard/admin/ironic/ironic.service.js index aad2d242..872c7e5e 100755 --- a/ironic_ui/static/dashboard/admin/ironic/ironic.service.js +++ b/ironic_ui/static/dashboard/admin/ironic/ironic.service.js @@ -55,6 +55,7 @@ removeNodeFromMaintenanceMode: removeNodeFromMaintenanceMode, setNodeProvisionState: setNodeProvisionState, updateNode: updateNode, + updatePort: updatePort, validateNode: validateNode }; @@ -456,6 +457,32 @@ return $q.reject(msg); }); } + + /** + * @description Update the definition of a specified port. + * + * http://developer.openstack.org/api-ref/baremetal/#update-a-port + * + * @param {string} portUuid – UUID of a port. + * @param {object[]} patch – Sequence of update operations + * @return {promise} Promise + */ + function updatePort(portUuid, patch) { + return apiService.patch('/api/ironic/ports/' + portUuid, + {patch: patch}) + .then(function(response) { + var msg = gettext('Successfully updated port %s'); + toastService.add('success', interpolate(msg, [portUuid], false)); + return response.data; // The updated port + }) + .catch(function(response) { + var msg = interpolate(gettext('Unable to update port %s: %s'), + [portUuid, response.data], + false); + toastService.add('error', msg); + return $q.reject(msg); + }); + } } }()); diff --git a/ironic_ui/static/dashboard/admin/ironic/node-details/node-details.controller.js b/ironic_ui/static/dashboard/admin/ironic/node-details/node-details.controller.js index 37632327..6ae588db 100755 --- a/ironic_ui/static/dashboard/admin/ironic/node-details/node-details.controller.js +++ b/ironic_ui/static/dashboard/admin/ironic/node-details/node-details.controller.js @@ -32,6 +32,7 @@ 'horizon.dashboard.admin.ironic.actions', 'horizon.dashboard.admin.ironic.basePath', 'horizon.dashboard.admin.ironic.edit-node.service', + 'horizon.dashboard.admin.ironic.edit-port.service', 'horizon.dashboard.admin.ironic.maintenance.service', 'horizon.dashboard.admin.ironic.node-state-transition.service', 'horizon.dashboard.admin.ironic.validUuidPattern' @@ -46,6 +47,7 @@ actions, basePath, editNodeService, + editPortService, maintenanceService, nodeStateTransitionService, validUuidPattern) { @@ -81,6 +83,7 @@ ctrl.editNode = editNode; ctrl.createPort = createPort; ctrl.deletePort = deletePort; + ctrl.editPort = editPort; ctrl.refresh = refresh; $scope.emptyObject = function(obj) { @@ -223,6 +226,18 @@ ctrl.actions.createPort(ctrl.node); } + /** + * @description: Edit a specified port + * + * @param {port} port - Port to be edited + * @return {void} + */ + function editPort(port) { + editPortService.modal(port, ctrl.node).then(function() { + ctrl.refresh(); + }); + } + /** * @name horizon.dashboard.admin.ironic.NodeDetailsController.deletePort * @description Delete a list of ports diff --git a/ironic_ui/static/dashboard/admin/ironic/node-details/node-details.controller.spec.js b/ironic_ui/static/dashboard/admin/ironic/node-details/node-details.controller.spec.js index 0f5afd82..635d9714 100755 --- a/ironic_ui/static/dashboard/admin/ironic/node-details/node-details.controller.spec.js +++ b/ironic_ui/static/dashboard/admin/ironic/node-details/node-details.controller.spec.js @@ -18,7 +18,7 @@ 'use strict'; describe('horizon.dashboard.admin.ironic.node-details', function () { - var ctrl, $q; + var ctrl, $q, nodeStateTransitionService; var nodeUuid = "0123abcd-0123-4567-abcd-0123456789ab"; var nodeName = "herp"; var numPorts = 2; @@ -27,9 +27,17 @@ return '' + index + index + nodeUuid.substring(2); } + function portMacAddr(index) { + var mac = '' + index + index; + for (var i = 0; i < 5; i++) { + mac += ':' + index + index; + } + return mac; + } + function createPort(nodeUuid, index, extra) { - var uuid = portUuid(nodeUuid, index); - var port = {uuid: uuid, id: uuid}; + var port = {uuid: portUuid(nodeUuid, index), + address: portMacAddr(index)}; if (angular.isDefined(extra)) { port.extra = extra; } @@ -37,7 +45,9 @@ } function createNode(name, uuid) { - return {name: name, uuid: uuid, id: uuid}; + return {name: name, + uuid: uuid, + provision_state: 'enroll'}; } var ironicAPI = { @@ -88,16 +98,20 @@ var $location = _$location_; $location.path('/admin/ironic/' + nodeUuid + '/'); + nodeStateTransitionService = $injector.get( + 'horizon.dashboard.admin.ironic.node-state-transition.service'); + ctrl = controller( 'horizon.dashboard.admin.ironic.NodeDetailsController', {$scope: scope, $location: $location, + 'horizon.dashboard.admin.ironic.edit-port.service': {}, 'horizon.dashboard.admin.ironic.actions': {}}); scope.$apply(); })); - it('should be defined', function () { + it('controller should be defined', function () { expect(ctrl).toBeDefined(); }); @@ -107,7 +121,9 @@ it('should have a node', function () { expect(ctrl.node).toBeDefined(); - expect(ctrl.node).toEqual(createNode(nodeName, nodeUuid)); + var node = createNode(nodeName, nodeUuid); + node.id = node.uuid; + expect(ctrl.node).toEqual(node); }); it('should have ports', function () { @@ -116,7 +132,10 @@ var ports = []; for (var i = 0; i < numPorts; i++) { - ports.push(createPort(ctrl.node.uuid, i)); + var port = createPort(ctrl.node.uuid, i); + port.id = port.uuid; + port.name = port.address; + ports.push(port); } expect(ctrl.portsSrc).toEqual(ports); }); @@ -138,5 +157,16 @@ expect(ctrl.getVifPortId(createPort(ctrl.node.uuid, 1, extra))). toEqual("port_uuid"); }); + + it('should have node-state-transitions', function () { + expect(ctrl.nodeStateTransitions).toBeDefined(); + expect(ctrl.nodeStateTransitions).toEqual( + nodeStateTransitionService.getTransitions(ctrl.node.provision_state)); + }); + + it('should have node-validation', function () { + expect(ctrl.nodeValidation).toBeDefined(); + expect(ctrl.nodeValidation).toEqual([]); + }); }); })(); diff --git a/ironic_ui/static/dashboard/admin/ironic/node-details/sections/configuration.html b/ironic_ui/static/dashboard/admin/ironic/node-details/sections/configuration.html index 75d0bdd6..1d4cf807 100644 --- a/ironic_ui/static/dashboard/admin/ironic/node-details/sections/configuration.html +++ b/ironic_ui/static/dashboard/admin/ironic/node-details/sections/configuration.html @@ -90,12 +90,21 @@ - - - + + + {$ ::'Edit port' | translate $} + + + + {$ ::'Delete port' | translate $} + +