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
This commit is contained in:
Peter Piela 2017-02-03 18:46:59 -05:00
parent 9e977e1328
commit 0c4c948324
9 changed files with 306 additions and 13 deletions

View File

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

View File

@ -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<port_id>[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):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -90,12 +90,21 @@
</ul>
</td>
<td class="actions_column">
<action-list>
<action action-classes="'btn btn-default btn-sm'"
callback="ctrl.deletePort"
item="[port]">
<span class="fa fa-trash"></span>
<action-list uib-dropdown class="pull-right">
<action button-type="split-button"
action-classes="'btn btn-default btn-sm'"
callback="ctrl.editPort"
item="port">
{$ ::'Edit port' | translate $}
</action>
<menu>
<action button-type="menu-item"
callback="ctrl.deletePort"
item="[port]">
<span class="fa fa-trash"></span>
{$ ::'Delete port' | translate $}
</action>
</menu>
</action-list>
</td>
</tr>