Updated to ironic API v1.11 and added manageable state

Newly registered nodes begin in the enroll provision state by default
instead of available.

Actions added to the current dropdown on node view to enable move to
manageable, available and active states. Actions are enabled and
disabled in view as the current state permits.

All necessary fields required to move a node to available state are
still required on the enroll node modal. This will be changed to only
the necessary fields to move a node to enroll state once the
functionality for editing a node is ready.

Change-Id: I349a293a1069ad01fd782d1828bad607f9b9d6b0
Co-Authored-By: Peter Piela <ppiela@cray.com>
This commit is contained in:
Elizabeth Elwell 2016-05-13 17:42:13 +01:00
parent 7d226b75da
commit e476fe9344
12 changed files with 254 additions and 40 deletions

View File

@ -27,7 +27,7 @@ from openstack_dashboard.api import base
LOG = logging.getLogger(__name__)
DEFAULT_IRONIC_API_VERSION = '1.6'
DEFAULT_IRONIC_API_VERSION = '1.11'
DEFAULT_INSECURE = False
DEFAULT_CACERT = None
@ -103,6 +103,19 @@ def node_set_power_state(request, node_id, state):
return ironicclient(request).node.set_power_state(node_id, state)
def node_set_provision_state(request, node_uuid, state):
"""Set the target provision state for a given node.
:param request: HTTP request.
:param node_uuid: The UUID of the node.
:param state: the target provision state to set.
:return: node.
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)
def node_set_maintenance(request, node_id, state, maint_reason=None):
"""Set the maintenance mode on a given node.

View File

@ -130,6 +130,23 @@ class StatesPower(generic.View):
return ironic.node_set_power_state(request, node_id, state)
@urls.register
class StatesProvision(generic.View):
url_regex = r'ironic/nodes/(?P<node_uuid>[0-9a-f-]+)/states/provision$'
@rest_utils.ajax(data_required=True)
def put(self, request, node_uuid):
"""Set the provision state for a specified node.
:param request: HTTP request.
:param node_id: Node uuid
:return: Return code
"""
state = request.DATA.get('state')
return ironic.node_set_provision_state(request, node_uuid, state)
@urls.register
class Maintenance(generic.View):

View File

@ -24,6 +24,7 @@ PANEL_GROUP = 'admin'
ADD_PANEL = 'ironic_ui.content.ironic.panel.Ironic'
# A list of applications to be prepended to INSTALLED_APPS
ADD_INSTALLED_APPS = ['ironic_ui', ]
# A list of AngularJS modules to be loaded when Angular bootstraps.
ADD_ANGULAR_MODULES = ['horizon.dashboard.admin.ironic']
# Automatically discover static resources in installed apps
AUTO_DISCOVER_STATIC_FILES = True

View File

