Set current boot device on a node

This would also include collecting the possible boot devices.

Change-Id: I49c47169d5c511f992480c60f64062183064bfc6
Closes-Bug: #1671567
This commit is contained in:
Ramamani Yeleswarapu 2017-06-13 10:58:08 -07:00
parent 6a1f2e2f1b
commit 41488737f6
15 changed files with 733 additions and 31 deletions

View File

@ -1,6 +1,7 @@
# #
# Copyright 2015, 2016 Hewlett Packard Enterprise Development Company LP # Copyright 2015, 2016 Hewlett Packard Enterprise Development Company LP
# Copyright 2016 Cray Inc. # Copyright 2016 Cray Inc.
# Copyright 2017 Intel Corporation
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -229,6 +230,35 @@ def node_get_boot_device(request, node_id):
return ironicclient(request).node.get_boot_device(node_id) return ironicclient(request).node.get_boot_device(node_id)
def node_set_boot_device(request, node_id, device, persistent):
"""Set the boot device for a specified node.
:param request: HTTP request.
:param node_id: The UUID or name of the node.
:param device: boot device.
:param persistent: True or False.
:return: null.
http://docs.openstack.org/developer/python-ironicclient/api/ironicclient.v1.node.html#ironicclient.v1.node.NodeManager.set_boot_device
"""
return ironicclient(request).node.set_boot_device(node_id,
device,
persistent)
def node_get_supported_boot_devices(request, node_id):
"""Get the list of supported boot devices for a specified node.
:param request: HTTP request.
:param node_id: The UUID or name of the node.
:return: List of supported boot devices (strings)
http://docs.openstack.org/developer/python-ironicclient/api/ironicclient.v1.node.html#ironicclient.v1.node.NodeManager.get_boot_device
"""
result = ironicclient(request).node.get_supported_boot_devices(node_id)
return result.get('supported_boot_devices', [])
def driver_list(request): def driver_list(request):
"""Retrieve a list of drivers. """Retrieve a list of drivers.

View File

@ -1,6 +1,7 @@
# #
# Copyright 2015, 2016 Hewlett Packard Enterprise Development Company LP # Copyright 2015, 2016 Hewlett Packard Enterprise Development Company LP
# Copyright 2016 Cray Inc. # Copyright 2016 Cray Inc.
# Copyright 2017 Intel Corporation
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -277,6 +278,37 @@ class BootDevice(generic.View):
""" """
return ironic.node_get_boot_device(request, node_id) return ironic.node_get_boot_device(request, node_id)
@rest_utils.ajax(data_required=True)
def put(self, request, node_id):
"""Set the boot device for a specific node
:param request: HTTP request.
:param node_id: Node name or uuid
:return: null.
"""
return ironic.node_set_boot_device(
request,
node_id,
request.DATA.get('boot_device'),
persistent=request.DATA.get('persistent'))
@urls.register
class SupportedBootDevices(generic.View):
url_regex = r'ironic/nodes/(?P<node_id>{})/boot_device/supported$' . \
format(LOGICAL_NAME_PATTERN)
@rest_utils.ajax()
def get(self, request, node_id):
"""Get the list of supported boot devices for a specified node
:param request: HTTP request.
:param node_id: Node name or uuid
:return: List of supported boot devices
"""
return ironic.node_get_supported_boot_devices(request, node_id)
@urls.register @urls.register
class Drivers(generic.View): class Drivers(generic.View):

View File

@ -0,0 +1,65 @@
/*
* Copyright 2017 Intel Corporation
*
* 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';
/**
* @ngdoc controller
* @name horizon.dashboard.admin.ironic:BootDeviceController
* @ngController
*
* @description
* Controller used to prompt the user for information associated with
* setting the boot device of a node
*/
angular
.module('horizon.dashboard.admin.ironic')
.controller('BootDeviceController', BootDeviceController);
BootDeviceController.$inject = [
'$uibModalInstance',
'horizon.app.core.openstack-service-api.ironic',
'node'
];
function BootDeviceController($uibModalInstance, ironic, node) {
var ctrl = this;
ctrl.modalTitle = gettext("Set Boot Device");
ironic.getSupportedBootDevices(node.uuid).then(
function(bootDevices) {
ctrl.supportedBootDevices = bootDevices;
});
// Initialize form fields to current values
ctrl.bootDevice = null;
ctrl.persistent = 'False';
ironic.getBootDevice(node.uuid).then(function(device) {
ctrl.bootDevice = device.boot_device;
ctrl.persistent = device.persistent ? 'True' : 'False';
});
ctrl.cancel = function() {
$uibModalInstance.dismiss('cancel');
};
ctrl.setSelectedBootDevice = function() {
$uibModalInstance.close({device: ctrl.bootDevice,
persistent: ctrl.persistent === 'True'});
};
}
})();

