Allow selecting interfaces while enrolling nodes

Adds support for selecting from the enabled interfaces for the
underlying driver while creating nodes. A new tab is added in the
enroll node modal.

Also enhanced "package.json" and "karma.conf.js" to widen the
range of accepted versions and jasmine capabilities.

Change-Id: Ie1b24cbf147b849a1d57fcdcbd735429ea7c9e34
Partial-Bug: #1672729
This commit is contained in:
Anup Navare 2017-06-13 19:15:25 +00:00 committed by Peter Piela
parent dfbe630ab2
commit c73492c877
19 changed files with 405 additions and 68 deletions

View File

@ -25,7 +25,7 @@ from horizon.utils.memoized import memoized # noqa
from openstack_dashboard.api import base from openstack_dashboard.api import base
DEFAULT_IRONIC_API_VERSION = '1.31' DEFAULT_IRONIC_API_VERSION = '1.34'
DEFAULT_INSECURE = False DEFAULT_INSECURE = False
DEFAULT_CACERT = None DEFAULT_CACERT = None
IRONIC_CLIENT_CLASS_NAME = 'baremetal' IRONIC_CLIENT_CLASS_NAME = 'baremetal'
@ -282,6 +282,19 @@ def driver_properties(request, driver_name):
return ironicclient(request).driver.properties(driver_name) return ironicclient(request).driver.properties(driver_name)
def driver_details(request, driver_name):
"""Retrieve the details of a specified driver
:param request: HTTP request
:param driver_name: Name of the driver
:return: dictionary of driver details
https://docs.openstack.org/python-ironicclient/latest/cli/osc/v1/index.html#baremetal-driver-show
"""
details = ironicclient(request).driver.get(driver_name)
return details.to_dict()
def port_create(request, params): def port_create(request, params):
"""Create network port """Create network port

View File

@ -450,3 +450,20 @@ class RaidConfig(generic.View):
request, request,
node_id, node_id,
request.DATA.get('target_raid_config')) request.DATA.get('target_raid_config'))
@urls.register
class DriverDetails(generic.View):
url_regex = r'ironic/drivers/(?P<driver_name>[0-9a-zA-Z_-]+)$'. \
format(LOGICAL_NAME_PATTERN)
@rest_utils.ajax()
def get(self, request, driver_name):
"""Get the details of a specified driver
:param request: HTTP request
:param driver_name: Driver name
:return: Dictionary of details
"""
return ironic.driver_details(request, driver_name)

View File