@ -52,6 +52,7 @@
// selected driver
ctrl.driverProperties = null;
ctrl.driverPropertyGroups = null;
ctrl.moveNodeToManageableState = false;
// Parameter object that defines the node to be enrolled
ctrl.node = {
@ -276,9 +277,14 @@
});
ironic.createNode(ctrl.node).then(
function() {
function(response) {
$log.info("create node response = " + JSON.stringify(response));
$modalInstance.close();
$rootScope.$emit(ironicEvents.ENROLL_NODE_SUCCESS);
if (ctrl.moveNodeToManageableState) {
$log.info("Setting node provision state");
ironic.setNodeProvisionState(response.data.uuid, 'manage');
}
},
function() {
// No additional error processing for now

View File

@ -254,10 +254,8 @@
<!--end driver details tab-->
</div>
<!--end tabbed content-->
</form>
<!--end enroll node form-->
</div>
</div>
<!--modal footer-->
@ -267,6 +265,7 @@
<span class="fa fa-close"></span>
<span class="ng-scope" translate>Cancel</span>
</button>
<button type="submit"
ng-disabled="!ctrl.driverProperties ||
enrollNodeForm.$invalid"

View File

@ -18,6 +18,41 @@
(function () {
'use strict';
var provisionStateTransitionMatrix = {
enroll: {
manageable: 'manage'
},
manageable: {
active: 'adopt',
available: 'provide'
},
active: {
manageable: 'deleted'
},
available: {
active: 'active',
manageable: 'manage'
},
adopt_failed: {
manageable: 'manage',
active: 'adopt'
},
inspect_failed: {
manageable: 'manage'
},
clean_failed: {
manageable: 'manage'
},
deploy_failed: {
active: 'active',
manageable: 'deleted'
},
error: {
active: 'rebuild',
manageable: 'deleted'
}
};
angular
.module('horizon.app.core.openstack-service-api')
.factory('horizon.app.core.openstack-service-api.ironic', ironicAPI);
@ -45,10 +80,12 @@
getNode: getNode,
getNodes: getNodes,
getPortsWithNode: getPortsWithNode,
getProvisionStateTransitionVerb: getProvisionStateTransitionVerb,
powerOffNode: powerOffNode,
powerOnNode: powerOnNode,
putNodeInMaintenanceMode: putNodeInMaintenanceMode,
removeNodeFromMaintenanceMode: removeNodeFromMaintenanceMode
removeNodeFromMaintenanceMode: removeNodeFromMaintenanceMode,
setNodeProvisionState: setNodeProvisionState
};
return service;
@ -199,6 +236,33 @@
});
}
/**
* @description Set the target provision state of the node.
*
* http://docs.openstack.org/developer/ironic/webapi/v1.html#
* put--v1-nodes-(node_ident)-states-provision
*
* @param {string} uuid UUID of a node.
* @param {string} state Target provision state
* @return {promise} Promise
*/
function setNodeProvisionState(uuid, state) {
var data = {
state: state
};
return apiService.put('/api/ironic/nodes/' + uuid + '/states/provision',
data)
.success(function() {
toastService.add(
'success',
gettext('Successfully set target node provision state'));
})
.error(function(reason) {
var msg = gettext('Unable to set node provision state: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
/**
* @description Create an Ironic node
*
@ -319,6 +383,25 @@
toastService.add('error', interpolate(msg, [reason], false));
});
}
/**
* @description Get the verb used to transition a node from a source
* provision-state to a target provision-state
*
* @param {string} sourceState source state
* @param {string} targetState target state
* @return {string} Verb used to transition from source to target state.
* null if the requested transition is not allowed.
*/
function getProvisionStateTransitionVerb(sourceState, targetState) {
var verb = null;
if (angular.isDefined(provisionStateTransitionMatrix[sourceState]) &&
angular.isDefined(
provisionStateTransitionMatrix[sourceState][targetState])) {
verb = provisionStateTransitionMatrix[sourceState][targetState];
}
return verb;
}
}
}());

View File

@ -75,6 +75,7 @@
deleteNodes: deleteNodes,
deletePort: deletePort,
deletePorts: deletePorts,
getProvisionStateTransitionVerb: getProvisionStateTransitionVerb,
powerOn: powerOn,
powerOff: powerOff,
powerOnAll: powerOnNodes,
@ -82,7 +83,8 @@
putNodeInMaintenanceMode: putInMaintenanceMode,
removeNodeFromMaintenanceMode: removeFromMaintenanceMode,
putAllInMaintenanceMode: putNodesInMaintenanceMode,
removeAllFromMaintenanceMode: removeNodesFromMaintenanceMode
removeAllFromMaintenanceMode: removeNodesFromMaintenanceMode,
setProvisionState: setProvisionState
};
return service;
@ -191,6 +193,24 @@
return applyFuncToNodes(removeFromMaintenanceMode, nodes);
}
/*
* @name horizon.dashboard.admin.ironic.actions.setProvisionState
* @description Set the provisioning state of a specified node
*
* @param {object} args - Object with two properties named 'node'
* and 'verb'.
* node: node object.
* verb: string the value of which is the verb used to move
* the node to the desired target state for the node.
*/
function setProvisionState(args) {
ironic.setNodeProvisionState(args.node.uuid, args.verb);
}
function getProvisionStateTransitionVerb(sourceState, targetState) {
return ironic.getProvisionStateTransitionVerb(sourceState, targetState);
}
function createPort(node) {
return createPortService.modal(node);
}

