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
DEFAULT_IRONIC_API_VERSION = '1.31'
DEFAULT_IRONIC_API_VERSION = '1.34'
DEFAULT_INSECURE = False
DEFAULT_CACERT = None
IRONIC_CLIENT_CLASS_NAME = 'baremetal'
@ -282,6 +282,19 @@ def driver_properties(request, 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):
"""Create network port

View File

@ -450,3 +450,20 @@ class RaidConfig(generic.View):
request,
node_id,
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.
*/
'../test-shim.js',
'../node_modules/string.prototype.endswith/*.js',
// from jasmine.html
toxPath + 'xstatic/pkg/jquery/data/jquery.js',
@ -113,9 +114,9 @@
autoWatch: true,
frameworks: ['jasmine'],
frameworks: ['jasmine', 'jasmine-matchers'],
browsers: ['Chrome'],
browsers: ['Chrome', 'PhantomJS'],
browserNoActivityTimeout: 60000,
@ -129,7 +130,9 @@
plugins: [
'karma-chrome-launcher',
'karma-phantomjs-launcher',
'karma-jasmine',
'karma-jasmine-matchers',
'karma-ng-html2js-preprocessor',
'karma-coverage',
'karma-threshold-reporter'

View File

@ -27,31 +27,41 @@
'$uibModalInstance',
'horizon.app.core.openstack-service-api.ironic',
'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.driver-property.service',
'horizon.dashboard.admin.ironic.graph.service',
'horizon.dashboard.admin.ironic.validHostNamePattern',
'horizon.dashboard.admin.ironic.driverInterfaces',
'$log',
'$q',
'ctrl'
];
function BaseNodeController($uibModalInstance,
ironic,
glance,
formFieldService,
baseNodeService,
driverPropertyService,
graphService,
validHostNamePattern,
driverInterfaces,
$log,
$q,
ctrl) {
ctrl.validHostNameRegex = new RegExp(validHostNamePattern);
ctrl.drivers = null;
ctrl.images = null;
ctrl.loadingDriverProperties = false;
ctrl.driverType = null;
// Object containing the set of properties associated with the currently
// selected driver
ctrl.driverProperties = 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.submitButtonTitle = gettext("Submit");
@ -91,10 +101,14 @@
name: null,
driver: null,
driver_info: {},
network_interface: null,
resource_class: null
};
// Initialize hardware interfaces
angular.forEach(driverInterfaces, function(interfaceName) {
ctrl.node[interfaceName + '_interface'] = null;
});
angular.forEach(ctrl.propertyCollections, function(collection) {
ctrl.node[collection.id] = {};
});
@ -189,8 +203,8 @@
* @param {string} driverName - Name of driver
* @return {void}
*/
ctrl.loadDriverProperties = function(driverName) {
ctrl.node.driver = driverName;
ctrl._loadDriverProperties = function(driverName) {
ctrl.node.driver = null;
ctrl.node.driver_info = {};
ctrl.loadingDriverProperties = true;
@ -198,6 +212,7 @@
ctrl.driverPropertyGroups = null;
return ironic.getDriverProperties(driverName).then(function(properties) {
ctrl.node.driver = driverName;
ctrl.driverProperties = {};
angular.forEach(properties, function(desc, property) {
ctrl.driverProperties[property] =
@ -275,5 +290,58 @@
}
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';
describe('horizon.dashboard.admin.ironic.base-node', function () {
var ironicBackendMockService, uibModalInstance;
var ironicBackendMockService, uibModalInstance, driverInterfaces;
var ctrl = {};
beforeEach(module('horizon.dashboard.admin.ironic'));
@ -51,6 +51,11 @@
controller('BaseNodeController', {ctrl: ctrl});
}));
beforeEach(inject(function($injector) {
driverInterfaces =
$injector.get('horizon.dashboard.admin.ironic.driverInterfaces');
}));
afterEach(function() {
ironicBackendMockService.postTest();
});
@ -64,6 +69,7 @@
expect(ctrl.images).toBeNull();
expect(ctrl.loadingDriverProperties).toBe(false);
expect(ctrl.driverProperties).toBeNull();
expect(ctrl.driverInterfaceFields).toEqual({});
expect(ctrl.driverPropertyGroups).toBeNull();
expect(ctrl.modalTitle).toBeDefined();
angular.forEach(ctrl.propertyCollections, function(collection) {
@ -74,14 +80,17 @@
.toContain(jasmine.objectContaining({id: "properties"}));
expect(ctrl.propertyCollections)
.toContain(jasmine.objectContaining({id: "extra"}));
expect(ctrl.node).toEqual({
var node = {
name: null,
driver: null,
driver_info: {},
properties: {},
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(
BASE_NODE_CONTROLLER_PROPERTIES.sort());
});
@ -89,7 +98,7 @@
it('_loadDrivers', function () {
ctrl._loadDrivers();
ironicBackendMockService.flush();
expect(ctrl.drivers).toEqual(ironicBackendMockService.getDrivers());
expect(ctrl.drivers).toEqual(ironicBackendMockService.getBaseDrivers());
});
it('_getImages', function () {
@ -103,5 +112,24 @@
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-toggle="tab"
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>
<!--base node form-->
@ -69,7 +78,7 @@
</div>
</div>
<!--network interface-->
<div class="form-group">
<div ng-if="ctrl.driverType === 'classic'" class="form-group">
<label for="network_interface"
class="control-label"
translate>
@ -91,6 +100,29 @@
</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-->
<div class="form-group required">
<label for="driver"
@ -102,7 +134,7 @@
class="form-control"
ng-options="driver as driver.name for driver in ctrl.drivers"
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>
</select>
</div>
@ -253,6 +285,20 @@
</div>
</div>
<!--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>
<!--end tabbed content-->
</form>

View File

@ -30,6 +30,7 @@
'horizon.framework.widgets.toast.service',
'horizon.app.core.openstack-service-api.ironic',
'horizon.dashboard.admin.ironic.events',
'horizon.dashboard.admin.ironic.driverInterfaces',
'horizon.dashboard.admin.ironic.edit-node.service',
'horizon.dashboard.admin.ironic.update-patch.service',
'$log',
@ -42,6 +43,7 @@
toastService,
ironic,
ironicEvents,
driverInterfaces,
editNodeService,
updatePatchService,
$log,
@ -67,8 +69,6 @@
ctrl.node[instanceInfoId] = {};
ctrl.node[instanceInfoId] = {};
init(node);
function init(node) {
@ -80,11 +80,9 @@
function _loadNodeData(nodeId) {
ironic.getNode(nodeId).then(function(node) {
ctrl.baseNode = node;
ctrl.baseNode = angular.copy(node);
ctrl.node.name = node.name;
ctrl.node.resource_class = node.resource_class;
ctrl.node.network_interface = node.network_interface;
for (var i = 0; i < ctrl.drivers.length; i++) {
if (ctrl.drivers[i].name === node.driver) {
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) {
if (angular.isDefined(ctrl.driverProperties[property])) {
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);
@ -121,9 +130,9 @@
this.id = id;
this.path = path;
};
angular.forEach([new PatchItem("name", "/name"),
new PatchItem("resource_class", "/resource_class"),
new PatchItem("network_interface", "/network_interface"),
new PatchItem("driver", "/driver"),
new PatchItem("properties", "/properties"),
new PatchItem("extra", "/extra"),
@ -135,6 +144,15 @@
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();
};
@ -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("to " + JSON.stringify(ctrl.node));

View File

@ -78,7 +78,11 @@
});
expect(ctrl.node.name).toEqual(editNode.name);
expect(ctrl.node.resource_class).toEqual(editNode.resource_class);
if (ctrl.driverType === 'classic') {
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.extra).toEqual(editNode.extra);
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(
function(response) {
$log.info("create node response = " + JSON.stringify(response));

View File

@ -30,12 +30,14 @@
ironicBackendMockService.$inject = [
'$httpBackend',
'horizon.framework.util.uuid.service',
'horizon.dashboard.admin.ironic.validMacAddressPattern'
'horizon.dashboard.admin.ironic.validMacAddressPattern',
'horizon.dashboard.admin.ironic.driverInterfaces'
];
function ironicBackendMockService($httpBackend,
uuidService,
validMacAddressPattern) {
validMacAddressPattern,
driverInterfaces) {
// Default node object.
var defaultNode = {
chassis_uuid: null,
@ -106,6 +108,38 @@
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
var nextAvailableSystemPort = 1024;
@ -114,13 +148,10 @@
// Console info
consoleType: "shellinabox",
consoleUrl: "http://localhost:",
defaultDriver: "agent_ipmitool",
defaultDriver: "ipmi",
supportedBootDevices: ["pxe", "bios", "safe"]
};
// List of supported drivers
var drivers = [{name: params.defaultDriver}];
// List of images
var images = [];
@ -142,6 +173,7 @@
getNodeBootDevice: getNodeBootDevice,
getNodeSupportedBootDevices: getNodeSupportedBootDevices,
nodeGetConsoleUrl: nodeGetConsoleUrl,
getBaseDrivers: getBaseDrivers,
getDrivers: getDrivers,
getImages: getImages,
getPort: getPort,
@ -189,8 +221,22 @@
function createNode(params) {
var node = null;
if (angular.isDefined(params.driver)) {
if (angular.isDefined(params.driver) &&
angular.isDefined(drivers[params.driver])) {
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) {
node[key] = value;
});
@ -624,13 +670,28 @@
// Get the currently available drivers
$httpBackend.whenGET(/\/api\/ironic\/drivers\/$/)
.respond(responseCode.SUCCESS, {drivers: drivers});
.respond(function() {
return [responseCode.SUCCESS,
{drivers: service.getBaseDrivers()}];
});
// Get driver properties
$httpBackend.whenGET(/\/api\/ironic\/drivers\/([^\/]+)\/properties$/,
undefined,
['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
$httpBackend.whenGET(/\/api\/glance\/images/)
@ -768,14 +829,29 @@
} // 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() {
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
*

View File

@ -56,5 +56,17 @@
EDIT_PORT_SUCCESS:'horizon.dashboard.admin.ironic.EDIT_PORT_SUCCESS'
};
$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,
deletePort: deletePort,
getDrivers: getDrivers,
getDriverDetails: getDriverDetails,
getDriverProperties: getDriverProperties,
getNode: getNode,
getNodes: getNodes,
@ -71,6 +72,19 @@
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
* http://developer.openstack.org/api-ref/baremetal/?

View File

@ -25,6 +25,7 @@
'deletePort',
'deletePortgroup',
'getDrivers',
'getDriverDetails',
'getDriverProperties',
'getNode',
'getNodes',
@ -120,10 +121,31 @@
it('getDrivers', function() {
ironicAPI.getDrivers()
.then(function(drivers) {
expect(drivers.length).toBeGreaterThan(0);
angular.forEach(drivers, function(driver) {
expect(driver.name).toBeDefined();
expect(drivers).toEqual(ironicBackendMockService.getBaseDrivers());
})
.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);
@ -639,6 +661,7 @@
ironicBackendMockService.flush();
});
});
});
})();

View File

@ -83,8 +83,8 @@
ctrl.portgroupDetailsTemplateUrl = path + "portgroup-details.html";
ctrl.node = null;
ctrl.nodeValidation = [];
ctrl.nodeValidationMap = {}; // Indexed by interface
ctrl.nodeValidation = []; // List of validation results
ctrl.nodeValidationMap = {}; // Validation results indexed by interface
ctrl.nodeStateTransitions = [];
ctrl.nodePowerTransitions = [];
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
* @description Retrieve the ports associated with the current node,
@ -227,7 +214,7 @@
angular.forEach(response.data, function(interfaceStatus) {
interfaceStatus.id = interfaceStatus.interface;
ctrl.nodeValidationMap[interfaceStatus.interface] = interfaceStatus;
interfaceStatus.hw_interface = nodeGetInterface(interfaceStatus.interface);
interfaceStatus.hw_interface = ctrl.node[interfaceStatus.interface + '_interface'];
nodeValidation.push(interfaceStatus);
});
ctrl.nodeValidation = nodeValidation;

View File

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

View File

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

View File

@ -82,9 +82,9 @@
/**
* @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} op - add or remove
* @return {void}
@ -92,9 +92,14 @@
UpdatePatch.prototype._processItem = function(item, path, op) {
$log.info("UpdatePatch._processItem: " + path + " " + op);
if (isProperty(item)) {
if (op === 'remove') {
this.patch.push({op: op, path: path});
} else {
this.patch.push({op: op, path: path, value: 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);
});
} else {

View File

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