@ -44,6 +44,7 @@
* Django or via jasmine template. * Django or via jasmine template.
*/ */
'../test-shim.js', '../test-shim.js',
'../node_modules/string.prototype.endswith/*.js',
// from jasmine.html // from jasmine.html
toxPath + 'xstatic/pkg/jquery/data/jquery.js', toxPath + 'xstatic/pkg/jquery/data/jquery.js',
@ -113,9 +114,9 @@
autoWatch: true, autoWatch: true,
frameworks: ['jasmine'], frameworks: ['jasmine', 'jasmine-matchers'],
browsers: ['Chrome'], browsers: ['Chrome', 'PhantomJS'],
browserNoActivityTimeout: 60000, browserNoActivityTimeout: 60000,
@ -129,7 +130,9 @@
plugins: [ plugins: [
'karma-chrome-launcher', 'karma-chrome-launcher',
'karma-phantomjs-launcher',
'karma-jasmine', 'karma-jasmine',
'karma-jasmine-matchers',
'karma-ng-html2js-preprocessor', 'karma-ng-html2js-preprocessor',
'karma-coverage', 'karma-coverage',
'karma-threshold-reporter' 'karma-threshold-reporter'

View File

@ -27,31 +27,41 @@
'$uibModalInstance', '$uibModalInstance',
'horizon.app.core.openstack-service-api.ironic', 'horizon.app.core.openstack-service-api.ironic',
'horizon.app.core.openstack-service-api.glance', 'horizon.app.core.openstack-service-api.glance',
'horizon.dashboard.admin.ironic.form-field.service',
'horizon.dashboard.admin.ironic.base-node.service', 'horizon.dashboard.admin.ironic.base-node.service',
'horizon.dashboard.admin.ironic.driver-property.service', 'horizon.dashboard.admin.ironic.driver-property.service',
'horizon.dashboard.admin.ironic.graph.service', 'horizon.dashboard.admin.ironic.graph.service',
'horizon.dashboard.admin.ironic.validHostNamePattern', 'horizon.dashboard.admin.ironic.validHostNamePattern',
'horizon.dashboard.admin.ironic.driverInterfaces',
'$log', '$log',
'$q',
'ctrl' 'ctrl'
]; ];
function BaseNodeController($uibModalInstance, function BaseNodeController($uibModalInstance,
ironic, ironic,
glance, glance,
formFieldService,
baseNodeService, baseNodeService,
driverPropertyService, driverPropertyService,
graphService, graphService,
validHostNamePattern, validHostNamePattern,
driverInterfaces,
$log, $log,
$q,
ctrl) { ctrl) {
ctrl.validHostNameRegex = new RegExp(validHostNamePattern); ctrl.validHostNameRegex = new RegExp(validHostNamePattern);
ctrl.drivers = null; ctrl.drivers = null;
ctrl.images = null; ctrl.images = null;
ctrl.loadingDriverProperties = false; ctrl.loadingDriverProperties = false;
ctrl.driverType = null;
// Object containing the set of properties associated with the currently // Object containing the set of properties associated with the currently
// selected driver // selected driver
ctrl.driverProperties = null; ctrl.driverProperties = null;
ctrl.driverPropertyGroups = null; ctrl.driverPropertyGroups = null;
// Dictionary of form-fields for supported interfaces indexed by interface
// name for the currently selected driver
ctrl.driverInterfaceFields = {};
ctrl.modalTitle = gettext("Node"); ctrl.modalTitle = gettext("Node");
ctrl.submitButtonTitle = gettext("Submit"); ctrl.submitButtonTitle = gettext("Submit");
@ -91,10 +101,14 @@
name: null, name: null,
driver: null, driver: null,
driver_info: {}, driver_info: {},
network_interface: null,
resource_class: null resource_class: null
}; };
// Initialize hardware interfaces
angular.forEach(driverInterfaces, function(interfaceName) {
ctrl.node[interfaceName + '_interface'] = null;
});
angular.forEach(ctrl.propertyCollections, function(collection) { angular.forEach(ctrl.propertyCollections, function(collection) {
ctrl.node[collection.id] = {}; ctrl.node[collection.id] = {};
}); });
@ -189,8 +203,8 @@
* @param {string} driverName - Name of driver * @param {string} driverName - Name of driver
* @return {void} * @return {void}
*/ */
ctrl.loadDriverProperties = function(driverName) { ctrl._loadDriverProperties = function(driverName) {
ctrl.node.driver = driverName; ctrl.node.driver = null;
ctrl.node.driver_info = {}; ctrl.node.driver_info = {};
ctrl.loadingDriverProperties = true; ctrl.loadingDriverProperties = true;
@ -198,6 +212,7 @@
ctrl.driverPropertyGroups = null; ctrl.driverPropertyGroups = null;
return ironic.getDriverProperties(driverName).then(function(properties) { return ironic.getDriverProperties(driverName).then(function(properties) {
ctrl.node.driver = driverName;
ctrl.driverProperties = {}; ctrl.driverProperties = {};
angular.forEach(properties, function(desc, property) { angular.forEach(properties, function(desc, property) {
ctrl.driverProperties[property] = ctrl.driverProperties[property] =
@ -275,5 +290,58 @@
} }
return ready; return ready;
}; };
/**
* @description Load details for a specified driver.
* Includes driver type and supported interfaces.
*
* @param {string} driverName - driver name
* @return {void}
*/
ctrl._loadDriverDetails = function(driverName) {
// Re-initialize driver related properties
ctrl.driverType = null;
angular.forEach(driverInterfaces, function(interfaceName) {
ctrl.node[interfaceName + '_interface'] = null;
});
ctrl.driverInterfaceFields = {};
return ironic.getDriverDetails(driverName).then(function(details) {
ctrl.driverType = details.type;
// Extract interface information for dynamic drivers
angular.forEach(driverInterfaces, function(interfaceName) {
var enabled = 'enabled_' + interfaceName + '_interfaces';
if (angular.isDefined(details[enabled]) && details[enabled] !== null) {
var options = [];
angular.forEach(details[enabled], function(value) {
options.push({label: value, value: value});
});
ctrl.driverInterfaceFields[interfaceName] =
new formFieldService.FormField(
{type: 'radio',
id: interfaceName,
title: interfaceName,
options: options,
value: details['default_' + interfaceName + '_interface']});
}
});
});
};
/**
* @description Load a specified driver.
*
* @param {string} driverName - driver name
* @return {promise} Promise that completes
* when both properties and details are loaded.
*/
ctrl.loadDriver = function(driverName) {
var promises = [];
promises.push(ctrl._loadDriverProperties(driverName));
promises.push(ctrl._loadDriverDetails(driverName));
return $q.all(promises);
};
} }
})(); })();

View File

