Add support for manual cleaning of nodes

- The action list associated with a node in manageable state will have
a "Clean" item.
- When the clean action is initiated the user is prompted with a modal
dialog in which he or she enters or copies a set of cleaning steps
in JSON format.
- Basic validation is performed on the JSON. The user is able to
submit the cleaning request only when validation is successful.
- Cleaning is not currently available as a batch action.

Change-Id: I2af9385e2d9532a9ec46993d65f8c510e419b6c9
Closes-Bug: #1648559
This commit is contained in:
Peter Piela 2017-02-09 11:36:18 -05:00
parent d92d573f54
commit 736c2dab54
8 changed files with 201 additions and 10 deletions

View File

@ -100,17 +100,21 @@ def node_set_power_state(request, node_id, state):
return ironicclient(request).node.set_power_state(node_id, state) return ironicclient(request).node.set_power_state(node_id, state)
def node_set_provision_state(request, node_uuid, state): def node_set_provision_state(request, node_uuid, state, cleansteps=None):
"""Set the target provision state for a given node. """Set the target provision state for a given node.
:param request: HTTP request. :param request: HTTP request.
:param node_uuid: The UUID of the node. :param node_uuid: The UUID of the node.
:param state: the target provision state to set. :param state: the target provision state to set.
:param cleansteps: Optional list of cleaning steps
:return: node. :return: node.
http://docs.openstack.org/developer/python-ironicclient/api/ironicclient.v1.node.html#ironicclient.v1.node.NodeManager.set_provision_state http://docs.openstack.org/developer/python-ironicclient/api/ironicclient.v1.node.html#ironicclient.v1.node.NodeManager.set_provision_state
""" """
return ironicclient(request).node.set_provision_state(node_uuid, state) node_manager = ironicclient(request).node
return node_manager.set_provision_state(node_uuid,
state,
cleansteps=cleansteps)
def node_set_maintenance(request, node_id, state, maint_reason=None): def node_set_maintenance(request, node_id, state, maint_reason=None):

View File

@ -170,7 +170,11 @@ class StatesProvision(generic.View):
:return: Return code :return: Return code
""" """
verb = request.DATA.get('verb') verb = request.DATA.get('verb')
return ironic.node_set_provision_state(request, node_uuid, verb) clean_steps = request.DATA.get('clean_steps')
return ironic.node_set_provision_state(request,
node_uuid,
verb,
clean_steps)
@urls.register @urls.register

View File

@ -0,0 +1,69 @@
/*
* 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';
/**
* @ngdoc controller
* @name horizon.dashboard.admin.ironic:CleanNodeController
* @ngController
*
* @description
* Controller used to prompt the user for a list of clean-steps
* in JSON format that will be applied a node
*/
angular
.module('horizon.dashboard.admin.ironic')
.controller('CleanNodeController', CleanNodeController);
CleanNodeController.$inject = [
'$uibModalInstance'
];
function CleanNodeController($uibModalInstance) {
var ctrl = this;
ctrl.errMsg = '';
ctrl.cancel = function() {
$uibModalInstance.dismiss('cancel');
};
ctrl.clean = function(cleanSteps) {
try {
var steps = JSON.parse(cleanSteps);
if (angular.isArray(steps) && steps.length > 0) {
var valid = true;
angular.forEach(steps, function(step) {
if (angular.isUndefined(step.interface) ||
angular.isUndefined(step.step)) {
valid = false;
}
});
if (valid) {
$uibModalInstance.close(steps);
} else {
ctrl.errMsg = gettext('Each cleaning step must be an object that contains "interface" and "step" properties'); // eslint-disable-line max-len
}
} else {
ctrl.errMsg = gettext('Clean steps should be an non-empty array');
}
} catch (e) {
ctrl.errMsg = gettext('Unable to validate the JSON input');
}
};
}
})();

View File

@ -0,0 +1,41 @@
<div class="modal-header" modal-draggable>
<h3 class="modal-title" translate>Clean Node</h3>
</div>
<div class="modal-body clearfix">
<div class="content">
<div translate class="subtitle">Provide a list of cleaning steps in JSON format</div>
<div class="form-group"
ng-init="cleanSteps=''">
<div class="form-field">
<textarea type="text"
class="form-control input-sm"
ng-model="cleanSteps"
ng-change="ctrl.errMsg=''"
auto-focus
rows="8"
required
placeholder=""/>
</div>
</div>
<div uib-alert
ng-hide="ctrl.errMsg === ''"
ng-class="'alert-danger'">
{$ ctrl.errMsg $}
</div>
</div>
</div>
<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-disabled="cleanSteps === '' || ctrl.errMsg !== ''"
ng-click="ctrl.clean(cleanSteps)"
translate>
Clean node
</button>
</div>

