diff --git a/ironic_ui/api/ironic.py b/ironic_ui/api/ironic.py index 7163096c..ad160743 100755 --- a/ironic_ui/api/ironic.py +++ b/ironic_ui/api/ironic.py @@ -290,3 +290,40 @@ def port_update(request, port_uuid, patch): port = ironicclient(request).port.update(port_uuid, patch) return dict([(f, getattr(port, f, '')) for f in res_fields.PORT_DETAILED_RESOURCE.fields]) + + +def portgroup_list(request, node_id): + """List the portgroups associated with a given node. + + :param request: HTTP request. + :param node_id: The UUID or name of the node. + :return: A full list of portgroups. (limit=0) + + http://docs.openstack.org/developer/python-ironicclient/api/ironicclient.v1.portgroup.html#ironicclient.v1.portgroup.PortgroupManager.list_portgroups + """ + return ironicclient(request).portgroup.list(node_id, limit=0, detail=True) + + +def portgroup_create(request, params): + """Create a portgroup. + + :param request: HTTP request. + :param params: Portgroup creation parameters. + :return: Portgroup. + + http://docs.openstack.org/developer/python-ironicclient/api/ironicclient.v1.portgroup.html#ironicclient.v1.portgroup.PortgroupManager.create + """ + portgroup_manager = ironicclient(request).portgroup + return portgroup_manager.create(**params) + + +def portgroup_delete(request, portgroup_id): + """Delete a portgroup from the DB. + + :param request: HTTP request. + :param portgroup_id: The UUID or name of the portgroup. + :return: Portgroup. + + http://docs.openstack.org/developer/python-ironicclient/api/ironicclient.v1.portgroup.html#ironicclient.v1.portgroup.PortgroupManager.delete + """ + return ironicclient(request).portgroup.delete(portgroup_id) diff --git a/ironic_ui/api/ironic_rest_api.py b/ironic_ui/api/ironic_rest_api.py index 5550b99c..03f12f5b 100755 --- a/ironic_ui/api/ironic_rest_api.py +++ b/ironic_ui/api/ironic_rest_api.py @@ -310,3 +310,40 @@ class DriverProperties(generic.View): :return: Dictionary of properties """ return ironic.driver_properties(request, driver_name) + + +@urls.register +class Portgroups(generic.View): + + url_regex = r'ironic/portgroups/$' + + @rest_utils.ajax() + def get(self, request): + """Get the list of portgroups associated with a specified node. + + :param request: HTTP request. + :return: List of portgroups. + """ + portgroups = ironic.portgroup_list(request, + request.GET.get('node_id')) + return { + 'portgroups': [i.to_dict() for i in portgroups] + } + + @rest_utils.ajax(data_required=True) + def post(self, request): + """Create a portgroup. + + :param request: HTTP request. + :return: Portgroup. + """ + return ironic.portgroup_create(request, request.DATA).to_dict() + + @rest_utils.ajax(data_required=True) + def delete(self, request): + """Delete a portgroup. + + :param request: HTTP request. + """ + return ironic.portgroup_delete(request, + request.DATA.get('portgroup_id')) diff --git a/ironic_ui/static/dashboard/admin/ironic/ironic.backend-mock.service.js b/ironic_ui/static/dashboard/admin/ironic/ironic.backend-mock.service.js index ace5834f..07612224 100644 --- a/ironic_ui/static/dashboard/admin/ironic/ironic.backend-mock.service.js +++ b/ironic_ui/static/dashboard/admin/ironic/ironic.backend-mock.service.js @@ -82,6 +82,22 @@ uuid: undefined }; + // Default portgroup object. + var defaultPortgroup = { + address: null, + created_at: null, + extra: {}, + internal_info: {}, + mode: "active-backup", + name: null, + node_uuid: undefined, + ports: [], + properties: {}, + standalone_ports_supported: true, + updated_at: null, + uuid: undefined + }; + // Value of the next available system port var nextAvailableSystemPort = 1024; @@ -110,7 +126,8 @@ nodeGetConsoleUrl: nodeGetConsoleUrl, getDrivers: getDrivers, getImages: getImages, - getPort: getPort + getPort: getPort, + getPortgroup: getPortgroup }; var responseCode = { @@ -127,6 +144,9 @@ // Dictionary of active ports indexed by port-uuid var ports = {}; + // Dictionary of active portgroups indexed by portgroup-uuid + var portgroups = {}; + return service; /** @@ -162,7 +182,8 @@ var backendNode = { base: node, consolePort: getNextAvailableSystemPort(), - ports: {} // Indexed by port-uuid + ports: {}, // Indexed by port-uuid + portgroups: {} // Indexed by portgroup-uuid }; nodes[node.uuid] = backendNode; @@ -256,6 +277,47 @@ return [status, port]; } + /** + * @description Create a portgroup. + * This function is not yet fully implemented. + * + * @param {object} params - Dictionary of parameters that define + * the portgroup to be created. + * @return {object|null} Portgroup object, or null if the port could + * not be created. + */ + function createPortgroup(params) { + var portgroup = null; + var status = responseCode.BAD_QUERY; + if (angular.isDefined(nodes[params.node_uuid])) { + if (angular.isDefined(params.name) && + params.name !== null && + angular.isDefined(portgroups[params.name])) { + status = responseCode.RESOURCE_CONFLICT; + } else { + portgroup = angular.copy(defaultPortgroup); + angular.forEach(params, function(value, key) { + portgroup[key] = value; + }); + + if (angular.isUndefined(portgroup.uuid)) { + portgroup.uuid = uuidService.generate(); + } + + portgroups[portgroup.uuid] = portgroup; + if (portgroup.name !== null) { + portgroups[portgroup.name] = portgroup; + } + + nodes[portgroup.node_uuid].portgroups[portgroup.uuid] = portgroup; + } + + status = responseCode.SUCCESS; + } + + return [status, portgroup]; + } + /** * description Get a specified port. * @@ -267,6 +329,18 @@ return angular.isDefined(ports[portUuid]) ? ports[portUuid] : null; } + /** + * description Get a specified portgroup. + * + * @param {string} portgroupId - Uuid or name of the requested portgroup. + * @return {object|null} Portgroup object, or null if the portgroup + * does not exist. + */ + function getPortgroup(portgroupId) { + return angular.isDefined(portgroups[portgroupId]) + ? portgroups[portgroupId] : null; + } + /** * @description Initialize the Backend-Mock service. * Create the handlers that intercept http requests. @@ -473,6 +547,45 @@ } return [status, {ports: ports}]; }); + + // Create portgroup + $httpBackend.whenPOST(/\/api\/ironic\/portgroups\/$/) + .respond(function(method, url, data) { + return createPortgroup(JSON.parse(data)); + }); + + // Get portgroups. This function is not fully implemented. + $httpBackend.whenGET(/\/api\/ironic\/ports\/$/) + .respond(function(method, url, data) { + var nodeId = JSON.parse(data).node_id; + var status = responseCode.RESOURCE_NOT_FOUND; + var portgroups = []; + if (angular.isDefined(nodes[nodeId])) { + angular.forEach(nodes[nodeId].portgroups, function(portgroup) { + portgroups.push(portgroup); + }); + status = responseCode.SUCCESS; + } + return [status, {portgroups: portgroups}]; + }); + + // Delete portgroup. This function is not yet implemented. + $httpBackend.whenDELETE(/\/api\/ironic\/portgroups\/$/) + .respond(function(method, url, data) { + var portgroupId = JSON.parse(data).portgroup_id; + var status = responseCode.RESOURCE_NOT_FOUND; + if (angular.isDefined(portgroups[portgroupId])) { + var portgroup = portgroups[portgroupId]; + if (portgroup.name !== null) { + delete portgroups[portgroup.name]; + delete portgroups[portgroup.uuid]; + } else { + delete portgroups[portgroupId]; + } + status = responseCode.EMPTY_RESPONSE; + } + return [status, ""]; + }); } /** diff --git a/ironic_ui/static/dashboard/admin/ironic/ironic.service.js b/ironic_ui/static/dashboard/admin/ironic/ironic.service.js index e3e36e00..9a5e5cb4 100755 --- a/ironic_ui/static/dashboard/admin/ironic/ironic.service.js +++ b/ironic_ui/static/dashboard/admin/ironic/ironic.service.js @@ -57,7 +57,10 @@ setNodeProvisionState: setNodeProvisionState, updateNode: updateNode, updatePort: updatePort, - validateNode: validateNode + validateNode: validateNode, + createPortgroup: createPortgroup, + getPortgroups: getPortgroups, + deletePortgroup: deletePortgroup }; return service; @@ -513,6 +516,79 @@ return $q.reject(msg); }); } - } + /** + * @description Retrieve a list of portgroups associated with a node. + * + * http://developer.openstack.org/api-ref/baremetal/#list-detailed-portgroups + * + * @param {string} nodeId – UUID or logical name of a node. + * @return {promise} List of portgroups. + */ + function getPortgroups(nodeId) { + return apiService.get('/api/ironic/portgroups/', + {params: {node_id: nodeId}}) + .then(function(response) { + // Add id property to support delete operations + // using the deleteModalService + angular.forEach(response.data.portgroups, function(portgroup) { + portgroup.id = portgroup.uuid; + }); + return response.data.portgroups; + }) + .catch(function(response) { + var msg = interpolate( + gettext('Unable to retrieve Ironic node portgroups: %s'), + [response.data], + false); + toastService.add('error', msg); + return $q.reject(msg); + }); + } + + /** + * @description Create a protgroup. + * + * http://developer.openstack.org/api-ref/baremetal/#create-portgroup + * + * @param {object} params – Object containing parameters that define + * the portgroup to be created. + * @return {promise} Promise containing the portgroup. + */ + function createPortgroup(params) { + return apiService.post('/api/ironic/portgroups/', params) + .then(function(response) { + toastService.add('success', + gettext('Portgroup successfully created')); + return response.data; // The newly created portgroup + }) + .catch(function(response) { + var msg = interpolate(gettext('Unable to create portgroup: %s'), + [response.data], + false); + toastService.add('error', msg); + return $q.reject(msg); + }); + } + + /** + * @description Delete a portgroup. + * + * http://developer.openstack.org/api-ref/baremetal/#delete-portgroup + * + * @param {string} portgroupId – UUID or name of the portgroup to be deleted. + * @return {promise} Promise. + */ + function deletePortgroup(portgroupId) { + return apiService.delete('/api/ironic/portgroups/', + {portgroup_id: portgroupId}) + .catch(function(response) { + var msg = interpolate(gettext('Unable to delete portgroup: %s'), + [response.data], + false); + toastService.add('error', msg); + return $q.reject(msg); + }); + } + } }()); diff --git a/ironic_ui/static/dashboard/admin/ironic/ironic.service.spec.js b/ironic_ui/static/dashboard/admin/ironic/ironic.service.spec.js index 897a4125..f6d529e5 100644 --- a/ironic_ui/static/dashboard/admin/ironic/ironic.service.spec.js +++ b/ironic_ui/static/dashboard/admin/ironic/ironic.service.spec.js @@ -20,12 +20,15 @@ var IRONIC_API_PROPERTIES = [ 'createNode', 'createPort', + 'createPortgroup', 'deleteNode', 'deletePort', + 'deletePortgroup', 'getDrivers', 'getDriverProperties', 'getNode', 'getNodes', + 'getPortgroups', 'getPortsWithNode', 'getBootDevice', 'nodeGetConsole', @@ -402,6 +405,112 @@ ironicBackendMockService.flush(); }); + + it('createPortgroup', function() { + var node; + createNode({driver: defaultDriver}) + .then(function(createNode) { + node = createNode; + return ironicAPI.createPortgroup({node_uuid: node.uuid}); + }) + .then(function(portgroup) { + expect(portgroup.node_uuid).toBe(node.uuid); + expect(portgroup) + .toEqual(ironicBackendMockService.getPortgroup(portgroup.uuid)); + }) + .catch(failTest); + + ironicBackendMockService.flush(); + }); + + it('createPortgroup - specify portgroup name', function() { + var node; + var portgroupName = "test-portgroup"; + + createNode({driver: defaultDriver}) + .then(function(createNode) { + node = createNode; + return ironicAPI.createPortgroup({node_uuid: node.uuid, + name: portgroupName}); + }) + .then(function(portgroup) { + expect(portgroup.node_uuid).toBe(node.uuid); + expect(portgroup.name).toBe(portgroupName); + expect(portgroup) + .toEqual(ironicBackendMockService.getPortgroup(portgroup.uuid)); + expect(portgroup) + .toEqual(ironicBackendMockService.getPortgroup(portgroup.name)); + }) + .catch(failTest); + + ironicBackendMockService.flush(); + }); + + it('createPortgroup - missing input data', function() { + ironicAPI.createPortgroup({}) + .then(failTest); + + ironicBackendMockService.flush(); + }); + + it('createPort - bad input data', function() { + ironicAPI.createPort({node_uuid: ""}) + .then(failTest); + + ironicBackendMockService.flush(); + }); + + it('deletePortgroup', function() { + createNode({driver: defaultDriver}) + .then(function(node) { + return ironicAPI.createPortgroup({node_uuid: node.uuid}); + }) + .then(function(portgroup) { + expect(portgroup).toBeDefined(); + expect(portgroup) + .toEqual(ironicBackendMockService.getPortgroup(portgroup.uuid)); + ironicAPI.deletePortgroup(portgroup.uuid).then(function() { + expect(ironicBackendMockService.getPortgroup(portgroup.uuid)) + .toBeNull(); + }); + }) + .catch(failTest); + + ironicBackendMockService.flush(); + }); + + it('deletePortgroup - by name', function() { + var portgroupName = "delete-portgroup"; + + createNode({driver: defaultDriver}) + .then(function(node) { + return ironicAPI.createPortgroup({node_uuid: node.uuid, + name: portgroupName}); + }) + .then(function(portgroup) { + expect(portgroup).toBeDefined(); + expect(portgroup) + .toEqual(ironicBackendMockService.getPortgroup(portgroup.uuid)); + expect(portgroup) + .toEqual(ironicBackendMockService.getPortgroup(portgroup.name)); + ironicAPI.deletePortgroup(portgroup.name).then(function() { + expect(ironicBackendMockService.getPortgroup(portgroup.name)) + .toBeNull(); + expect(ironicBackendMockService.getPortgroup(portgroup.uuid)) + .toBeNull(); + }); + }) + .catch(failTest); + + ironicBackendMockService.flush(); + }); + + it('deletePortgroup - nonexistent portgroup', function() { + ironicAPI.deletePortgroup(0) + .then(failTest); + + ironicBackendMockService.flush(); + }); }); }); })(); diff --git a/ironic_ui/static/dashboard/admin/ironic/node-actions.service.js b/ironic_ui/static/dashboard/admin/ironic/node-actions.service.js index 78ec8b97..f3352185 100755 --- a/ironic_ui/static/dashboard/admin/ironic/node-actions.service.js +++ b/ironic_ui/static/dashboard/admin/ironic/node-actions.service.js @@ -59,6 +59,7 @@ var service = { deleteNode: deleteNode, deletePort: deletePort, + deletePortgroups: deletePortgroups, setPowerState: setPowerState, setMaintenance: setMaintenance, setProvisionState: setProvisionState, @@ -188,7 +189,7 @@ 'Successfully deleted ports "%s"', ports.length), error: ngettext('Unable to delete port "%s"', - 'Unable to delete portss "%s"', + 'Unable to delete ports "%s"', ports.length) }, deleteEntity: ironic.deletePort, @@ -197,6 +198,32 @@ return deleteModalService.open($rootScope, ports, context); } + function deletePortgroups(portgroups) { + var context = { + labels: { + title: ngettext("Delete Portgroup", + "Delete Portgroups", + portgroups.length), + message: ngettext('Are you sure you want to delete portgroup "%s"? ' + + 'This action cannot be undone.', + 'Are you sure you want to delete portgroups "%s"? ' + + 'This action cannot be undone.', + portgroups.length), + submit: ngettext("Delete Portgroup", + "Delete Portgroups", + portgroups.length), + success: ngettext('Successfully deleted portgroup "%s"', + 'Successfully deleted portgroups "%s"', + portgroups.length), + error: ngettext('Unable to delete portgroup "%s"', + 'Unable to delete portgroups "%s"', + portgroups.length) + }, + deleteEntity: ironic.deletePortgroup + }; + return deleteModalService.open($rootScope, portgroups, context); + } + /* * @name horizon.dashboard.admin.ironic.actions.getPowerTransitions * @description Get the list of power transitions for a specified 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 ebb4730e..0e4479ca 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 @@ -53,6 +53,7 @@ var path = basePath + '/node-details/sections/'; ctrl.noPortsText = gettext('No network ports have been defined'); + ctrl.noPortgroupsText = gettext('No portgroups have been defined'); ctrl.actions = actions; ctrl.maintenanceService = maintenanceService; @@ -68,6 +69,9 @@ } ]; + ctrl.portDetailsTemplateUrl = path + "port-details.html"; + ctrl.portgroupDetailsTemplateUrl = path + "portgroup-details.html"; + ctrl.node = null; ctrl.nodeValidation = []; ctrl.nodeValidationMap = {}; // Indexed by interface @@ -75,6 +79,8 @@ ctrl.nodePowerTransitions = []; ctrl.ports = []; ctrl.portsSrc = []; + ctrl.portgroups = []; + ctrl.portgroupsSrc = []; ctrl.basePath = basePath; ctrl.re_uuid = new RegExp(validUuidPattern); ctrl.isUuid = isUuid; @@ -85,6 +91,7 @@ ctrl.editPort = editPort; ctrl.refresh = refresh; ctrl.toggleConsoleMode = toggleConsoleMode; + ctrl.deletePortgroups = deletePortgroups; $scope.emptyObject = function(obj) { return angular.isUndefined(obj) || Object.keys(obj).length === 0; @@ -112,6 +119,7 @@ ctrl.nodePowerTransitions = actions.getPowerTransitions(ctrl.node); retrievePorts(); retrieveBootDevice(); + retrievePortgroups(); validateNode(); }); } @@ -161,6 +169,19 @@ }); } + /** + * @name horizon.dashboard.admin.ironic.NodeDetailsController.retrievePortgroups + * @description Retrieve the port groups associated with the current node, + * and store them in the controller instance. + * + * @return {void} + */ + function retrievePortgroups() { + ironic.getPortgroups(ctrl.node.uuid).then(function(portgroups) { + ctrl.portgroupsSrc = portgroups; + }); + } + /** * @name horizon.dashboard.admin.ironic.NodeDetailsController.validateNode * @description Retrieve the ports associated with the current node, @@ -256,6 +277,19 @@ }); } + /** + * @name horizon.dashboard.admin.ironic.NodeDetailsController.portgroupDelete + * @description Delete a list of portgroups. + * + * @param {port []} portgroups – portgroups to be deleted. + * @return {void} + */ + function deletePortgroups(portgroups) { + actions.deletePortgroups(portgroups).then(function() { + ctrl.refresh(); + }); + } + /** * @name horizon.dashboard.admin.ironic.NodeDetailsController.refresh * @description Update node information 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 afc8104d..f47891cd 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 @@ -69,6 +69,10 @@ return $q.when(ports); }, + getPortgroups: function() { + return $q.when([]); + }, + getBootDevice: function () { return $q.when(bootDevice); }, 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 1203c2aa..5d62ee7d 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 @@ -1,7 +1,7 @@
-
+

General


@@ -15,7 +15,9 @@
{$ ctrl.node.created_at | date:'medium' | noValue $}
+
+

Ports

@@ -26,7 +28,7 @@ class="table table-striped table-rsp table-detail"> - + +   MAC Address - - Properties + + PXE Enabled + + + Portgroup Actions @@ -62,34 +68,24 @@ - + + + + {$ port.address $} -
    -
  • pxe_enabled: {$ port.pxe_enabled $}
  • -
  • - {$ propertyObject $}: - -
  • -
+ {$ port.pxe_enabled $} + + {$ port.portgroup_uuid | noValue $} @@ -113,6 +109,13 @@ + + + + + + @@ -120,6 +123,113 @@
+ + +
+

Portgroups

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + {$ ::'Create portgroup' | translate $} + + + + + {$ ::'Delete portgroups' | translate $} + + + +
+ +   + UUID + + MAC Address + + Name + + Actions +
+ + + + + {$ portgroup.uuid $} + + {$ portgroup.address | noValue $} + + {$ portgroup.name | noValue $} + + + + {$ ::'Edit portgroup' | translate $} + + +
  • + + + {$ :: 'Delete portgroup' | translate $} + +
  • +
    +
    +
    + + +
    +
    diff --git a/ironic_ui/static/dashboard/admin/ironic/node-details/sections/port-details.html b/ironic_ui/static/dashboard/admin/ironic/node-details/sections/port-details.html new file mode 100644 index 00000000..05a5327f --- /dev/null +++ b/ironic_ui/static/dashboard/admin/ironic/node-details/sections/port-details.html @@ -0,0 +1,32 @@ +
    +
    +
    Attributes
    +
    +
    +
    UUID
    +
    {$ port.uuid $} +
    +
    +
    +
    Local Link Connection
    +
    +
    +
    {$ id $}
    +
    {$ value $}
    +
    +
    +
    +
    Extra
    +
    +
    +
    {$ id $}
    +
    + + {$ value $} + {$ value $} + +
    +
    +
    +
    diff --git a/ironic_ui/static/dashboard/admin/ironic/node-details/sections/portgroup-details.html b/ironic_ui/static/dashboard/admin/ironic/node-details/sections/portgroup-details.html new file mode 100644 index 00000000..a30ffea4 --- /dev/null +++ b/ironic_ui/static/dashboard/admin/ironic/node-details/sections/portgroup-details.html @@ -0,0 +1,28 @@ +
    +
    +
    Attributes
    +
    +
    +
    Mode
    +
    {$ portgroup.mode $}
    +
    SA ports
    +
    {$ portgroup.standalone_ports_supported $}
    +
    +
    +
    +
    Properties
    +
    +
    +
    {$ id $}
    +
    {$ value $}
    +
    +
    +
    +
    Extra
    +
    +
    +
    {$ id $}
    +
    {$ value $}
    +
    +
    +
    diff --git a/releasenotes/notes/view-portgroups-a3efb4407536caf2.yaml b/releasenotes/notes/view-portgroups-a3efb4407536caf2.yaml new file mode 100644 index 00000000..14f6b556 --- /dev/null +++ b/releasenotes/notes/view-portgroups-a3efb4407536caf2.yaml @@ -0,0 +1,27 @@ +--- +features: + - | + Support has been added for viewing and managing the portgroups + associated with an Ironic node. + - | + A portgroup table has been added to the node-details/configuration tab. + - | + Each row in the table displays a single portgroup, and has column entries + for its UUID, MAC address, name, and number of ports. A dropdown menu + is also provided that contains actions that can be applied to the + portgroup. + - | + Detailed information for a portgroup is obtained by clicking the + detail-toggle-selector (right-chevron) located in its table row. + The additional information is displayed in a row expansion. + - | + The port table in node-details/configuration tab has been modified + as follows: + + * A column has been added that displays the UUID of the portgroup + to which the port belongs. + * The ``Properties`` column has been replaced with a column that + displays only the pxe_enabled property. + * Additional properties are displayed by clicking the + detail-toggle-selector for that port in a similar manner to the + portgroup table.