@ -17,7 +17,7 @@
'use strict'; 'use strict';
describe('horizon.dashboard.admin.ironic.base-node', function () { describe('horizon.dashboard.admin.ironic.base-node', function () {
var ironicBackendMockService, uibModalInstance; var ironicBackendMockService, uibModalInstance, driverInterfaces;
var ctrl = {}; var ctrl = {};
beforeEach(module('horizon.dashboard.admin.ironic')); beforeEach(module('horizon.dashboard.admin.ironic'));
@ -51,6 +51,11 @@
controller('BaseNodeController', {ctrl: ctrl}); controller('BaseNodeController', {ctrl: ctrl});
})); }));
beforeEach(inject(function($injector) {
driverInterfaces =
$injector.get('horizon.dashboard.admin.ironic.driverInterfaces');
}));
afterEach(function() { afterEach(function() {
ironicBackendMockService.postTest(); ironicBackendMockService.postTest();
}); });
@ -64,6 +69,7 @@
expect(ctrl.images).toBeNull(); expect(ctrl.images).toBeNull();
expect(ctrl.loadingDriverProperties).toBe(false); expect(ctrl.loadingDriverProperties).toBe(false);
expect(ctrl.driverProperties).toBeNull(); expect(ctrl.driverProperties).toBeNull();
expect(ctrl.driverInterfaceFields).toEqual({});
expect(ctrl.driverPropertyGroups).toBeNull(); expect(ctrl.driverPropertyGroups).toBeNull();
expect(ctrl.modalTitle).toBeDefined(); expect(ctrl.modalTitle).toBeDefined();
angular.forEach(ctrl.propertyCollections, function(collection) { angular.forEach(ctrl.propertyCollections, function(collection) {
@ -74,14 +80,17 @@
.toContain(jasmine.objectContaining({id: "properties"})); .toContain(jasmine.objectContaining({id: "properties"}));
expect(ctrl.propertyCollections) expect(ctrl.propertyCollections)
.toContain(jasmine.objectContaining({id: "extra"})); .toContain(jasmine.objectContaining({id: "extra"}));
expect(ctrl.node).toEqual({ var node = {
name: null, name: null,
driver: null, driver: null,
driver_info: {}, driver_info: {},
properties: {}, properties: {},
extra: {}, extra: {},
network_interface: null, resource_class: null};
resource_class: null}); angular.forEach(driverInterfaces, function(interfaceName) {
node[interfaceName + '_interface'] = null;
});
expect(ctrl.node).toEqual(node);
expect(Object.getOwnPropertyNames(ctrl).sort()).toEqual( expect(Object.getOwnPropertyNames(ctrl).sort()).toEqual(
BASE_NODE_CONTROLLER_PROPERTIES.sort()); BASE_NODE_CONTROLLER_PROPERTIES.sort());
}); });
@ -89,7 +98,7 @@
it('_loadDrivers', function () { it('_loadDrivers', function () {
ctrl._loadDrivers(); ctrl._loadDrivers();
ironicBackendMockService.flush(); ironicBackendMockService.flush();
expect(ctrl.drivers).toEqual(ironicBackendMockService.getDrivers()); expect(ctrl.drivers).toEqual(ironicBackendMockService.getBaseDrivers());
}); });
it('_getImages', function () { it('_getImages', function () {
@ -103,5 +112,24 @@
expect(uibModalInstance.dismiss).toHaveBeenCalledWith('cancel'); expect(uibModalInstance.dismiss).toHaveBeenCalledWith('cancel');
}); });
it('_loadDriverProperties', function() {
var driverName = "ipmi";
ctrl._loadDriverProperties(driverName);
var drivers = ironicBackendMockService.getDrivers();
ironicBackendMockService.flush();
expect(ctrl.node.driver).toEqual(driverName);
expect(ctrl.node.driver).toEqual(drivers[driverName].details.name);
expect(ctrl.node.driver_info).toEqual({});
expect(ctrl.driverPropertyGroups).toBeNonEmptyArray();
});
it('_loadDriverDetails', function() {
var driverName = "ipmi";
ctrl._loadDriverDetails(driverName);
ironicBackendMockService.flush();
var drivers = ironicBackendMockService.getDrivers();
expect(ctrl.driverType).toEqual(drivers[driverName].details.type);
});
}); });
})(); })();

View File

