Added support for creating/deleting network ports

Network ports can be created in both the node list and node
detail pages. Network ports can be deleted from the the node
detail page.

Change-Id: I3047a0adef9df58aaabd0f2d99a09351233fa06b
This commit is contained in:
Peter Piela 2016-07-19 16:39:24 -04:00 committed by Elizabeth Elwell
parent f43592f456
commit 04973a788d
14 changed files with 632 additions and 45 deletions

View File

@ -166,3 +166,24 @@ def driver_properties(request, driver_name):
:return: Property list :return: Property list
""" """
return ironicclient(request).driver.properties(driver_name) return ironicclient(request).driver.properties(driver_name)
def port_create(request, params):
"""Create network port
:param request: HTTP request
:param params: Port creation parameters
:return: Port
"""
port_manager = ironicclient(request).port
return port_manager.create(**params)
def port_delete(request, port_uuid):
"""Delete a network port
:param request: HTTP request
:param port_uuid: Port uuid
:return: Port
"""
return ironicclient(request).port.delete(port_uuid)

View File

@ -93,6 +93,25 @@ class Ports(generic.View):
'items': [i.to_dict() for i in items], 'items': [i.to_dict() for i in items],
} }
@rest_utils.ajax(data_required=True)
def post(self, request):
"""Create a network port
:param request: HTTP request
:return: Port
"""
port = request.DATA.get('port')
return ironic.port_create(request, port).to_dict()
@rest_utils.ajax(data_required=True)
def delete(self, request):
"""Delete a network port
:param request: HTTP request
"""
params = request.DATA.get('port_uuid')
return ironic.port_delete(request, params)
@urls.register @urls.register
class StatesPower(generic.View): class StatesPower(generic.View):

View File

