Added support for creating portgroups

The existing functionality for portgroups in the node-details/configuration
tab has been augmented with the capability to create portgroups. Clicking
on the menu button above the portgroup table will launch a modal dialog
that guides the user in entering the parameters required to create a
portgroup.

Change-Id: Ieef7fff4f29318ae74e11559dc69e6317c3c25d6
This commit is contained in:
Peter Piela 2017-07-26 11:14:53 -04:00
parent 35ad02697e
commit 7dc721fd79
9 changed files with 503 additions and 1 deletions

View File

@ -0,0 +1,103 @@
/*
* 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';
/**
* Controller used to support operations on an Ironic portgroup
*/
angular
.module('horizon.dashboard.admin.ironic')
.controller('BasePortgroupController', BasePortgroupController);
BasePortgroupController.$inject = [
'$uibModalInstance',
'horizon.dashboard.admin.ironic.validMacAddressPattern',
'horizon.dashboard.admin.ironic.validDatapathIdPattern',
'horizon.dashboard.admin.ironic.form-field.service',
'horizon.dashboard.admin.ironic.property-collection.service',
'ctrl'
];
function BasePortgroupController($uibModalInstance,
validMacAddressPattern,
validDatapathIdPattern,
formFieldService,
propertyCollectionService,
ctrl) {
ctrl.address = new formFieldService.FormField({
id: "macAddress",
title: gettext("MAC address"),
desc: gettext("MAC address for this portgroup."),
pattern: new RegExp(validMacAddressPattern),
value: null,
autoFocus: true
});
ctrl.name = new formFieldService.FormField({
id: "portgroupName",
title: gettext("Name"),
desc: gettext("Name for the portgroup.")
});
ctrl.standalone_ports_supported = new formFieldService.FormField({
type: "radio",
id: "standalonePorts",
title: gettext("Standalone Ports Supported"),
desc: gettext(
"Specifies whether ports in this portgroup can be used as standalone ports."),
options: ['True', 'False'],
value: 'True'});
ctrl.mode = new formFieldService.FormField({
type: "radio",
id: "mode",
title: gettext("Mode"),
desc: gettext("Linux portgroup mode. For possible values refer to https://www.kernel.org/doc/Documentation/networking/bonding.txt"), // eslint-disable-line max-len
options: ['balance-rr',
'active-backup',
'balance-xor',
'broadcast',
'802.3ad',
'balance-tlb',
'balance-alb'],
value: 'active-backup'});
ctrl.properties = new propertyCollectionService.PropertyCollection({
id: 'properties',
title: gettext('Properties'),
addPropertyLabel: gettext('Add Property'),
placeholder: gettext('Property Name')
});
ctrl.extra = new propertyCollectionService.PropertyCollection({
id: 'extra',
title: gettext('Extras'),
addPropertyLabel: gettext('Add Extra'),
placeholder: gettext('Property Name')
});
/**
* Cancel the modal
*
* @return {void}
*/
ctrl.cancel = function() {
$uibModalInstance.dismiss('cancel');
};
}
})();

View File