@ -26,6 +26,15 @@
data-target="#driverDetails" data-target="#driverDetails"
data-toggle="tab" data-toggle="tab"
translate>Driver Details</a></li> translate>Driver Details</a></li>
<li ng-if="ctrl.driverType === 'classic' || !ctrl.driverProperties"
class="disabled">
<a data-target="#driverInterfaces"
translation>Driver Interfaces</a></li>
<li ng-if="ctrl.driverType === 'dynamic' && ctrl.driverProperties">
<a href=""
data-target="#driverInterfaces"
data-toggle="tab"
translate>Driver Interfaces</a><li>
</ul> </ul>
<!--base node form--> <!--base node form-->
@ -69,7 +78,7 @@
</div> </div>
</div> </div>
<!--network interface--> <!--network interface-->
<div class="form-group"> <div ng-if="ctrl.driverType === 'classic'" class="form-group">
<label for="network_interface" <label for="network_interface"
class="control-label" class="control-label"
translate> translate>
@ -91,6 +100,29 @@
</div> </div>
</div> </div>
</div> </div>
<!--storage interface-->
<div ng-if="ctrl.driverType === 'classic'" class="form-group">
<label for="storage_interface"
class="control-label"
translate>
Storage Interface
</label>
<span class="help-icon"
data-container="body"
title=""
data-toggle="tooltip"
data-original-title="{$ ::'Interface used for attaching and detaching volumes on this node.' | translate $}">
<span class="fa fa-question-circle"></span>
</span>
<div>
<div class="btn-group">
<label class="btn btn-default"
ng-repeat="opt in ['noop', 'cinder']"
ng-model="ctrl.node.storage_interface"
uib-btn-radio="opt">{$ opt $}</label>
</div>
</div>
</div>
<!--node driver--> <!--node driver-->
<div class="form-group required"> <div class="form-group required">
<label for="driver" <label for="driver"
@ -102,7 +134,7 @@
class="form-control" class="form-control"
ng-options="driver as driver.name for driver in ctrl.drivers" ng-options="driver as driver.name for driver in ctrl.drivers"
ng-model="ctrl.selectedDriver" ng-model="ctrl.selectedDriver"
ng-change="ctrl.loadDriverProperties(ctrl.selectedDriver.name)"> ng-change="ctrl.loadDriver(ctrl.selectedDriver.name)">
<option value="" disabled selected translate>Select a Driver</option> <option value="" disabled selected translate>Select a Driver</option>
</select> </select>
</div> </div>
@ -253,6 +285,20 @@
</div> </div>
</div> </div>
<!--end driver details tab--> <!--end driver details tab-->
<!-- start of driver interfaces tab -->
<div class="tab-pane", id="driverInterfaces">
<p class="text-center"
ng-if="ctrl.loadingDriverProperties">
<small><em><i class="fa fa-spin fa-refresh"></i></em></small>
</p>
<form id="DriverInterfacesForm", name="DriverInterfacesForm">
<div ng-repeat="field in ctrl.driverInterfaceFields"
class="form-group">
<form-field field="field" form="DriverInterfacesForm"></form-field>
</div>
</form>
</div>
</div> </div>
<!--end tabbed content--> <!--end tabbed content-->
</form> </form>

View File

@ -30,6 +30,7 @@
'horizon.framework.widgets.toast.service', 'horizon.framework.widgets.toast.service',
'horizon.app.core.openstack-service-api.ironic', 'horizon.app.core.openstack-service-api.ironic',
'horizon.dashboard.admin.ironic.events', 'horizon.dashboard.admin.ironic.events',
'horizon.dashboard.admin.ironic.driverInterfaces',
'horizon.dashboard.admin.ironic.edit-node.service', 'horizon.dashboard.admin.ironic.edit-node.service',
'horizon.dashboard.admin.ironic.update-patch.service', 'horizon.dashboard.admin.ironic.update-patch.service',
'$log', '$log',
@ -42,6 +43,7 @@
toastService, toastService,
ironic, ironic,
ironicEvents, ironicEvents,
driverInterfaces,
editNodeService, editNodeService,
updatePatchService, updatePatchService,
$log, $log,
@ -67,8 +69,6 @@
ctrl.node[instanceInfoId] = {}; ctrl.node[instanceInfoId] = {};
ctrl.node[instanceInfoId] = {};
init(node); init(node);
function init(node) { function init(node) {
@ -80,11 +80,9 @@
function _loadNodeData(nodeId) { function _loadNodeData(nodeId) {
ironic.getNode(nodeId).then(function(node) { ironic.getNode(nodeId).then(function(node) {
ctrl.baseNode = node; ctrl.baseNode = angular.copy(node);
ctrl.node.name = node.name; ctrl.node.name = node.name;
ctrl.node.resource_class = node.resource_class; ctrl.node.resource_class = node.resource_class;
ctrl.node.network_interface = node.network_interface;
for (var i = 0; i < ctrl.drivers.length; i++) { for (var i = 0; i < ctrl.drivers.length; i++) {
if (ctrl.drivers[i].name === node.driver) { if (ctrl.drivers[i].name === node.driver) {
ctrl.selectedDriver = ctrl.drivers[i]; ctrl.selectedDriver = ctrl.drivers[i];
@ -92,12 +90,23 @@
} }
} }
ctrl.loadDriverProperties(node.driver).then(function() { ctrl.loadDriver(node.driver).then(function() {
angular.forEach(node.driver_info, function(value, property) { angular.forEach(node.driver_info, function(value, property) {
if (angular.isDefined(ctrl.driverProperties[property])) { if (angular.isDefined(ctrl.driverProperties[property])) {
ctrl.driverProperties[property].inputValue = value; ctrl.driverProperties[property].inputValue = value;
} }
}); });
if (ctrl.driverType === 'classic') {
ctrl.node.network_interface = node.network_interface;
ctrl.node.storage_interface = node.storage_interface;
} else {
angular.forEach(
ctrl.driverInterfaceFields,
function(field, interfaceName) {
field.value = ctrl.baseNode[interfaceName + '_interface'];
});
}
}); });
ctrl.node.properties = angular.copy(node.properties); ctrl.node.properties = angular.copy(node.properties);
@ -121,9 +130,9 @@
this.id = id; this.id = id;
this.path = path; this.path = path;
}; };
angular.forEach([new PatchItem("name", "/name"), angular.forEach([new PatchItem("name", "/name"),
new PatchItem("resource_class", "/resource_class"), new PatchItem("resource_class", "/resource_class"),
new PatchItem("network_interface", "/network_interface"),
new PatchItem("driver", "/driver"), new PatchItem("driver", "/driver"),
new PatchItem("properties", "/properties"), new PatchItem("properties", "/properties"),
new PatchItem("extra", "/extra"), new PatchItem("extra", "/extra"),
@ -135,6 +144,15 @@
item.path); item.path);
}); });
angular.forEach(driverInterfaces, function(interfaceName) {
var propName = interfaceName + '_interface';
if (angular.isDefined(sourceNode[propName])) {
patcher.buildPatch(sourceNode[propName],
targetNode[propName],
'/' + propName);
}
});
return patcher.getPatch(); return patcher.getPatch();
}; };
@ -154,6 +172,10 @@
} }
}); });
angular.forEach(ctrl.driverInterfaceFields, function(field, interfaceName) {
ctrl.node[interfaceName + '_interface'] = field.value;
});
$log.info("Updating node " + JSON.stringify(ctrl.baseNode)); $log.info("Updating node " + JSON.stringify(ctrl.baseNode));
$log.info("to " + JSON.stringify(ctrl.node)); $log.info("to " + JSON.stringify(ctrl.node));