View File

@ -0,0 +1,64 @@
/*
* Copyright 2016 Cray Inc.
* Copyright (c) 2016 Hewlett Packard Enterprise Development Company LP
*
* 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.maintenance.service
* @description Service for putting nodes in, and removing them from
* maintenance mode
*/
angular
.module('horizon.dashboard.admin.ironic')
.factory('horizon.dashboard.admin.ironic.clean-node.service',
cleanNodeService);
cleanNodeService.$inject = [
'$uibModal',
'horizon.dashboard.admin.ironic.basePath',
'horizon.app.core.openstack-service-api.ironic'
];
function cleanNodeService($uibModal, basePath, ironic) {
var service = {
clean: clean
};
return service;
/*
* @description Initiate manual cleaning of an Ironic node.
* The user is prompted for a list of steps that are then
* used to clean the node.
*
* @param {object} node - Node to be cleaned
* @return {void}
*/
function clean(node) {
var options = {
controller: 'CleanNodeController as ctrl',
backdrop: 'static',
templateUrl: basePath + '/clean-node/clean-node.html'
};
$uibModal.open(options).result.then(function(cleanSteps) {
return ironic.setNodeProvisionState(node.uuid,
'clean',
cleanSteps);
});
}
}
})();

View File

@ -244,14 +244,14 @@
* @param {string} uuid UUID of a node. * @param {string} uuid UUID of a node.
* @param {string} verb Provisioning verb used to move node to desired * @param {string} verb Provisioning verb used to move node to desired
* target state * target state
* @param {object []} cleanSteps - List of cleaning steps. Only used
* when the value of verb is 'clean'
* @return {promise} Promise * @return {promise} Promise
*/ */
function setNodeProvisionState(uuid, verb) { function setNodeProvisionState(uuid, verb, cleanSteps) {
var data = {
verb: verb
};
return apiService.put('/api/ironic/nodes/' + uuid + '/states/provision', return apiService.put('/api/ironic/nodes/' + uuid + '/states/provision',
data) {verb: verb,
clean_steps: cleanSteps})
.then(function() { .then(function() {
var msg = gettext( var msg = gettext(
'A request has been made to change the provisioning state of node %s'); 'A request has been made to change the provisioning state of node %s');

View File

@ -30,6 +30,7 @@
'horizon.dashboard.admin.ironic.events', 'horizon.dashboard.admin.ironic.events',
'horizon.framework.widgets.modal.deleteModalService', 'horizon.framework.widgets.modal.deleteModalService',
'horizon.dashboard.admin.ironic.create-port.service', 'horizon.dashboard.admin.ironic.create-port.service',
'horizon.dashboard.admin.ironic.clean-node.service',
'$q', '$q',
'$rootScope' '$rootScope'
]; ];
@ -39,6 +40,7 @@
ironicEvents, ironicEvents,
deleteModalService, deleteModalService,
createPortService, createPortService,
cleanNodeService,
$q, $q,
$rootScope) { $rootScope) {
var service = { var service = {
@ -169,7 +171,11 @@
* the node to the desired target state for the node. * the node to the desired target state for the node.
*/ */
function setProvisionState(args) { function setProvisionState(args) {
ironic.setNodeProvisionState(args.node.uuid, args.verb); if (args.verb === 'clean') {
cleanNodeService.clean(args.node);
} else {
ironic.setNodeProvisionState(args.node.uuid, args.verb);
}
} }
function createPort(node) { function createPort(node) {

View File

@ -56,6 +56,9 @@
states.manageable.addTransition('manageable', states.manageable.addTransition('manageable',
'inspect', 'inspect',
gettext('Inspect')); gettext('Inspect'));
states.manageable.addTransition('manageable',
'clean',
gettext('Clean'));
states.active.addTransition('available', 'deleted'); states.active.addTransition('available', 'deleted');
@ -99,7 +102,7 @@
* @return {void} * @return {void}
*/ */
this.addTransition = function(target, verb, label) { this.addTransition = function(target, verb, label) {
this.transitions[target] = this.transitions[verb] =
{source: this.name, {source: this.name,
target: target, target: target,
verb: verb, verb: verb,