@ -0,0 +1,61 @@
/*
* 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';
describe('horizon.dashboard.admin.ironic.base-portgroup', function () {
var uibModalInstance;
var ctrl = {};
beforeEach(module('horizon.dashboard.admin.ironic'));
beforeEach(module(function($provide) {
$provide.value('$uibModal', {});
}));
beforeEach(module(function($provide) {
uibModalInstance = {
dismiss: jasmine.createSpy()
};
$provide.value('$uibModalInstance', uibModalInstance);
}));
beforeEach(inject(function($injector) {
var controller = $injector.get('$controller');
controller('BasePortgroupController', {ctrl: ctrl});
}));
it('controller should be defined', function () {
expect(ctrl).toBeDefined();
});
it('base construction', function () {
expect(Object.getOwnPropertyNames(ctrl).sort()).toEqual(
BASE_PORTGROUP_CONTROLLER_PROPERTIES.sort());
angular.forEach(
['address', 'name', 'standalone_ports_supported', 'mode'],
function(propertyName) {
expect(Object.keys(ctrl[propertyName])).toContain('value');
});
});
it('cancel', function () {
ctrl.cancel();
expect(uibModalInstance.dismiss).toHaveBeenCalledWith('cancel');
});
});
})();

View File

@ -0,0 +1,37 @@
<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">{$ ::ctrl.modalTitle $}</h3>
</div>
<div class="modal-body">
<form id="CreatePortgroupForm" name="CreatePortgroupForm">
<form-field field="ctrl.address" form="CreatePortgroupForm"></form-field>
<form-field field="ctrl.name" form="CreatePortgroupForm"></form-field>
<form-field field="ctrl.standalone_ports_supported"
form="CreatePortgroupForm"></form-field>
<form-field field="ctrl.mode" form="CreatePortgroupForm"></form-field>
</form>
<property-collection-editor collection="ctrl.properties"></property-collection-editor>
<property-collection-editor collection="ctrl.extra"></property-collection-editor>
</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="CreatePortgroupForm.$invalid ||
!ctrl.properties.complete() ||
!ctrl.extra.complete()"
ng-click="ctrl.submit()"
class="btn btn-primary">
{$ ::ctrl.submitButtonTitle $}
</button>
</div>

View File

@ -0,0 +1,74 @@
/*
* 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';
/**
* Controller used to create a portgroup on a specified node
*/
angular
.module('horizon.dashboard.admin.ironic')
.controller('CreatePortgroupController', CreatePortgroupController);
CreatePortgroupController.$inject = [
'$controller',
'$uibModalInstance',
'horizon.app.core.openstack-service-api.ironic',
'node'
];
function CreatePortgroupController($controller,
$uibModalInstance,
ironic,
node) {
var ctrl = this;
$controller('BasePortgroupController',
{ctrl: ctrl,
$uibModalInstance: $uibModalInstance});
ctrl.modalTitle = gettext("Create Portgroup");
ctrl.submitButtonTitle = ctrl.modalTitle;
/**
* Create the defined portgroup
*
* @return {void}
*/
ctrl.createPortgroup = function() {
var portgroup = {
extra: ctrl.extra.properties,
properties: ctrl.properties.properties
};
portgroup.node_uuid = node.uuid;
angular.forEach(['address', 'name', 'standalone_ports_supported', 'mode'],
function(propertyName) {
if (ctrl[propertyName].hasValue()) {
portgroup[propertyName] = ctrl[propertyName].value;
}
});
ironic.createPortgroup(portgroup).then(
function(createdPortgroup) {
$uibModalInstance.close(createdPortgroup);
});
};
ctrl.submit = function() {
ctrl.createPortgroup();
};
}
})();

View File

@ -0,0 +1,145 @@
/*
* 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';
describe('horizon.dashboard.admin.ironic.create-portgroup', function () {
var ironicBackendMockService, uibModalInstance, ironicAPI, controller;
beforeEach(module('horizon.dashboard.admin.ironic'));
beforeEach(module('horizon.framework.util'));
beforeEach(module(function($provide) {
$provide.value('$uibModal', {});
}));
beforeEach(module(function($provide) {
uibModalInstance = {};
$provide.value('$uibModalInstance', uibModalInstance);
}));
beforeEach(module(function($provide) {
$provide.value('horizon.framework.widgets.toast.service', {
add: function() {}
});
}));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(inject(function($injector) {
ironicBackendMockService =
$injector.get('horizon.dashboard.admin.ironic.backend-mock.service');
ironicBackendMockService.init();
ironicAPI =
$injector.get('horizon.app.core.openstack-service-api.ironic');
controller = $injector.get('$controller');
}));
afterEach(function() {
ironicBackendMockService.postTest();
});
function createController() {
return ironicAPI.createNode({
driver: ironicBackendMockService.params.defaultDriver})
.then(function(response) {
var node = response.data;
return {node: response.data,
ctrl: controller('CreatePortgroupController',
{node: node})};
});
}
it('controller should be defined', function () {
createController()
.then(function(data) {
expect(data.ctrl).toBeDefined();
})
.catch(function() {
fail();
});
ironicBackendMockService.flush();
});
it('base construction', function () {
createController()
.then(function(data) {
var ctrl = data.ctrl;
var properties = angular.copy(BASE_PORTGROUP_CONTROLLER_PROPERTIES);
properties.push('modalTitle');
properties.push('submitButtonTitle');
properties.push('createPortgroup');
properties.push('submit');
expect(Object.getOwnPropertyNames(ctrl).sort()).toEqual(
properties.sort());
})
.catch(function() {
fail();
});
ironicBackendMockService.flush();
});
it('submit - success', function () {
var portgroupParams = {
address: '00:00:00:00:00:00',
name: 'my-portgroup',
standalone_ports_supported: 'False',
mode: '802.3ad',
properties: {
prop_1: 'prop-1-value'
},
extra: {
extra_1: 'extra-1-value'
}
};
spyOn(ironicAPI, 'createPortgroup').and.callThrough();
uibModalInstance.close = function(portgroup) {
expect(portgroup).toEqual(
ironicBackendMockService.getPortgroup(portgroupParams.name));
};
createController()
.then(function(data) {
var ctrl = data.ctrl;
angular.forEach(
portgroupParams,
function(value, param) {
if (param === 'properties' ||
param === 'extra') {
ctrl[param].properties = value;
} else {
ctrl[param].value = value;
}
});
portgroupParams.node_uuid = data.node.uuid;
ctrl.submit();
expect(ironicAPI.createPortgroup)
.toHaveBeenCalledWith(portgroupParams);
})
.catch(function() {
fail();
});
ironicBackendMockService.flush();
});
});
})();

View File

@ -0,0 +1,56 @@
/*
* 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.create-portgroup.service',
createPortgroupService);
createPortgroupService.$inject = [
'$uibModal',
'horizon.dashboard.admin.ironic.basePath'
];
function createPortgroupService($uibModal, basePath) {
var service = {
createPortgroup: createPortgroup
};
return service;
/**
* @description Launch a modal dialog that will guide the user
* in creating a new portgroup
*
* @param {object} node - Node to which the portgroup will be associated
* @return {promise} Object describing the created portgroup
*/
function createPortgroup(node) {
var options = {
controller: 'CreatePortgroupController as ctrl',
backdrop: 'static',
resolve: {
node: function() {
return node;
}
},
templateUrl: basePath + '/base-portgroup/base-portgroup.html'
};
return $uibModal.open(options).result;
}
}
})();