View File

@ -26,6 +26,7 @@
'$scope',
'$rootScope',
'$location',
'horizon.framework.widgets.toast.service',
'horizon.app.core.openstack-service-api.ironic',
'horizon.dashboard.admin.ironic.events',
'horizon.dashboard.admin.ironic.actions',
@ -37,6 +38,7 @@
function IronicNodeDetailsController($scope,
$rootScope,
$location,
toastService,
ironic,
ironicEvents,
actions,
@ -61,6 +63,7 @@
}
];
ctrl.node = null;
ctrl.ports = [];
ctrl.portsSrc = [];
ctrl.basePath = basePath;
@ -72,6 +75,7 @@
ctrl.createPort = createPort;
ctrl.deletePort = deletePort;
ctrl.deletePorts = deletePorts;
ctrl.refresh = refresh;
var createPortHandler =
$rootScope.$on(ironicEvents.CREATE_PORT_SUCCESS,
@ -119,9 +123,19 @@
* @return {promise} promise
*/
function retrieveNode(uuid) {
var lastError = ctrl.node ? ctrl.node.last_error : null;
return ironic.getNode(uuid).then(function (response) {
ctrl.node = response.data;
ctrl.node.id = uuid;
if (lastError &&
ctrl.node.last_error !== "" &&
ctrl.node.last_error !== lastError) {
toastService.add(
'error',
"Node " + ctrl.node.name + ". " + ctrl.node.last_error);
}
});
}
@ -212,5 +226,15 @@
});
ctrl.actions.deletePorts(selectedPorts);
}
/**
* @name horizon.dashboard.admin.ironic.NodeDetailsController.refresh
* @description Update node information
*
* @return {void}
*/
function refresh() {
init();
}
}
})();

View File

@ -2,31 +2,48 @@
ng-controller="horizon.dashboard.admin.ironic.NodeDetailsController as ctrl">
<div class="pull-right">
<button class="btn btn-default btn-sm"
style="margin-right:10px;"
ng-click="ctrl.refresh()">
<span translate>Refresh</span>
</button>
<action-list dropdown>
<action button-type="split-button"
action-classes="'btn btn-default btn-sm'"
callback="ctrl.actions.powerOn"
item="ctrl.node"
disabled="ctrl.node['power_state']!=='power off'">
disabled="ctrl.node.power_state!=='power off'">
{$ 'Power on' | translate $}
</action>
<menu>
<action button-type="menu-item"
callback="ctrl.actions.powerOff"
item="ctrl.node"
disabled="ctrl.node['power_state']!=='power on'">
disabled="ctrl.node.power_state!=='power on'">
{$ 'Power off' | translate $}
</action>
<action button-type="menu-item"
callback="ctrl.putNodeInMaintenanceMode"
disabled="ctrl.node['maintenance']">
disabled="ctrl.node.maintenance">
{$ 'Maintenance on' | translate $}
</action>
<action button-type="menu-item"
callback="ctrl.removeNodeFromMaintenanceMode"
disabled="!ctrl.node['maintenance']">
disabled="!ctrl.node.maintenance">
{$ 'Maintenance off' | translate $}
</action>
<action ng-repeat="targetState in ['manageable', 'available', 'active']"
button-type="menu-item"
callback="ctrl.actions.setProvisionState"
item="{node: ctrl.node,
verb: ctrl.actions.getProvisionStateTransitionVerb(
ctrl.node.provision_state,
targetState)}"
disabled="ctrl.actions.getProvisionStateTransitionVerb(
ctrl.node.provision_state,
targetState) === null">
{$ ('Move to ' | translate) + targetState $}
</action>
</menu>
</action-list>
</div>

View File