@ -0,0 +1,35 @@
/*
* 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';
angular
.module('horizon.dashboard.admin.ironic')
.directive('autoFocus', AutoFocus);
AutoFocus.$inject = ['$timeout'];
function AutoFocus($timeout) {
return {
restrict: 'AC',
link: function(scope, elem) {
$timeout(function() {
elem[0].focus();
}, 1000);
}
};
}
})();

View File

@ -0,0 +1,95 @@
/*
* 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 create a network port on a specified node
*/
angular
.module('horizon.dashboard.admin.ironic')
.controller('CreatePortController', CreatePortController);
CreatePortController.$inject = [
'$rootScope',
'$modalInstance',
'horizon.app.core.openstack-service-api.ironic',
'horizon.dashboard.admin.ironic.events',
'$log',
'node'
];
function CreatePortController($rootScope,
$modalInstance,
ironic,
ironicEvents,
$log,
node) {
var ctrl = this;
// Paramater object that defines the port to be created
ctrl.port = {
node_uuid: node.id,
address: null,
extra: {}
};
/**
* Cancel the port creation process
*
* @return {void}
*/
ctrl.cancel = function() {
$modalInstance.dismiss('cancel');
};
/**
* Create the defined port
*
* @return {void}
*/
ctrl.createPort = function() {
ironic.createPort(ctrl.port).then(
function() {
$modalInstance.close();
$rootScope.$emit(ironicEvents.CREATE_PORT_SUCCESS);
},
function() {
});
};
/**
* Delete a port metadata property
*
* @param {string} propertyName - Name of the property
* @return {void}
*/
ctrl.deleteExtra = function(propertyName) {
delete ctrl.port.extra[propertyName];
};
/**
* Check whether the specified port 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.port.extra);
};
}
})();

View File

@ -0,0 +1,90 @@
<div class="modal-header" modal-draggable>
<button type="button"
class="close"
ng-click="$dismiss()"
aria-hidden="true"
aria-label="Close">
<span aria-hidden="true" class="fa fa-times"></span>
</button>
<h3 class="modal-title" translate>Create Port</h3>
</div>
<div class="modal-body">
<form id="CreatePortForm" name="CreatePortForm">
<div class="form-group"
ng-class="{'has-error': CreatePortForm.macAddress.$invalid &&
CreatePortForm.macAddress.$dirty}">
<label for="macAddress"
class="control-label"
translate>MAC address</label>
<div>
<input type="text"
class="form-control"
ng-model="ctrl.port.address"
id="macAddress"
name="macAddress"
ng-required="true"
ng-pattern="'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})'"
auto-focus
placeholder="{$ 'MAC address for this port. Required.' | translate $}"/>
</div>
</div>
</form>
<form id="AddExtraForm" name="AddExtraForm" style="margin-bottom:10px;">
<label for="extras" class="control-label" translate>Extras</label>
<div class="input-group input-group-sm">
<span class="input-group-addon"
style="width:25%;text-align:right">
Add Extra:</span>
<input class="form-control"
type="text"
ng-model="extraName"
validate-unique="ctrl.checkExtraUnique"
placeholder="{$ 'Property Name' | translate $}"/>
<span class="input-group-btn">
<button class="btn btn-primary"
type="button"
ng-disabled="!extraName || AddExtraForm.$invalid"
ng-click="ctrl.port.extra[extraName] = null;
extraName = null">
<span class="fa fa-plus"> </span>
</button>
</span>
</div>
</form>
<form class="form-horizontal" id="ExtraForm" name="ExtraForm">
<div class="input-group input-group-sm"
ng-repeat="(propertyName, propertyValue) in ctrl.port.extra">
<span class="input-group-addon"
style="width:25%;text-align:right">
{$ propertyName $}
</span>
<input class="form-control"
type="text"
name="{$ propertyName $}"
ng-model="ctrl.port.extra[propertyName]"
ng-required="true"/>
<div class="input-group-btn">
<a class="btn btn-default"
ng-click="ctrl.deleteExtra(propertyName)">
<span class="fa fa-minus"> </span>
</a>
</div>
</div>
</form>
</div>
<!--modal footer-->
<div class="modal-footer ng-scope">
<button class="btn btn-default" ng-click="ctrl.cancel()">
<span class="fa fa-close"></span>
<span class="ng-scope" translate>Cancel</span>
</button>
<button type="submit"
ng-disabled="CreatePortForm.$invalid || ExtraForm.$invalid"
ng-click="ctrl.createPort()"
class="btn btn-primary"
translate>
Create Port
</button>
</div>

View File

@ -0,0 +1,49 @@
/*
* 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';
angular
.module('horizon.dashboard.admin.ironic')
.factory('horizon.dashboard.admin.ironic.create-port.service',
createPortService);
createPortService.$inject = [
'$modal',
'horizon.dashboard.admin.basePath'
];
function createPortService($modal, basePath) {
var service = {
modal: modal
};
return service;
function modal(node) {
var options = {
controller: 'CreatePortController as ctrl',
backdrop: 'static',
resolve: {
node: function() {
return node;
}
},
templateUrl: basePath + '/ironic/create-port/create-port.html'
};
return $modal.open(options);
}
}
})();

View File

@ -27,6 +27,7 @@
'$rootScope', '$rootScope',
'$modalInstance', '$modalInstance',
'horizon.app.core.openstack-service-api.ironic', 'horizon.app.core.openstack-service-api.ironic',
'horizon.dashboard.admin.ironic.events',
'horizon.app.core.openstack-service-api.glance', 'horizon.app.core.openstack-service-api.glance',
'horizon.dashboard.admin.ironic.enroll-node.service', 'horizon.dashboard.admin.ironic.enroll-node.service',
'horizon.dashboard.admin.ironic.validHostNamePattern', 'horizon.dashboard.admin.ironic.validHostNamePattern',
@ -36,6 +37,7 @@
function EnrollNodeController($rootScope, function EnrollNodeController($rootScope,
$modalInstance, $modalInstance,
ironic, ironic,
ironicEvents,
glance, glance,
enrollNodeService, enrollNodeService,
validHostNamePattern, validHostNamePattern,
@ -276,7 +278,7 @@
ironic.createNode(ctrl.node).then( ironic.createNode(ctrl.node).then(
function() { function() {
$modalInstance.close(); $modalInstance.close();
$rootScope.$emit('ironic-ui:new-node'); $rootScope.$emit(ironicEvents.ENROLL_NODE_SUCCESS);
}, },
function() { function() {
// No additional error processing for now // No additional error processing for now

View File

@ -31,6 +31,16 @@
'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$') // eslint-disable-line max-len '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$') // eslint-disable-line max-len
.constant('horizon.dashboard.admin.ironic.validUuidPattern', .constant('horizon.dashboard.admin.ironic.validUuidPattern',
'^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$'); // eslint-disable-line max-len '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$') // eslint-disable-line max-len
.constant('horizon.dashboard.admin.ironic.events', events());
function events() {
return {
ENROLL_NODE_SUCCESS:'horizon.dashboard.admin.ironic.ENROLL_NODE_SUCCESS',
DELETE_NODE_SUCCESS:'horizon.dashboard.admin.ironic.DELETE_NODE_SUCCESS',
CREATE_PORT_SUCCESS:'horizon.dashboard.admin.ironic.CREATE_PORT_SUCCESS',
DELETE_PORT_SUCCESS:'horizon.dashboard.admin.ironic.DELETE_PORT_SUCCESS'
};
}
})(); })();

View File

@ -28,7 +28,7 @@
]; ];
/** /**
* Service that provides access to the Ironic client API * @description Service that provides access to the Ironic client API
* *
* @param {object} apiService - HTTP service * @param {object} apiService - HTTP service
* @param {object} toastService - User message service * @param {object} toastService - User message service
@ -37,7 +37,9 @@
function ironicAPI(apiService, toastService) { function ironicAPI(apiService, toastService) {
var service = { var service = {
createNode: createNode, createNode: createNode,
createPort: createPort,
deleteNode: deleteNode, deleteNode: deleteNode,
deletePort: deletePort,
getDrivers: getDrivers, getDrivers: getDrivers,
getDriverProperties: getDriverProperties, getDriverProperties: getDriverProperties,
getNode: getNode, getNode: getNode,
@ -52,7 +54,7 @@
return service; return service;
/** /**
* Retrieve a list of nodes * @description Retrieve a list of nodes
* http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-nodes * http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-nodes
* *
* @return {promise} Node collection in JSON * @return {promise} Node collection in JSON
@ -67,7 +69,7 @@
} }
/** /**
* Retrieve information about the given node. * @description Retrieve information about the given node.
* *
* http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1- * http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-
* nodes-(node_ident) * nodes-(node_ident)
@ -84,7 +86,7 @@
} }
/** /**
* Retrieve a list of ports associated with a node. * @description Retrieve a list of ports associated with a node.
* *
* http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-ports * http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-ports
* *
@ -106,7 +108,7 @@
} }
/** /**
* Put the node in maintenance mode. * @description Put the node in maintenance mode.
* *
* http://docs.openstack.org/developer/ironic/webapi/v1.html# * http://docs.openstack.org/developer/ironic/webapi/v1.html#
* put--v1-nodes-(node_ident)-maintenance * put--v1-nodes-(node_ident)-maintenance
@ -130,7 +132,7 @@
} }
/** /**
* Remove the node from maintenance mode. * @description Remove the node from maintenance mode.
* *
* http://docs.openstack.org/developer/ironic/webapi/v1.html# * http://docs.openstack.org/developer/ironic/webapi/v1.html#
* delete--v1-nodes-(node_ident)-maintenance * delete--v1-nodes-(node_ident)-maintenance
@ -148,7 +150,7 @@
} }
/** /**
* Set the power state of the node. * @description Set the power state of the node.
* *
* http://docs.openstack.org/developer/ironic/webapi/v1.html# * http://docs.openstack.org/developer/ironic/webapi/v1.html#
* put--v1-nodes-(node_ident)-states-power * put--v1-nodes-(node_ident)-states-power
@ -173,7 +175,7 @@
} }
/** /**
* Set the power state of the node. * @description Set the power state of the node.
* *
* http://docs.openstack.org/developer/ironic/webapi/v1.html# * http://docs.openstack.org/developer/ironic/webapi/v1.html#
* put--v1-nodes-(node_ident)-states-power * put--v1-nodes-(node_ident)-states-power
@ -198,7 +200,7 @@
} }
/** /**
* Create an Ironic node * @description Create an Ironic node
* *
* http://docs.openstack.org/developer/ironic/webapi/v1.html#post--v1-nodes * http://docs.openstack.org/developer/ironic/webapi/v1.html#post--v1-nodes
* *
@ -220,7 +222,7 @@
} }
/** /**
* Delete the specified node from inventory * @description Delete the specified node from inventory
* *
* http://docs.openstack.org/developer/ironic/webapi/v1.html# * http://docs.openstack.org/developer/ironic/webapi/v1.html#
* delete--v1-nodes * delete--v1-nodes
@ -244,7 +246,7 @@
} }
/** /**
* Retrieve the list of Ironic drivers * @description Retrieve the list of Ironic drivers
* *
* http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-drivers * http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-drivers
* *
@ -259,7 +261,7 @@
} }
/** /**
* Retrieve properities of a specified driver * @description Retrieve properities of a specified driver
* *
* http://docs.openstack.org/developer/ironic/webapi/v1.html# * http://docs.openstack.org/developer/ironic/webapi/v1.html#
* get--v1-drivers-properties * get--v1-drivers-properties
@ -276,6 +278,47 @@
toastService.add('error', interpolate(msg, [reason], false)); toastService.add('error', interpolate(msg, [reason], false));
}); });
} }
/**
* @description Create a network port
*
* @param {object} port Object containing parameters that define
* the port to be created
* @return {promise} Promise
*/
function createPort(port) {
var data = {
port: port
};
return apiService.post('/api/ironic/ports/', data)
.success(function() {
toastService.add('success',
gettext('Port successfully created'));
})
.error(function(reason) {
var msg = gettext('Unable to create port: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
/**
* @description Delete a network port
*
* @param {string} portUuid UUID of the port to be deleted
* @return {promise} Promise
*/
function deletePort(portUuid) {
var data = {
port_uuid: portUuid
};
return apiService.delete('/api/ironic/ports/', data)
.success(function() {
})
.error(function(reason) {
var msg = gettext('Unable to delete port: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
} }
}()); }());

View File

@ -34,6 +34,20 @@
var DELETE_NODES_SUCCESS = gettext('Successfully deleted nodes "%s"'); var DELETE_NODES_SUCCESS = gettext('Successfully deleted nodes "%s"');
var DELETE_NODES_ERROR = gettext('Error deleting nodes "%s"'); var DELETE_NODES_ERROR = gettext('Error deleting nodes "%s"');
var DELETE_PORT_TITLE = gettext("Delete Port");
var DELETE_PORT_MSG =
gettext('Are you sure you want to delete port "%s"? ' +
'This action cannot be undone.');
var DELETE_PORT_SUCCESS = gettext('Successfully deleted port "%s"');
var DELETE_PORT_ERROR = gettext('Unable to delete port "%s"');
var DELETE_PORTS_TITLE = gettext("Delete Ports");
var DELETE_PORTS_MSG =
gettext('Are you sure you want to delete ports "%s"? ' +
'This action cannot be undone.');
var DELETE_PORTS_SUCCESS = gettext('Successfully deleted ports "%s"');
var DELETE_PORTS_ERROR = gettext('Error deleting ports "%s"');
angular angular
.module('horizon.dashboard.admin.ironic') .module('horizon.dashboard.admin.ironic')
.factory('horizon.dashboard.admin.ironic.actions', actions); .factory('horizon.dashboard.admin.ironic.actions', actions);
@ -41,15 +55,26 @@
actions.$inject = [ actions.$inject = [
'horizon.app.core.openstack-service-api.ironic', 'horizon.app.core.openstack-service-api.ironic',
'horizon.framework.widgets.toast.service', 'horizon.framework.widgets.toast.service',
'horizon.dashboard.admin.ironic.events',
'horizon.framework.widgets.modal.deleteModalService', 'horizon.framework.widgets.modal.deleteModalService',
'horizon.dashboard.admin.ironic.create-port.service',
'$q', '$q',
'$rootScope' '$rootScope'
]; ];
function actions(ironic, toastService, deleteModalService, $q, $rootScope) { function actions(ironic,
toastService,
ironicEvents,
deleteModalService,
createPortService,
$q,
$rootScope) {
var service = { var service = {
createPort: createPort,
deleteNode: deleteNode, deleteNode: deleteNode,
deleteNodes: deleteNodes, deleteNodes: deleteNodes,
deletePort: deletePort,
deletePorts: deletePorts,
powerOn: powerOn, powerOn: powerOn,
powerOff: powerOff, powerOff: powerOff,
powerOnAll: powerOnNodes, powerOnAll: powerOnNodes,
@ -73,7 +98,7 @@
var context = { var context = {
labels: labels, labels: labels,
deleteEntity: ironic.deleteNode, deleteEntity: ironic.deleteNode,
successEvent: "ironic-ui:delete-node-success" successEvent: ironicEvents.DELETE_NODE_SUCCESS
}; };
return deleteModalService.open($rootScope, [node], context); return deleteModalService.open($rootScope, [node], context);
} }
@ -89,7 +114,7 @@
var context = { var context = {
labels: labels, labels: labels,
deleteEntity: ironic.deleteNode, deleteEntity: ironic.deleteNode,
successEvent: "ironic-ui:delete-node-success" successEvent: ironicEvents.DELETE_NODE_SUCCESS
}; };
return deleteModalService.open($rootScope, nodes, context); return deleteModalService.open($rootScope, nodes, context);
} }
@ -166,6 +191,42 @@
return applyFuncToNodes(removeFromMaintenanceMode, nodes); return applyFuncToNodes(removeFromMaintenanceMode, nodes);
} }
function createPort(node) {
return createPortService.modal(node);
}
function deletePort(port) {
var labels = {
title: DELETE_PORT_TITLE,
message: DELETE_PORT_MSG,
submit: DELETE_PORT_TITLE,
success: DELETE_PORT_SUCCESS,
error: DELETE_PORT_ERROR
};
var context = {
labels: labels,
deleteEntity: ironic.deletePort,
successEvent: ironicEvents.DELETE_PORT_SUCCESS
};
return deleteModalService.open($rootScope, [port], context);
}
function deletePorts(ports) {
var labels = {
title: DELETE_PORTS_TITLE,
message: DELETE_PORTS_MSG,
submit: DELETE_PORTS_TITLE,
success: DELETE_PORTS_SUCCESS,
error: DELETE_PORTS_ERROR
};
var context = {
labels: labels,
deleteEntity: ironic.deletePort,
successEvent: ironicEvents.DELETE_PORT_SUCCESS
};
return deleteModalService.open($rootScope, ports, context);
}
/* /*
* @name horizon.dashboard.admin.ironic.actions.applyFuncToNodes * @name horizon.dashboard.admin.ironic.actions.applyFuncToNodes
* @description Apply a specified function to each member of a * @description Apply a specified function to each member of a

View File

@ -23,21 +23,31 @@
IronicNodeDetailsController); IronicNodeDetailsController);
IronicNodeDetailsController.$inject = [ IronicNodeDetailsController.$inject = [
'$scope',
'$rootScope',
'$location', '$location',
'horizon.app.core.openstack-service-api.ironic', 'horizon.app.core.openstack-service-api.ironic',
'horizon.dashboard.admin.ironic.events',
'horizon.dashboard.admin.ironic.actions', 'horizon.dashboard.admin.ironic.actions',
'horizon.dashboard.admin.basePath', 'horizon.dashboard.admin.basePath',
'horizon.dashboard.admin.ironic.maintenance.service' 'horizon.dashboard.admin.ironic.maintenance.service',
'horizon.dashboard.admin.ironic.validUuidPattern'
]; ];
function IronicNodeDetailsController($location, function IronicNodeDetailsController($scope,
$rootScope,
$location,
ironic, ironic,
ironicEvents,
actions, actions,
basePath, basePath,
maintenanceService) { maintenanceService,
validUuidPattern) {
var ctrl = this; var ctrl = this;
var path = basePath + 'ironic/node-details/sections/'; var path = basePath + 'ironic/node-details/sections/';
ctrl.noPortsText = gettext('No network ports have been defined');
ctrl.actions = actions; ctrl.actions = actions;
ctrl.sections = [ ctrl.sections = [
@ -51,18 +61,42 @@
} }
]; ];
ctrl.ports = [];
ctrl.portsSrc = [];
ctrl.basePath = basePath; ctrl.basePath = basePath;
ctrl.re_uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/; ctrl.re_uuid = new RegExp(validUuidPattern);
ctrl.isUuid = isUuid; ctrl.isUuid = isUuid;
ctrl.getVifPortId = getVifPortId; ctrl.getVifPortId = getVifPortId;
ctrl.putNodeInMaintenanceMode = putNodeInMaintenanceMode; ctrl.putNodeInMaintenanceMode = putNodeInMaintenanceMode;
ctrl.removeNodeFromMaintenanceMode = removeNodeFromMaintenanceMode; ctrl.removeNodeFromMaintenanceMode = removeNodeFromMaintenanceMode;
ctrl.createPort = createPort;
ctrl.deletePort = deletePort;
ctrl.deletePorts = deletePorts;
var createPortHandler =
$rootScope.$on(ironicEvents.CREATE_PORT_SUCCESS,
function() {
init();
});
var deletePortHandler =
$rootScope.$on(ironicEvents.DELETE_PORT_SUCCESS,
function() {
init();
$scope.$broadcast('hzTable:clearSelected');
});
$scope.$on('$destroy', function() {
createPortHandler();
deletePortHandler();
});
init(); init();
/** /**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.init * @name horizon.dashboard.admin.ironic.NodeDetailsController.init
* @description Initialize the controller instance based on the current page url. * @description Initialize the controller instance based on the
* current page url.
* *
* @return {void} * @return {void}
*/ */
@ -87,20 +121,24 @@
function retrieveNode(uuid) { function retrieveNode(uuid) {
return ironic.getNode(uuid).then(function (response) { return ironic.getNode(uuid).then(function (response) {
ctrl.node = response.data; ctrl.node = response.data;
ctrl.node.id = uuid;
}); });
} }
/** /**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.retrievePorts * @name horizon.dashboard.admin.ironic.NodeDetailsController.retrievePorts
* @description Retrieve the ports associated with a specified node, and store * @description Retrieve the ports associated with a specified node,
* them in the controller instance. * and store them in the controller instance.
* *
* @param {string} nodeId Node name or UUID * @param {string} nodeId Node name or UUID
* @return {void} * @return {void}
*/ */
function retrievePorts(nodeId) { function retrievePorts(nodeId) {
ironic.getPortsWithNode(nodeId).then(function (response) { ironic.getPortsWithNode(nodeId).then(function (response) {
ctrl.ports = response.data.items; ctrl.portsSrc = response.data.items;
ctrl.portsSrc.forEach(function(port) {
port.id = port.uuid;
});
}); });
} }
@ -109,7 +147,8 @@
* @description Test whether a string is an OpenStack UUID * @description Test whether a string is an OpenStack UUID
* *
* @param {string} str string * @param {string} str string
* @return {boolean} True if the string is an OpenStack UUID, otherwise false * @return {boolean} True if the string is an OpenStack UUID,
* otherwise false
*/ */
function isUuid(str) { function isUuid(str) {
return !!str.match(ctrl.re_uuid); return !!str.match(ctrl.re_uuid);
@ -120,7 +159,8 @@
* @description Get the vif_port_id property of a specified port * @description Get the vif_port_id property of a specified port
* *
* @param {object} port instance of port * @param {object} port instance of port
* @return {string} Value of vif_port_id property or "" if the property does not exist * @return {string} Value of vif_port_id property or
* "" if the property does not exist
*/ */
function getVifPortId(port) { function getVifPortId(port) {
return angular.isDefined(port.extra) && return angular.isDefined(port.extra) &&
@ -135,5 +175,42 @@
function removeNodeFromMaintenanceMode() { function removeNodeFromMaintenanceMode() {
maintenanceService.removeNodeFromMaintenanceMode(ctrl.node); maintenanceService.removeNodeFromMaintenanceMode(ctrl.node);
} }
/**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.createPort
* @description Initiate creation of a newtwork port for the current
* node
*
* @return {void}
*/
function createPort() {
ctrl.actions.createPort(ctrl.node);
}
/**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.deletePort
* @description Delete a specified port
*
* @param {port []} port port to be deleted
* @return {void}
*/
function deletePort(port) {
ctrl.actions.deletePort({id: port.uuid, name: port.address});
}
/**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.deletePorts
* @description Delete a specified list of ports
*
* @param {port []} ports list of ports to be deleted
* @return {void}
*/
function deletePorts(ports) {
var selectedPorts = [];
angular.forEach(ports, function(port) {
selectedPorts.push({id: port.uuid, name: port.address});
});
ctrl.actions.deletePorts(selectedPorts);
}
} }
})(); })();