View File

@ -31,6 +31,7 @@
'horizon.dashboard.admin.ironic.basePath', 'horizon.dashboard.admin.ironic.basePath',
'horizon.dashboard.admin.ironic.edit-node.service', 'horizon.dashboard.admin.ironic.edit-node.service',
'horizon.dashboard.admin.ironic.create-port.service', 'horizon.dashboard.admin.ironic.create-port.service',
'horizon.dashboard.admin.ironic.create-portgroup.service',
'horizon.dashboard.admin.ironic.edit-port.service', 'horizon.dashboard.admin.ironic.edit-port.service',
'horizon.dashboard.admin.ironic.maintenance.service', 'horizon.dashboard.admin.ironic.maintenance.service',
'horizon.dashboard.admin.ironic.bootdevice.service', 'horizon.dashboard.admin.ironic.bootdevice.service',
@ -46,6 +47,7 @@
basePath, basePath,
editNodeService, editNodeService,
createPortService, createPortService,
createPortgroupService,
editPortService, editPortService,
maintenanceService, maintenanceService,
bootDeviceService, bootDeviceService,
@ -90,6 +92,7 @@
ctrl.getVifPortId = getVifPortId; ctrl.getVifPortId = getVifPortId;
ctrl.editNode = editNode; ctrl.editNode = editNode;
ctrl.createPort = createPort; ctrl.createPort = createPort;
ctrl.createPortgroup = createPortgroup;
ctrl.deletePort = deletePort; ctrl.deletePort = deletePort;
ctrl.editPort = editPort; ctrl.editPort = editPort;
ctrl.refresh = refresh; ctrl.refresh = refresh;
@ -318,5 +321,18 @@
function toggleConsoleMode() { function toggleConsoleMode() {
ironic.nodeSetConsoleMode(ctrl.node.uuid, !ctrl.node.console_enabled); ironic.nodeSetConsoleMode(ctrl.node.uuid, !ctrl.node.console_enabled);
} }
/**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.createPortgroup
* @description Initiate creation of a portgroup for the current
* node
*
* @return {void}
*/
function createPortgroup() {
createPortgroupService.createPortgroup(ctrl.node).then(function() {
ctrl.refresh();
});
}
} }
})(); })();

View File

@ -138,7 +138,7 @@
<action-list uib-dropdown class="pull-right"> <action-list uib-dropdown class="pull-right">
<action button-type="split-button" <action button-type="split-button"
action-classes="'btn btn-default btn-sm'" action-classes="'btn btn-default btn-sm'"
callback=""> callback="ctrl.createPortgroup">
{$ ::'Create portgroup' | translate $} {$ ::'Create portgroup' | translate $}
</action> </action>
<menu> <menu>

View File

@ -41,6 +41,16 @@ var BASE_NODE_CONTROLLER_PROPERTIES = [
'submitButtonTitle', 'submitButtonTitle',
'validHostNameRegex']; 'validHostNameRegex'];
/* exported BASE_PORTGROUP_CONTROLLER_PROPERTIES */
var BASE_PORTGROUP_CONTROLLER_PROPERTIES = [
'address',
'cancel',
'extra',
'mode',
'name',
'properties',
'standalone_ports_supported'];
/* exported PROPERTY_COLLECTION_PROPERTIES */ /* exported PROPERTY_COLLECTION_PROPERTIES */
var PROPERTY_COLLECTION_PROPERTIES = [ var PROPERTY_COLLECTION_PROPERTIES = [