@ -6,19 +6,19 @@
<hr class="header_rule">
<dl class="dl-horizontal">
<dt translate>Name</dt>
<dd>{$ ctrl.node['name'] $}</dd>
<dd>{$ ctrl.node.name $}</dd>
<dt translate>Maintenance</dt>
<dd>{$ ctrl.node['maintenance'] | yesno $}</dd>
<dd>{$ ctrl.node.maintenance | yesno $}</dd>
<dt translate>Maintenance Reason</dt>
<dd>{$ ctrl.node['maintenance_reason'] | noValue $}</dd>
<dd>{$ ctrl.node.maintenance_reason | noValue $}</dd>
<dt translate>Inspection Started At</dt>
<dd>{$ ctrl.node['inspection_started_at'] | date: 'medium' | noValue $}</dd>
<dd>{$ ctrl.node.inspection_started_at | date: 'medium' | noValue $}</dd>
<dt translate>Inspection Finished At</dt>
<dd>{$ ctrl.node['inspection_finished_at'] | date: 'medium' | noValue $}</dd>
<dd>{$ ctrl.node.inspection_finished_at | date: 'medium' | noValue $}</dd>
<dt translate>Reservation</dt>
<dd>{$ ctrl.node['reservation'] | noValue $}</dd>
<dd>{$ ctrl.node.reservation | noValue $}</dd>
<dt translate>Console Enabled</dt>
<dd>{$ ctrl.node['console_enabled'] | yesno $}</dd>
<dd>{$ ctrl.node.console_enabled | yesno $}</dd>
</dl>
</div>
@ -29,22 +29,22 @@
<dl class="dl-horizontal">
<dt translate>Instance ID</dt>
<dd>
<a href="/admin/instances/{$ ctrl.node['instance_uuid'] $}/detail">
{$ ctrl.node['instance_uuid'] | noValue $}
<a href="/admin/instances/{$ ctrl.node.instance_uuid $}/detail">
{$ ctrl.node.instance_uuid | noValue $}
</a>
</dd>
<dt translate>Power State</dt>
<dd ng-class="{'running': ctrl.node['target_power_state']}">{$ ctrl.node['power_state'] $}</dd>
<dd ng-class="{'running': ctrl.node.target_power_state}">{$ ctrl.node.power_state $}</dd>
<dt translate>Target Power State</dt>
<dd>{$ ctrl.node['target_power_state'] | noValue $}</dd>
<dd>{$ ctrl.node.target_power_state | noValue $}</dd>
<dt translate>Provision State</dt>
<dd>{$ ctrl.node['provision_state'] | noValue $}</dd>
<dd>{$ ctrl.node.provision_state | noValue $}</dd>
<dt translate>Target Provision State</dt>
<dd>{$ ctrl.node['target_provision_state'] | noValue $}</dd>
<dd>{$ ctrl.node.target_provision_state | noValue $}</dd>
<dt translate>Last Error</dt>
<dd>{$ ctrl.node['last_error'] | noValue $}</dd>
<dd>{$ ctrl.node.last_error | noValue $}</dd>
<dt translate>Updated At</dt>
<dd>{$ ctrl.node['updated_at'] | date: 'medium' | noValue $}</dd>
<dd>{$ ctrl.node.updated_at | date: 'medium' | noValue $}</dd>
</dl>
</div>
</div>

View File