View File

@ -8,9 +8,9 @@
<dt translate>Node ID</dt> <dt translate>Node ID</dt>
<dd>{$ ctrl.node.uuid $}</dd> <dd>{$ ctrl.node.uuid $}</dd>
<dt translate>Chassis ID</dt> <dt translate>Chassis ID</dt>
<dd>{$ ctrl.node.chassis_uuid $}</dd> <dd>{$ ctrl.node.chassis_uuid | noValue $}</dd>
<dt translate>Created At</dt> <dt translate>Created At</dt>
<dd>{$ ctrl.node.created_at $}</dd> <dd>{$ ctrl.node.created_at | date:'medium' $}</dd>
<dt translate>Extra</dt> <dt translate>Extra</dt>
<dd>{$ ctrl.node.extra $}</dd> <dd>{$ ctrl.node.extra $}</dd>
</dl> </dl>
@ -20,19 +20,89 @@
<div class="col-md-6 status detail"> <div class="col-md-6 status detail">
<h4 translate>Ports</h4> <h4 translate>Ports</h4>
<hr class="header_rule"> <hr class="header_rule">
<dl class="dl-horizontal"> <table hz-table ng-cloak
<dt ng-repeat-start="port in ctrl.ports"> st-table="ctrl.ports"
{$ 'MAC ' + (1 + $index) $}</dt> st-safe-src="ctrl.portsSrc"
<dd ng-if="vif_port_id = ctrl.getVifPortId(port)"> class="table table-striped table-rsp table-detail">
<thead>
<tr>
<th colspan="4" class="action-col">
<action-list dropdown class="pull-right">
<action button-type="split-button"
action-classes="'btn btn-default btn-sm'"
callback="ctrl.createPort">
{$ 'Create port' | translate $}
</action>
<menu>
<action button-type="menu-item"
callback="ctrl.deletePorts"
item="tCtrl.selected"
disabled="tCtrl.selected.length === 0">
<span class="fa fa-trash"></span>
{$ 'Delete ports' | translate $}
</action>
</menu>
</action-list>
</th>
</tr>
<tr>
<th class="multi_select_column">
<input type="checkbox"
hz-select-all="ctrl.ports"/>
</th>
<th translate class="rsp-p1" style="white-space:nowrap">
MAC Address
</th>
<th translate class="rsp-p2" style="width:100%;">
Extra
</th>
<th translate class="actions_column">
Actions
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="port in ctrl.ports">
<td class="multi_select_column">
<input type="checkbox"
hz-select="port"
ng-model="tCtrl.selections[port.id].checked"/>
<td ng-if="vif_port_id = ctrl.getVifPortId(port)"
class="rsp-p1">
<a href="/dashboard/admin/networks/ports/{$ vif_port_id $}/detail"> <a href="/dashboard/admin/networks/ports/{$ vif_port_id $}/detail">
{$ port.address $} {$ port.address $}
</a> </a>
</dd> </td>
<dd ng-if="!vif_port_id"> <td ng-if="!vif_port_id" class="rsp-p1">
{$ port.address $} {$ port.address $}
</td>
<td>
<dl class="dl-horizontal">
<dt style="width:auto;" ng-repeat-start="(id, value) in port.extra">
{$ id $}
</dt>
<dd>
{$ value $}
</dd> </dd>
<p ng-repeat-end></p> <p ng-repeat-end></p>
</dl> </dl>
</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>
</action-list>
</td>
</tr>
<tr hz-no-items
items="ctrl.ports"
message="ctrl.noPortsText">
</tr>
</tbody>
</table>
</div> </div>
</div> </div>