View File

@ -0,0 +1,97 @@
/*
* 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.BootDeviceController', function () {
var BOOT_DEVICE_CONTROLLER_PROPERTIES = [
'bootDevice',
'cancel',
'modalTitle',
'persistent',
'setSelectedBootDevice',
'supportedBootDevices'
];
var uibModalInstance, ironicBackendMockService, node;
var ctrl = {};
beforeEach(module('horizon.dashboard.admin.ironic'));
beforeEach(module('horizon.framework.util'));
beforeEach(module(function($provide) {
$provide.value('$uibModal', {});
}));
beforeEach(module(function($provide) {
uibModalInstance = {
close: jasmine.createSpy(),
dismiss: jasmine.createSpy()
};
$provide.value('$uibModalInstance', uibModalInstance);
}));
beforeEach(module(function($provide) {
$provide.value('horizon.framework.widgets.toast.service',
{});
}));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(inject(function($injector) {
ironicBackendMockService =
$injector.get('horizon.dashboard.admin.ironic.backend-mock.service');
ironicBackendMockService.init();
var ironicAPI =
$injector.get('horizon.app.core.openstack-service-api.ironic');
ironicAPI.createNode(
{driver: ironicBackendMockService.params.defaultDriver})
.then(function(response) {
node = response.data;
var controller = $injector.get('$controller');
ctrl = controller('BootDeviceController', {node: node});
});
ironicBackendMockService.flush();
}));
it('controller should be defined', function () {
expect(ctrl).toBeDefined();
expect(Object.getOwnPropertyNames(ctrl).sort()).toEqual(
BOOT_DEVICE_CONTROLLER_PROPERTIES.sort());
expect(ctrl.supportedBootDevices).toEqual(
ironicBackendMockService.getNodeSupportedBootDevices(node.uuid));
var bootDevice = ironicBackendMockService.getNodeBootDevice(node.uuid);
expect(ctrl.bootDevice).toEqual(bootDevice.boot_device);
expect(ctrl.persistent).toEqual(bootDevice.persistent ? 'True' : 'False');
});
it('cancel', function () {
ctrl.cancel();
expect(uibModalInstance.dismiss).toHaveBeenCalled();
});
it('setSelectedBootDevice', function () {
ctrl.bootDevice = 'pxe';
ctrl.persistent = 'False';
ctrl.setSelectedBootDevice();
expect(uibModalInstance.close).toHaveBeenCalledWith(
{device: ctrl.bootDevice,
persistent: ctrl.persistent === 'True'});
});
});
})();

View File

@ -0,0 +1,58 @@
<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="SetBootDeviceForm" name="SetBootDeviceForm">
<!--boot device-->
<div class="form-group required">
<label for="bootDevice"
class="control-label"
translate>Boot Device</label>
<span class="hz-icon-required fa fa-asterisk"></span>
<div>
<select id="bootDevice"
class="form-control"
ng-options="device as device for device in ctrl.supportedBootDevices"
ng-model="ctrl.bootDevice">
<option value="" disabled selected translate>Select a boot device</option>
</select>
</div>
</div>
<div class="form-group">
<div>
<label for="persistent"
class="control-label"
translate>Persistent</label>
</div>
<div class="btn-group" id="persistent">
<label class="btn btn-default"
ng-model="ctrl.persistent"
ng-repeat="opt in ['True', 'False']"
uib-btn-radio="opt">{$ opt $}</label>
</div>
</div>
</form>
</div>
<!--modal footer-->
<div class="modal-footer">
<button class="btn btn-default secondary"
type="button"
ng-click="ctrl.cancel()"
translate>
Cancel
</button>
<button class="btn btn-primary"
type="button"
ng-click="ctrl.setSelectedBootDevice()"
ng-disabled="ctrl.bootDevice === null"
translate>
Set Boot Device
</button>
</div>

View File

@ -0,0 +1,68 @@
/*
* Copyright 2017 Intel Corporation
*
* 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';
/*
* @ngdoc service
* @name horizon.dashboard.admin.ironic.bootdevice.service
* @description Service for setting the boot device of a node
*/
angular
.module('horizon.dashboard.admin.ironic')
.factory('horizon.dashboard.admin.ironic.bootdevice.service',
bootDeviceService);
bootDeviceService.$inject = [
'$uibModal',
'horizon.dashboard.admin.ironic.basePath',
'horizon.app.core.openstack-service-api.ironic'
];
function bootDeviceService($uibModal, basePath, ironic) {
var service = {
setBootDevice: setBootDevice
};
return service;
/*
* @description Set the boot device of a specified node
*
* @param {object} node - node object
* @return {promise}
*/
function setBootDevice(node) {
var promise;
var options = {
controller: "BootDeviceController as ctrl",
backdrop: 'static',
resolve: {
node: function() {
return node;
}
},
templateUrl: basePath + '/bootdevice/bootdevice.html'
};
promise = $uibModal.open(options).result.then(
function(result) {
return ironic.nodeSetBootDevice(node.uuid,
result.device,
result.persistent);
});
return promise;
}
}
})();