View File

@ -78,7 +78,11 @@
}); });
expect(ctrl.node.name).toEqual(editNode.name); expect(ctrl.node.name).toEqual(editNode.name);
expect(ctrl.node.resource_class).toEqual(editNode.resource_class); expect(ctrl.node.resource_class).toEqual(editNode.resource_class);
if (ctrl.driverType === 'classic') {
expect(ctrl.node.network_interface).toEqual(editNode.network_interface); expect(ctrl.node.network_interface).toEqual(editNode.network_interface);
} else {
expect(ctrl.node.network_interface).toBeNull();
}
expect(ctrl.node.properties).toEqual(editNode.properties); expect(ctrl.node.properties).toEqual(editNode.properties);
expect(ctrl.node.extra).toEqual(editNode.extra); expect(ctrl.node.extra).toEqual(editNode.extra);
expect(ctrl.node.instance_info).toEqual(editNode.instance_info); expect(ctrl.node.instance_info).toEqual(editNode.instance_info);

View File

@ -71,6 +71,10 @@
} }
}); });
angular.forEach(ctrl.driverInterfaceFields, function(field, interfaceName) {
ctrl.node[interfaceName + '_interface'] = field.value;
});
ironic.createNode(ctrl.node).then( ironic.createNode(ctrl.node).then(
function(response) { function(response) {
$log.info("create node response = " + JSON.stringify(response)); $log.info("create node response = " + JSON.stringify(response));

View File

@ -30,12 +30,14 @@
ironicBackendMockService.$inject = [ ironicBackendMockService.$inject = [
'$httpBackend', '$httpBackend',
'horizon.framework.util.uuid.service', 'horizon.framework.util.uuid.service',
'horizon.dashboard.admin.ironic.validMacAddressPattern' 'horizon.dashboard.admin.ironic.validMacAddressPattern',
'horizon.dashboard.admin.ironic.driverInterfaces'
]; ];
function ironicBackendMockService($httpBackend, function ironicBackendMockService($httpBackend,
uuidService, uuidService,
validMacAddressPattern) { validMacAddressPattern,
driverInterfaces) {
// Default node object. // Default node object.
var defaultNode = { var defaultNode = {
chassis_uuid: null, chassis_uuid: null,
@ -106,6 +108,38 @@
uuid: undefined uuid: undefined
}; };
var drivers = {
ipmi: {
details: {
default_boot_interface: "pxe",
default_console_interface: "no-console",
default_deploy_interface: "iscsi",
default_inspect_interface: "no-inspect",
default_management_interface: "ipmitool",
default_network_interface: "flat",
default_power_interface: "ipmitool",
default_raid_interface: "no-raid",
default_vendor_interface: "ipmitool",
enabled_boot_interfaces: ["pxe"],
enabled_console_interfaces: ["no-console"],
enabled_deploy_interfaces: ["iscsi", "direct"],
enabled_inspect_interfaces: ["no-inspect"],
enabled_management_interfaces: ["ipmitool"],
enabled_network_interfaces: ["flat", "noop"],
enabled_power_interfaces: ["ipmitool"],
enabled_raid_interfaces: ["no-raid", "agent"],
enabled_vendor_interfaces: ["ipmitool", "no-vendor"],
hosts: ["testhost"],
name: "ipmi",
type: "dynamic"
},
properties: {
deploy_kernel: "UUID (from Glance)",
deploy_ramdisk: "UUID (from Glance)"
}
}
};
// Value of the next available system port // Value of the next available system port
var nextAvailableSystemPort = 1024; var nextAvailableSystemPort = 1024;
@ -114,13 +148,10 @@
// Console info // Console info
consoleType: "shellinabox", consoleType: "shellinabox",
consoleUrl: "http://localhost:", consoleUrl: "http://localhost:",
defaultDriver: "agent_ipmitool", defaultDriver: "ipmi",
supportedBootDevices: ["pxe", "bios", "safe"] supportedBootDevices: ["pxe", "bios", "safe"]
}; };
// List of supported drivers
var drivers = [{name: params.defaultDriver}];
// List of images // List of images
var images = []; var images = [];
@ -142,6 +173,7 @@
getNodeBootDevice: getNodeBootDevice, getNodeBootDevice: getNodeBootDevice,
getNodeSupportedBootDevices: getNodeSupportedBootDevices, getNodeSupportedBootDevices: getNodeSupportedBootDevices,
nodeGetConsoleUrl: nodeGetConsoleUrl, nodeGetConsoleUrl: nodeGetConsoleUrl,
getBaseDrivers: getBaseDrivers,
getDrivers: getDrivers, getDrivers: getDrivers,
getImages: getImages, getImages: getImages,
getPort: getPort, getPort: getPort,
@ -189,8 +221,22 @@
function createNode(params) { function createNode(params) {
var node = null; var node = null;
if (angular.isDefined(params.driver)) { if (angular.isDefined(params.driver) &&
angular.isDefined(drivers[params.driver])) {
node = angular.copy(defaultNode); node = angular.copy(defaultNode);
// For dynamic drivers, initialize interfaces based on
// default values
var details = drivers[params.driver].details;
if (details.type === 'dynamic') {
angular.forEach(driverInterfaces, function(interfaceName) {
var defaultInterface = 'default_' + interfaceName + '_interface';
if (angular.isDefined(details[defaultInterface])) {
node[interfaceName + '_interface'] = details[defaultInterface];
}
});
}
angular.forEach(params, function(value, key) { angular.forEach(params, function(value, key) {
node[key] = value; node[key] = value;
}); });
@ -624,13 +670,28 @@
// Get the currently available drivers // Get the currently available drivers
$httpBackend.whenGET(/\/api\/ironic\/drivers\/$/) $httpBackend.whenGET(/\/api\/ironic\/drivers\/$/)
.respond(responseCode.SUCCESS, {drivers: drivers}); .respond(function() {
return [responseCode.SUCCESS,
{drivers: service.getBaseDrivers()}];
});
// Get driver properties // Get driver properties
$httpBackend.whenGET(/\/api\/ironic\/drivers\/([^\/]+)\/properties$/, $httpBackend.whenGET(/\/api\/ironic\/drivers\/([^\/]+)\/properties$/,
undefined, undefined,
['driverName']) ['driverName'])
.respond(responseCode.SUCCESS, []); .respond(function(method, url, data, headers, params) {
return [responseCode.SUCCESS,
drivers[params.driverName].properties];
});
// Get driver details
$httpBackend.whenGET(/\/api\/ironic\/drivers\/([^\/]+)$/,
undefined,
['driverName'])
.respond(function(method, url, data, headers, params) {
return [responseCode.SUCCESS,
drivers[params.driverName].details];
});
// Get glance images // Get glance images
$httpBackend.whenGET(/\/api\/glance\/images/) $httpBackend.whenGET(/\/api\/glance\/images/)
@ -768,14 +829,29 @@
} // init() } // init()
/** /**
* @description Get the list of supported drivers * @description Get the map of supported drivers
* *
* @return {[]} Array of driver objects * @return {Object} Dictionary of driver objects
*/ */
function getDrivers() { function getDrivers() {
return drivers; return drivers;
} }
/**
* @description Get list of available drivers
*
* @return {[]} List of drivers. Each driver contains name
* and type properties.
*/
function getBaseDrivers() {
var driverList = [];
angular.forEach(drivers, function(driver) {
driverList.push({name: driver.details.name,
type: driver.details.type});
});
return driverList;
}
/** /**
* @description Get the list of images * @description Get the list of images
* *

View File

@ -56,5 +56,17 @@
EDIT_PORT_SUCCESS:'horizon.dashboard.admin.ironic.EDIT_PORT_SUCCESS' EDIT_PORT_SUCCESS:'horizon.dashboard.admin.ironic.EDIT_PORT_SUCCESS'
}; };
$provide.constant('horizon.dashboard.admin.ironic.events', events); $provide.constant('horizon.dashboard.admin.ironic.events', events);
$provide.constant('horizon.dashboard.admin.ironic.driverInterfaces',
['boot',
'console',
'deploy',
'inspect',
'management',
'network',
'power',
'raid',
'storage',
'vendor']);
} }
})(); })();

View File

@ -46,6 +46,7 @@
deleteNode: deleteNode, deleteNode: deleteNode,
deletePort: deletePort, deletePort: deletePort,
getDrivers: getDrivers, getDrivers: getDrivers,
getDriverDetails: getDriverDetails,
getDriverProperties: getDriverProperties, getDriverProperties: getDriverProperties,
getNode: getNode, getNode: getNode,
getNodes: getNodes, getNodes: getNodes,
@ -71,6 +72,19 @@
return service; return service;
/**
* @description Get details of a specified driver
*
* @param {string} driverName - Name of the driver.
* @return {promise} Promise containing driver details.
*/
function getDriverDetails(driverName) {
return apiService.get('/api/ironic/drivers/' + driverName)
.then(function(response) {
return response.data;
});
}
/** /**
* @description Retrieve a list of nodes * @description Retrieve a list of nodes
* http://developer.openstack.org/api-ref/baremetal/? * http://developer.openstack.org/api-ref/baremetal/?

View File

@ -25,6 +25,7 @@
'deletePort', 'deletePort',
'deletePortgroup', 'deletePortgroup',
'getDrivers', 'getDrivers',
'getDriverDetails',
'getDriverProperties', 'getDriverProperties',
'getNode', 'getNode',
'getNodes', 'getNodes',
@ -120,10 +121,31 @@
it('getDrivers', function() { it('getDrivers', function() {
ironicAPI.getDrivers() ironicAPI.getDrivers()
.then(function(drivers) { .then(function(drivers) {
expect(drivers.length).toBeGreaterThan(0); expect(drivers).toEqual(ironicBackendMockService.getBaseDrivers());
angular.forEach(drivers, function(driver) { })
expect(driver.name).toBeDefined(); .catch(failTest);
ironicBackendMockService.flush();
}); });
it('getDriverDetails', function() {
var driver = ironicBackendMockService.params.defaultDriver;
ironicAPI.getDriverDetails(driver)
.then(function(details) {
var drivers = ironicBackendMockService.getDrivers();
expect(details).toEqual(drivers[driver].details);
})
.catch(failTest);
ironicBackendMockService.flush();
});
it('getDriverProperties', function() {
var driver = ironicBackendMockService.params.defaultDriver;
ironicAPI.getDriverProperties(driver)
.then(function(properties) {
var drivers = ironicBackendMockService.getDrivers();
expect(properties).toEqual(drivers[driver].properties);
}) })
.catch(failTest); .catch(failTest);
@ -639,6 +661,7 @@
ironicBackendMockService.flush(); ironicBackendMockService.flush();
}); });
}); });
}); });
})(); })();

View File

@ -83,8 +83,8 @@
ctrl.portgroupDetailsTemplateUrl = path + "portgroup-details.html"; ctrl.portgroupDetailsTemplateUrl = path + "portgroup-details.html";
ctrl.node = null; ctrl.node = null;
ctrl.nodeValidation = []; ctrl.nodeValidation = []; // List of validation results
ctrl.nodeValidationMap = {}; // Indexed by interface ctrl.nodeValidationMap = {}; // Validation results indexed by interface
ctrl.nodeStateTransitions = []; ctrl.nodeStateTransitions = [];
ctrl.nodePowerTransitions = []; ctrl.nodePowerTransitions = [];
ctrl.ports = []; ctrl.ports = [];
@ -155,19 +155,6 @@
}); });
} }
/**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.nodeGetInterface
* @description Retrieve the current underlying interface for specified interface
* type.
*
* @param {string} interfacename - Name of interface, e.g. power, boot, etc.
* @return {string} current name of interface for the requested interface type.
*/
function nodeGetInterface(interfacename) {
return ctrl.node[interfacename + '_interface'] === null ? 'None'
: ctrl.node[interfacename + '_interface'];
}
/** /**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.retrievePorts * @name horizon.dashboard.admin.ironic.NodeDetailsController.retrievePorts
* @description Retrieve the ports associated with the current node, * @description Retrieve the ports associated with the current node,
@ -227,7 +214,7 @@
angular.forEach(response.data, function(interfaceStatus) { angular.forEach(response.data, function(interfaceStatus) {
interfaceStatus.id = interfaceStatus.interface; interfaceStatus.id = interfaceStatus.interface;
ctrl.nodeValidationMap[interfaceStatus.interface] = interfaceStatus; ctrl.nodeValidationMap[interfaceStatus.interface] = interfaceStatus;
interfaceStatus.hw_interface = nodeGetInterface(interfaceStatus.interface); interfaceStatus.hw_interface = ctrl.node[interfaceStatus.interface + '_interface'];
nodeValidation.push(interfaceStatus); nodeValidation.push(interfaceStatus);
}); });
ctrl.nodeValidation = nodeValidation; ctrl.nodeValidation = nodeValidation;

View File

@ -338,7 +338,7 @@
<span ng-switch-default class="fa fa-minus"></span> <span ng-switch-default class="fa fa-minus"></span>
</td> </td>
<td class="rsp-p1"> <td class="rsp-p1">
<span ng-if="item.result"> <span>
{$ item.hw_interface $}</span> {$ item.hw_interface $}</span>
</td> </td>
<td class="rsp-p2"> <td class="rsp-p2">

View File

@ -27,19 +27,23 @@ var BASE_NODE_CONTROLLER_PROPERTIES = [
'cancel', 'cancel',
'collectionCheckPropertyUnique', 'collectionCheckPropertyUnique',
'collectionDeleteProperty', 'collectionDeleteProperty',
'driverInterfaceFields',
'driverProperties', 'driverProperties',
'driverPropertyGroups', 'driverPropertyGroups',
'drivers', 'drivers',
'images', 'images',
'isDriverPropertyActive', 'isDriverPropertyActive',
'loadDriverProperties', 'loadDriver',
'_loadDriverDetails',
'_loadDriverProperties',
'loadingDriverProperties', 'loadingDriverProperties',
'modalTitle', 'modalTitle',
'node', 'node',
'propertyCollections', 'propertyCollections',
'readyToSubmit', 'readyToSubmit',
'submitButtonTitle', 'submitButtonTitle',
'validHostNameRegex']; 'validHostNameRegex',
'driverType'];
/* exported BASE_PORT_CONTROLLER_PROPERTIES */ /* exported BASE_PORT_CONTROLLER_PROPERTIES */

View File

@ -82,9 +82,9 @@
/** /**
* @description Add instructions to the patch for processing a * @description Add instructions to the patch for processing a
* specified item * specified property or collection
* *
* @param {object} item - item to be added * @param {object} item - value of the item to be added
* @param {string} path - Path to the item being added * @param {string} path - Path to the item being added
* @param {string} op - add or remove * @param {string} op - add or remove
* @return {void} * @return {void}
@ -92,9 +92,14 @@
UpdatePatch.prototype._processItem = function(item, path, op) { UpdatePatch.prototype._processItem = function(item, path, op) {
$log.info("UpdatePatch._processItem: " + path + " " + op); $log.info("UpdatePatch._processItem: " + path + " " + op);
if (isProperty(item)) { if (isProperty(item)) {
if (op === 'remove') {
this.patch.push({op: op, path: path});
} else {
this.patch.push({op: op, path: path, value: item}); this.patch.push({op: op, path: path, value: item});
}
} else if (isCollection(item)) { } else if (isCollection(item)) {
angular.forEach(item, function(partName, part) { $log.info("Processing collection " + path);
angular.forEach(item, function(part, partName) {
this._processItem(part, path + "/" + partName, op); this._processItem(part, path + "/" + partName, op);
}); });
} else { } else {

View File

@ -12,14 +12,16 @@
"eslint": "1.10.3", "eslint": "1.10.3",
"eslint-config-openstack": "1.2.4", "eslint-config-openstack": "1.2.4",
"eslint-plugin-angular": "1.0.1", "eslint-plugin-angular": "1.0.1",
"jasmine-core": "2.4.1", "jasmine-core": "^2.4.1",
"karma": "1.1.2", "karma": "^1.1.2",
"karma-chrome-launcher": "1.0.1", "karma-chrome-launcher": "^1.0.1",
"karma-cli": "1.0.1", "karma-cli": "1.0.1",
"karma-coverage": "1.1.1", "karma-coverage": "^1.1.1",
"karma-jasmine": "1.0.2", "karma-jasmine": "^1.0.2",
"karma-ng-html2js-preprocessor": "1.0.0", "karma-jasmine-matchers": "^3.7.0",
"karma-threshold-reporter": "0.1.15" "karma-ng-html2js-preprocessor": "^1.0.0",
"karma-phantomjs-launcher": "^1.0.4",
"karma-threshold-reporter": "^0.1.15"
}, },
"scripts": { "scripts": {
"postinstall": "if [ ! -d .tox ] || [ ! -d .tox/py27 ]; then tox -epy27 --notest; fi", "postinstall": "if [ ! -d .tox ] || [ ! -d .tox/py27 ]; then tox -epy27 --notest; fi",
@ -27,5 +29,7 @@
"lint": "eslint --no-color ironic_ui/static", "lint": "eslint --no-color ironic_ui/static",
"lintq": "eslint --quiet ironic_ui/static" "lintq": "eslint --quiet ironic_ui/static"
}, },
"dependencies": {} "dependencies": {
"string.prototype.endswith": "^0.2.0"
}
} }

View File

@ -0,0 +1,7 @@
---
features:
- |
Adds support for selecting driver interfaces for dynamic drivers
while creating nodes. The support for driver interfaces is not compatible
with classic drivers. This feature is supported with Pike and further
versions of ironic.