View File

@ -24,6 +24,7 @@
IronicNodeListController.$inject = [ IronicNodeListController.$inject = [
'$rootScope', '$rootScope',
'horizon.app.core.openstack-service-api.ironic', 'horizon.app.core.openstack-service-api.ironic',
'horizon.dashboard.admin.ironic.events',
'horizon.dashboard.admin.ironic.actions', 'horizon.dashboard.admin.ironic.actions',
'horizon.dashboard.admin.basePath', 'horizon.dashboard.admin.basePath',
'horizon.dashboard.admin.ironic.maintenance.service', 'horizon.dashboard.admin.ironic.maintenance.service',
@ -32,6 +33,7 @@
function IronicNodeListController($rootScope, function IronicNodeListController($rootScope,
ironic, ironic,
ironicEvents,
actions, actions,
basePath, basePath,
maintenanceService, maintenanceService,
@ -87,11 +89,19 @@
]; ];
// Listen for the creation of new nodes, and update the node list // Listen for the creation of new nodes, and update the node list
$rootScope.$on('ironic-ui:new-node', function() { $rootScope.$on(ironicEvents.ENROLL_NODE_SUCCESS, function() {
init(); init();
}); });
$rootScope.$on('ironic-ui:delete-node-success', function() { $rootScope.$on(ironicEvents.DELETE_NODE_SUCCESS, function() {
init();
});
$rootScope.$on(ironicEvents.CREATE_PORT_SUCCESS, function() {
init();
});
$rootScope.$on(ironicEvents.DELETE_PORT_SUCCESS, function() {
init(); init();
}); });

View File

@ -122,7 +122,7 @@
</div> </div>
</td> </td>
<td class="rsp-p2">{$ node.provision_state $}</td> <td class="rsp-p2">{$ node.provision_state $}</td>
<td class="rsp-p2">{$ node.maintenance $}</td> <td class="rsp-p2">{$ node.maintenance | yesno $}</td>
<td class="rsp-p2">{$ node.ports.length $}</td> <td class="rsp-p2">{$ node.ports.length $}</td>
<td class="rsp-p2">{$ node.driver $}</td> <td class="rsp-p2">{$ node.driver $}</td>
<td class="actions_column"> <td class="actions_column">
@ -160,6 +160,11 @@
<span class="fa fa-trash"></span> <span class="fa fa-trash"></span>
{$ 'Delete node' | translate $} {$ 'Delete node' | translate $}
</action> </action>
<action button-type="menu-item"
callback="table.actions.createPort"
item="node">
{$ 'Create port' | translate $}
</action>
</menu> </menu>
</action-list> </action-list>
</td> </td>