View File

@ -0,0 +1,142 @@
/**
* 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";
/**
* @description Unit tests for the Ironic-UI boot-device service
*/
describe('horizon.dashboard.admin.ironic.bootdevice.service',
function() {
var $q,
$uibModal,
bootDeviceService,
ironicAPI,
ironicBackendMockService,
defaultDriver;
beforeEach(module('horizon.dashboard.admin.ironic'));
beforeEach(module('horizon.framework.util'));
beforeEach(module(function($provide) {
$provide.value('$uibModal', {
open: function() {
return $q.when({device: 'pxe',
persistent: true});
}
});
}));
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();
defaultDriver = ironicBackendMockService.params.defaultDriver;
}));
beforeEach(inject(function($injector) {
$q = $injector.get('$q');
$uibModal = $injector.get('$uibModal');
ironicAPI =
$injector.get('horizon.app.core.openstack-service-api.ironic');
bootDeviceService =
$injector.get('horizon.dashboard.admin.ironic.bootdevice.service');
}));
it('defines the bootDeviceService', function() {
expect(bootDeviceService).toBeDefined();
expect(bootDeviceService.setBootDevice).toBeDefined();
});
afterEach(function() {
ironicBackendMockService.postTest();
});
/**
* @description Utility function that creates a node and returns
* both it and its boot device
*
* @return {promise} Containing node and boot_device
*/
function createNode() {
return ironicAPI.createNode({driver: defaultDriver})
.then(function(response) {
return response.data;
})
.then(function(node) {
return ironicAPI.getBootDevice(node.uuid).then(function(device) {
return {node: node, boot_device: device};
});
});
}
it('setBootDevice', function() {
var targetBootDevice = {
device: "safe",
persistent: false
};
spyOn($uibModal, 'open').and.returnValue(
{result: $q.when(targetBootDevice)});
createNode().then(function(data) {
expect(data.node.boot_device).not.toEqual(targetBootDevice.device);
bootDeviceService.setBootDevice(data.node)
.then(function() {
ironicAPI.getBootDevice(data.node.uuid).then(function(device) {
expect(device).toEqual(
{boot_device: targetBootDevice.device,
persistent: targetBootDevice.persistent});
});
})
.catch(fail);
});
ironicBackendMockService.flush();
});
it('setBootDevice - cancel', function() {
spyOn($uibModal, 'open').and.returnValue(
{result: $q.reject('cancel')});
createNode().then(function(data) {
bootDeviceService.setBootDevice(data.node)
.then(fail)
.catch(function() {
ironicAPI.getBootDevice(data.node.uuid).then(function(device) {
expect(device).toEqual(data.boot_device);
});
});
});
ironicBackendMockService.flush();
});
});
})();

View File