@ -24,6 +24,7 @@
IronicNodeListController.$inject = [
'$scope',
'$rootScope',
'horizon.framework.widgets.toast.service',
'horizon.app.core.openstack-service-api.ironic',
'horizon.dashboard.admin.ironic.events',
'horizon.dashboard.admin.ironic.actions',
@ -34,6 +35,7 @@
function IronicNodeListController($scope,
$rootScope,
toastService,
ironic,
ironicEvents,
actions,
@ -43,7 +45,7 @@
var ctrl = this;
ctrl.nodes = [];
ctrl.nodeSrc = [];
ctrl.nodesSrc = [];
ctrl.basePath = basePath;
ctrl.actions = actions;
@ -52,6 +54,7 @@
ctrl.removeNodeFromMaintenanceMode = removeNodeFromMaintenanceMode;
ctrl.removeNodesFromMaintenanceMode = removeNodesFromMaintenanceMode;
ctrl.enrollNode = enrollNode;
ctrl.refresh = refresh;
/**
* Filtering - client-side MagicSearch
@ -132,11 +135,19 @@
}
function onGetNodes(response) {
ctrl.nodesSrc = response.data.items;
ctrl.nodesSrc.forEach(function (node) {
angular.forEach(response.data.items, function (node) {
node.id = node.uuid;
retrievePorts(node);
// Report any changes in last-error
if (node.last_error !== "" &&
angular.isDefined(ctrl.nodesSrc[node.uuid]) &&
node.last_error !== ctrl.nodesSrc[node.uuid].last_error) {
toastService.add('error',
"Node " + node.name + ". " + node.last_error);
}
});
ctrl.nodesSrc = response.data.items;
}
function retrievePorts(node) {
@ -166,6 +177,10 @@
function enrollNode() {
enrollNodeService.modal();
}
function refresh() {
init();
}
}
})();

View File

@ -15,11 +15,18 @@
<thead>
<tr>
<th colspan="8">
<button class="btn btn-default btn-sm pull-right"
ng-click="table.enrollNode()">
<span class="fa fa-plus"></span>
<span translate>Enroll Node</span>
</button>
<div class="pull-right">
<button class="btn btn-default btn-sm"
style="margin-right:10px;"
ng-click="table.refresh()">
<span translate>Refresh</span>
</button>
<button class="btn btn-default btn-sm"
ng-click="table.enrollNode()">
<span class="fa fa-plus"></span>
<span translate>Enroll Node</span>
</button>
</div>
</th>
<th class="action-col">
<action-list dropdown class="pull-right">
@ -117,7 +124,7 @@
<span ng-if="!node.instance_uuid">{$ 'No Instance' | translate $}</span>
</td>
<td class="rsp-p2" >
<div ng-class="{'running': node['target_power_state']}">
<div ng-class="{'running': node.target_power_state}">
{$ node.power_state $}
</div>
</td>
@ -130,32 +137,32 @@
<action button-type="split-button"
action-classes="'btn btn-default btn-sm'"
callback="table.actions.powerOn"
disabled="node['power_state']!=='power off'"
disabled="node.power_state !== 'power off'"
item="node">
{$ 'Power on' | translate $}
</action>
<menu>
<action button-type="menu-item"
callback="table.actions.powerOff"
disabled="node['power_state']!=='power on'"
disabled="node.power_state !== 'power on'"
item="node">
{$ 'Power off' | translate $}
</action>
<action button-type="menu-item"
callback="table.putNodeInMaintenanceMode"
disabled="node['maintenance']"
disabled="node.maintenance"
item="node">
{$ 'Maintenance on' | translate $}
</action>
<action button-type="menu-item"
callback="table.removeNodeFromMaintenanceMode"
disabled="!node['maintenance']"
disabled="!node.maintenance"
item="node">
{$ 'Maintenance off' | translate $}
</action>
<action button-type="menu-item"
callback="table.actions.deleteNode"
disabled="!(node['provision_state']==='available' || node['provision_state']==='nostate' || node['provision_state']==='manageable' || node['provision_state']==='enroll')"
disabled="!(node.provision_state === 'available' || node.provision_state === 'nostate' || node.provision_state === 'manageable' || node.provision_state === 'enroll')"
item="node">
<span class="fa fa-trash"></span>
{$ 'Delete node' | translate $}
@ -165,7 +172,19 @@
item="node">
{$ 'Create port' | translate $}
</action>
</menu>
<action ng-repeat="targetState in
['manageable', 'available', 'active']"
button-type="menu-item"
callback="table.actions.setProvisionState"
item="{node: node,
verb: table.actions.getProvisionStateTransitionVerb(
node.provision_state,
targetState)}"
disabled="table.actions.getProvisionStateTransitionVerb(
node.provision_state,
targetState) === null">
{$ ('Move to ' | translate) + targetState $}
</action>
</action-list>
</td>
</tr>