@ -103,12 +103,11 @@
// Additional service parameters // Additional service parameters
var params = { var params = {
// Currently, all nodes have the same boot device.
bootDevice: {boot_device: 'pxe', persistent: true},
// Console info // Console info
consoleType: "shellinabox", consoleType: "shellinabox",
consoleUrl: "http://localhost:", consoleUrl: "http://localhost:",
defaultDriver: "agent_ipmitool" defaultDriver: "agent_ipmitool",
supportedBootDevices: ["pxe", "bios", "safe"]
}; };
// List of supported drivers // List of supported drivers
@ -123,6 +122,8 @@
flush: flush, flush: flush,
postTest: postTest, postTest: postTest,
getNode: getNode, getNode: getNode,
getNodeBootDevice: getNodeBootDevice,
getNodeSupportedBootDevices: getNodeSupportedBootDevices,
nodeGetConsoleUrl: nodeGetConsoleUrl, nodeGetConsoleUrl: nodeGetConsoleUrl,
getDrivers: getDrivers, getDrivers: getDrivers,
getImages: getImages, getImages: getImages,
@ -183,7 +184,12 @@
base: node, base: node,
consolePort: getNextAvailableSystemPort(), consolePort: getNextAvailableSystemPort(),
ports: {}, // Indexed by port-uuid ports: {}, // Indexed by port-uuid
portgroups: {} // Indexed by portgroup-uuid portgroups: {}, // Indexed by portgroup-uuid
supportedBootDevices: service.params.supportedBootDevices,
bootDevice: {
boot_device: service.params.supportedBootDevices[0],
persistent: true
}
}; };
nodes[node.uuid] = backendNode; nodes[node.uuid] = backendNode;
@ -196,7 +202,7 @@
} }
/** /**
* description Get a specified node. * @description Get a specified node.
* *
* @param {string} nodeId - Uuid or name of the requested node. * @param {string} nodeId - Uuid or name of the requested node.
* @return {object|null} Base node object, or null if the node * @return {object|null} Base node object, or null if the node
@ -207,6 +213,29 @@
} }
/** /**
* @description Get the boot device of a specified node.
*
* @param {string} nodeId - Uuid or name of the requested node.
* @return {object} Boot device.
*/
function getNodeBootDevice(nodeId) {
return angular.isDefined(nodes[nodeId])
? nodes[nodeId].bootDevice : undefined;
}
/**
* @description Get the list of supported boot devices of
* a specified node.
*
* @param {string} nodeId - Uuid or name of the requested node.
* @return {string []} List of supported boot devices.
*/
function getNodeSupportedBootDevices(nodeId) {
return angular.isDefined(nodes[nodeId])
? nodes[nodeId].supportedBootDevices : undefined;
}
/*
* @description Get the console-url for a specified node. * @description Get the console-url for a specified node.
* *
* @param {string} nodeId - Uuid or name of the node. * @param {string} nodeId - Uuid or name of the node.
@ -493,7 +522,47 @@
$httpBackend.whenGET(/\/api\/ironic\/nodes\/([^\/]+)\/boot_device$/, $httpBackend.whenGET(/\/api\/ironic\/nodes\/([^\/]+)\/boot_device$/,
undefined, undefined,
['nodeId']) ['nodeId'])
.respond(responseCode.SUCCESS, service.params.bootDevice); .respond(function(method, url, data, headers, params) {
if (angular.isDefined(nodes[params.nodeId])) {
return [200, nodes[params.nodeId].bootDevice];
} else {
return [400, null];
}
});
// Get supported boot devices
$httpBackend.whenGET(
/\/api\/ironic\/nodes\/([^\/]+)\/boot_device\/supported$/,
undefined,
['nodeId'])
.respond(function(method, url, data, headers, params) {
if (angular.isDefined(nodes[params.nodeId])) {
return [200, nodes[params.nodeId].supportedBootDevices];
} else {
return [400, null];
}
});
// Set boot device
$httpBackend.whenPUT(/\/api\/ironic\/nodes\/(.+)\/boot_device/,
undefined,
undefined,
['nodeId'])
.respond(function(method, url, data, headers, params) {
data = JSON.parse(data);
var status = 404;
if (angular.isDefined(nodes[params.nodeId])) {
var node = nodes[params.nodeId];
if (node.supportedBootDevices.indexOf(data.boot_device) !== -1) {
node.bootDevice.boot_device = data.boot_device;
if (angular.isDefined(data.persistent)) {
node.bootDevice.persistent = data.persistent;
}
status = 200;
}
}
return [status, null];
});
// Validate the interfaces associated with a specified node // Validate the interfaces associated with a specified node
$httpBackend.whenGET(/\/api\/ironic\/nodes\/([^\/]+)\/validate$/, $httpBackend.whenGET(/\/api\/ironic\/nodes\/([^\/]+)\/validate$/,

View File

@ -1,6 +1,7 @@
/* /*
* © Copyright 2015,2016 Hewlett Packard Enterprise Development Company LP * © Copyright 2015,2016 Hewlett Packard Enterprise Development Company LP
* © Copyright 2016 Cray Inc. * © Copyright 2016 Cray Inc.
* Copyright 2017 Intel Corporation
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -50,9 +51,11 @@
getNodes: getNodes, getNodes: getNodes,
getPortsWithNode: getPortsWithNode, getPortsWithNode: getPortsWithNode,
getBootDevice: getBootDevice, getBootDevice: getBootDevice,
getSupportedBootDevices: getSupportedBootDevices,
nodeGetConsole: nodeGetConsole, nodeGetConsole: nodeGetConsole,
nodeSetConsoleMode: nodeSetConsoleMode, nodeSetConsoleMode: nodeSetConsoleMode,
nodeSetMaintenance: nodeSetMaintenance, nodeSetMaintenance: nodeSetMaintenance,
nodeSetBootDevice: nodeSetBootDevice,
nodeSetPowerState: nodeSetPowerState, nodeSetPowerState: nodeSetPowerState,
setNodeProvisionState: setNodeProvisionState, setNodeProvisionState: setNodeProvisionState,
updateNode: updateNode, updateNode: updateNode,
@ -119,11 +122,11 @@
* @description Retrieve the boot device for a node * @description Retrieve the boot device for a node
* https://developer.openstack.org/api-ref/baremetal/#get-boot-device * https://developer.openstack.org/api-ref/baremetal/#get-boot-device
* *
* @param {string} uuid UUID or logical name of a node. * @param {string} nodeId UUID or logical name of a node.
* @return {promise} Dictionary describing the current boot device * @return {promise} Dictionary describing the current boot device
*/ */
function getBootDevice(uuid) { function getBootDevice(nodeId) {
return apiService.get('/api/ironic/nodes/' + uuid + '/boot_device') return apiService.get('/api/ironic/nodes/' + nodeId + '/boot_device')
.then(function(response) { .then(function(response) {
return response.data; return response.data;
}) })
@ -137,6 +140,30 @@
}); });
} }
/**
* @description Retrieve the supported boot devices for a node
* https://developer.openstack.org/api-ref/baremetal/#get-supported-boot-devices
*
* @param {string} nodeId UUID or logical name of a node.
* @return {promise} List of supported boot devices
*/
function getSupportedBootDevices(nodeId) {
return apiService.get('/api/ironic/nodes/' + nodeId +
'/boot_device/supported')
.then(function(response) {
return response.data; // List of supported boot devices
})
.catch(function(response) {
var msg = interpolate(
gettext(
'Unable to retrieve supported boot devices for Ironic node. %s'),
[response.data],
false);
toastService.add('error', msg);
return $q.reject(msg);
});
}
/** /**
* @description Retrieve a list of ports associated with a node. * @description Retrieve a list of ports associated with a node.
* *
@ -200,6 +227,33 @@
}); });
} }
/**
* @description Set the boot device of a node
*
* http://developer.openstack.org/api-ref/baremetal/#set-boot-device
*
* @param {string} nodeId UUID or logical name of a node.
* @param {string} bootDevice - Selected boot device.
* @param {Boolean} persistent - True or False.
* @return {promise} Promise
*/
function nodeSetBootDevice(nodeId, bootDevice, persistent) {
return apiService.put('/api/ironic/nodes/' + nodeId + '/boot_device',
{boot_device: bootDevice,
persistent: persistent})
.then(function() {
toastService.add('success',
gettext('Refresh page to see set boot device'));
})
.catch(function(response) {
var msg = interpolate(gettext('Unable to set boot device: %s'),
[response.data],
false);
toastService.add('error', msg);
return $q.reject(msg);
});
}
/** /**
* @description Set the power state of the node. * @description Set the power state of the node.
* *

View File

@ -31,7 +31,9 @@
'getPortgroups', 'getPortgroups',
'getPortsWithNode', 'getPortsWithNode',
'getBootDevice', 'getBootDevice',
'getSupportedBootDevices',
'nodeGetConsole', 'nodeGetConsole',
'nodeSetBootDevice',
'nodeSetConsoleMode', 'nodeSetConsoleMode',
'nodeSetPowerState', 'nodeSetPowerState',
'nodeSetMaintenance', 'nodeSetMaintenance',
@ -296,27 +298,90 @@
.then(function(node) { .then(function(node) {
expect(node.console_enabled).toEqual(false); expect(node.console_enabled).toEqual(false);
return node; return node;
}); })
.catch(failTest);
ironicBackendMockService.flush(); ironicBackendMockService.flush();
}); });
it('getBootDevice', function() { it('getBootDevice', function() {
createNode({driver: defaultDriver}) createNode({driver: defaultDriver})
.then(function(node) {
expect(node.console_enabled).toEqual(false);
return node;
})
.then(function(node) { .then(function(node) {
return ironicAPI.getBootDevice(node.uuid) return ironicAPI.getBootDevice(node.uuid)
.then(function(bootDevice) { .then(function(bootDevice) {
return bootDevice; return {node: node, bootDevice: bootDevice};
}); });
}) })
.then(function(bootDevice) { .then(function(data) {
expect(bootDevice).toEqual( expect(data.bootDevice).toEqual(
ironicBackendMockService.params.bootDevice); ironicBackendMockService.getNodeBootDevice(data.node.uuid));
}); })
.catch(failTest);
ironicBackendMockService.flush();
});
it('getSupportedBootDevices', function() {
createNode({driver: defaultDriver})
.then(function(node) {
return ironicAPI.getSupportedBootDevices(node.uuid);
})
.then(function(bootDevices) {
expect(bootDevices).toEqual(
ironicBackendMockService.params.supportedBootDevices);
})
.catch(failTest);
ironicBackendMockService.flush();
});
it('nodeSetBootDevice', function() {
var bootDevice = {
boot_device: "bios",
persistent: false
};
createNode({driver: defaultDriver})
.then(function(node) {
return ironicAPI.nodeSetBootDevice(node.uuid,
bootDevice.boot_device,
bootDevice.persistent)
.then(function() {
return node;
});
})
.then(function(node) {
ironicAPI.getBootDevice(node.uuid).then(function(device) {
expect(device).toEqual(bootDevice);
});
})
.catch(failTest);
ironicBackendMockService.flush();
});
it('nodeSetBootDevice - bad device', function() {
createNode({driver: defaultDriver})
.then(function(node) {
return ironicAPI.getBootDevice(node.uuid)
.then(function(device) {
return {node: node, currentBootDevice: device};
});
})
.then(function(data) {
ironicAPI.nodeSetBootDevice(data.node.uuid,
"bad-device",
false)
.then(failTest)
.catch(function() {
// Ensure the boot device is unchanged
ironicAPI.getBootDevice(data.node.uuid)
.then(function(device) {
expect(device).toEqual(data.currentBootDevice);
});
});
})
.catch(failTest);
ironicBackendMockService.flush(); ironicBackendMockService.flush();
}); });

View File

@ -33,6 +33,7 @@
'horizon.dashboard.admin.ironic.create-port.service', 'horizon.dashboard.admin.ironic.create-port.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.node-state-transition.service', 'horizon.dashboard.admin.ironic.node-state-transition.service',
'horizon.dashboard.admin.ironic.validUuidPattern' 'horizon.dashboard.admin.ironic.validUuidPattern'
]; ];
@ -47,6 +48,7 @@
createPortService, createPortService,
editPortService, editPortService,
maintenanceService, maintenanceService,
bootDeviceService,
nodeStateTransitionService, nodeStateTransitionService,
validUuidPattern) { validUuidPattern) {
var ctrl = this; var ctrl = this;
@ -57,6 +59,7 @@
ctrl.actions = actions; ctrl.actions = actions;
ctrl.maintenanceService = maintenanceService; ctrl.maintenanceService = maintenanceService;
ctrl.bootDeviceService = bootDeviceService;
ctrl.sections = [ ctrl.sections = [
{ {

View File

@ -37,6 +37,14 @@
"Maintenance off" : "Maintenance on" | translate $}</span> "Maintenance off" : "Maintenance on" | translate $}</span>
</a> </a>
</li> </li>
<li role="presentation">
<a role="menuitem"
ng-click="ctrl.bootDeviceService.setBootDevice(ctrl.node);
$event.stopPropagation();
$event.preventDefault()">
<span>{$ "Set boot device" | translate $}</span>
</a>
</li>
<li role="presentation" <li role="presentation"
ng-repeat="transition in ctrl.nodeStateTransitions"> ng-repeat="transition in ctrl.nodeStateTransitions">
<a role="menuitem" <a role="menuitem"

View File

@ -382,18 +382,6 @@
</dl> </dl>
</div> </div>
</div> </div>
<!-- Boot Device -->
<div class="col-md-6 status detail">
<h4 translate>Boot Device</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
<dt translate>Device</dt>
<dd>{$ ctrl.node.bootDevice.boot_device | noValue $}</dd>
<dt translate>Persistent</dt>
<dd>{$ ctrl.node.bootDevice.persistent | noValue $}</dd>
</dl>
</div>
</div> </div>
<div class="row"> <div class="row">
@ -407,4 +395,16 @@
<dd ng-repeat-end>{$ propertyValue | noValue $}</dd> <dd ng-repeat-end>{$ propertyValue | noValue $}</dd>
</dl> </dl>
</div> </div>
<!-- Boot Device -->
<div class="col-md-6 status detail">
<h4 translate>Boot Device</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
<dt translate>Device</dt>
<dd>{$ ctrl.node.bootDevice.boot_device | noValue $}</dd>
<dt translate>Persistent</dt>
<dd>{$ ctrl.node.bootDevice.persistent | noValue $}</dd>
</dl>
</div>
</div> </div>

View File

@ -27,6 +27,7 @@
'horizon.app.core.openstack-service-api.ironic', 'horizon.app.core.openstack-service-api.ironic',
'horizon.dashboard.admin.ironic.actions', 'horizon.dashboard.admin.ironic.actions',
'horizon.dashboard.admin.ironic.maintenance.service', 'horizon.dashboard.admin.ironic.maintenance.service',
'horizon.dashboard.admin.ironic.bootdevice.service',
'horizon.dashboard.admin.ironic.enroll-node.service', 'horizon.dashboard.admin.ironic.enroll-node.service',
'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',
@ -38,6 +39,7 @@
ironic, ironic,
actions, actions,
maintenanceService, maintenanceService,
bootDeviceService,
enrollNodeService, enrollNodeService,
editNodeService, editNodeService,
createPortService, createPortService,
@ -48,6 +50,7 @@
ctrl.nodesSrc = []; ctrl.nodesSrc = [];
ctrl.actions = actions; ctrl.actions = actions;
ctrl.maintenanceService = maintenanceService; ctrl.maintenanceService = maintenanceService;
ctrl.bootDeviceService = bootDeviceService;
ctrl.enrollNode = enrollNode; ctrl.enrollNode = enrollNode;
ctrl.editNode = editNode; ctrl.editNode = editNode;

View File

@ -163,6 +163,14 @@
"Maintenance off" : "Maintenance on" | translate $}</span> "Maintenance off" : "Maintenance on" | translate $}</span>
</a> </a>
</li> </li>
<li role="presentation">
<a role="menuitem"
ng-click="table.bootDeviceService.setBootDevice(node);
$event.stopPropagation();
$event.preventDefault()">
<span>{$ "Set boot device" | translate $}</span>
</a>
</li>
<li role="presentation" <li role="presentation"
ng-class="{disabled: !( ng-class="{disabled: !(
node.provision_state === 'available' || node.provision_state === 'available' ||