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__) LOG = logging.getLogger(__name__)
DEFAULT_IRONIC_API_VERSION = '1.6' DEFAULT_IRONIC_API_VERSION = '1.11'
DEFAULT_INSECURE = False DEFAULT_INSECURE = False
DEFAULT_CACERT = None 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) 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): def node_set_maintenance(request, node_id, state, maint_reason=None):
"""Set the maintenance mode on a given node. """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) 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 @urls.register
class Maintenance(generic.View): class Maintenance(generic.View):

View File

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

View File

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

View File

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

View File

@ -18,6 +18,41 @@
(function () { (function () {
'use strict'; '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 angular
.module('horizon.app.core.openstack-service-api') .module('horizon.app.core.openstack-service-api')
.factory('horizon.app.core.openstack-service-api.ironic', ironicAPI); .factory('horizon.app.core.openstack-service-api.ironic', ironicAPI);
@ -45,10 +80,12 @@
getNode: getNode, getNode: getNode,
getNodes: getNodes, getNodes: getNodes,
getPortsWithNode: getPortsWithNode, getPortsWithNode: getPortsWithNode,
getProvisionStateTransitionVerb: getProvisionStateTransitionVerb,
powerOffNode: powerOffNode, powerOffNode: powerOffNode,
powerOnNode: powerOnNode, powerOnNode: powerOnNode,
putNodeInMaintenanceMode: putNodeInMaintenanceMode, putNodeInMaintenanceMode: putNodeInMaintenanceMode,
removeNodeFromMaintenanceMode: removeNodeFromMaintenanceMode removeNodeFromMaintenanceMode: removeNodeFromMaintenanceMode,
setNodeProvisionState: setNodeProvisionState
}; };
return service; 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 * @description Create an Ironic node
* *
@ -319,6 +383,25 @@
toastService.add('error', interpolate(msg, [reason], false)); 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, deleteNodes: deleteNodes,
deletePort: deletePort, deletePort: deletePort,
deletePorts: deletePorts, deletePorts: deletePorts,
getProvisionStateTransitionVerb: getProvisionStateTransitionVerb,
powerOn: powerOn, powerOn: powerOn,
powerOff: powerOff, powerOff: powerOff,
powerOnAll: powerOnNodes, powerOnAll: powerOnNodes,
@ -82,7 +83,8 @@
putNodeInMaintenanceMode: putInMaintenanceMode, putNodeInMaintenanceMode: putInMaintenanceMode,
removeNodeFromMaintenanceMode: removeFromMaintenanceMode, removeNodeFromMaintenanceMode: removeFromMaintenanceMode,
putAllInMaintenanceMode: putNodesInMaintenanceMode, putAllInMaintenanceMode: putNodesInMaintenanceMode,
removeAllFromMaintenanceMode: removeNodesFromMaintenanceMode removeAllFromMaintenanceMode: removeNodesFromMaintenanceMode,
setProvisionState: setProvisionState
}; };
return service; return service;
@ -191,6 +193,24 @@
return applyFuncToNodes(removeFromMaintenanceMode, nodes); 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) { function createPort(node) {
return createPortService.modal(node); return createPortService.modal(node);
} }

View File

@ -26,6 +26,7 @@
'$scope', '$scope',
'$rootScope', '$rootScope',
'$location', '$location',
'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.actions', 'horizon.dashboard.admin.ironic.actions',
@ -37,6 +38,7 @@
function IronicNodeDetailsController($scope, function IronicNodeDetailsController($scope,
$rootScope, $rootScope,
$location, $location,
toastService,
ironic, ironic,
ironicEvents, ironicEvents,
actions, actions,
@ -61,6 +63,7 @@
} }
]; ];
ctrl.node = null;
ctrl.ports = []; ctrl.ports = [];
ctrl.portsSrc = []; ctrl.portsSrc = [];
ctrl.basePath = basePath; ctrl.basePath = basePath;
@ -72,6 +75,7 @@
ctrl.createPort = createPort; ctrl.createPort = createPort;
ctrl.deletePort = deletePort; ctrl.deletePort = deletePort;
ctrl.deletePorts = deletePorts; ctrl.deletePorts = deletePorts;
ctrl.refresh = refresh;
var createPortHandler = var createPortHandler =
$rootScope.$on(ironicEvents.CREATE_PORT_SUCCESS, $rootScope.$on(ironicEvents.CREATE_PORT_SUCCESS,
@ -119,9 +123,19 @@
* @return {promise} promise * @return {promise} promise
*/ */
function retrieveNode(uuid) { function retrieveNode(uuid) {
var lastError = ctrl.node ? ctrl.node.last_error : null;
return ironic.getNode(uuid).then(function (response) { return ironic.getNode(uuid).then(function (response) {
ctrl.node = response.data; ctrl.node = response.data;
ctrl.node.id = uuid; 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); 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"> ng-controller="horizon.dashboard.admin.ironic.NodeDetailsController as ctrl">
<div class="pull-right"> <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-list dropdown>
<action button-type="split-button" <action button-type="split-button"
action-classes="'btn btn-default btn-sm'" action-classes="'btn btn-default btn-sm'"
callback="ctrl.actions.powerOn" callback="ctrl.actions.powerOn"
item="ctrl.node" item="ctrl.node"
disabled="ctrl.node['power_state']!=='power off'"> disabled="ctrl.node.power_state!=='power off'">
{$ 'Power on' | translate $} {$ 'Power on' | translate $}
</action> </action>
<menu> <menu>
<action button-type="menu-item" <action button-type="menu-item"
callback="ctrl.actions.powerOff" callback="ctrl.actions.powerOff"
item="ctrl.node" item="ctrl.node"
disabled="ctrl.node['power_state']!=='power on'"> disabled="ctrl.node.power_state!=='power on'">
{$ 'Power off' | translate $} {$ 'Power off' | translate $}
</action> </action>
<action button-type="menu-item" <action button-type="menu-item"
callback="ctrl.putNodeInMaintenanceMode" callback="ctrl.putNodeInMaintenanceMode"
disabled="ctrl.node['maintenance']"> disabled="ctrl.node.maintenance">
{$ 'Maintenance on' | translate $} {$ 'Maintenance on' | translate $}
</action> </action>
<action button-type="menu-item" <action button-type="menu-item"
callback="ctrl.removeNodeFromMaintenanceMode" callback="ctrl.removeNodeFromMaintenanceMode"
disabled="!ctrl.node['maintenance']"> disabled="!ctrl.node.maintenance">
{$ 'Maintenance off' | translate $} {$ 'Maintenance off' | translate $}
</action> </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> </menu>
</action-list> </action-list>
</div> </div>

View File

@ -6,19 +6,19 @@
<hr class="header_rule"> <hr class="header_rule">
<dl class="dl-horizontal"> <dl class="dl-horizontal">
<dt translate>Name</dt> <dt translate>Name</dt>
<dd>{$ ctrl.node['name'] $}</dd> <dd>{$ ctrl.node.name $}</dd>
<dt translate>Maintenance</dt> <dt translate>Maintenance</dt>
<dd>{$ ctrl.node['maintenance'] | yesno $}</dd> <dd>{$ ctrl.node.maintenance | yesno $}</dd>
<dt translate>Maintenance Reason</dt> <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> <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> <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> <dt translate>Reservation</dt>
<dd>{$ ctrl.node['reservation'] | noValue $}</dd> <dd>{$ ctrl.node.reservation | noValue $}</dd>
<dt translate>Console Enabled</dt> <dt translate>Console Enabled</dt>
<dd>{$ ctrl.node['console_enabled'] | yesno $}</dd> <dd>{$ ctrl.node.console_enabled | yesno $}</dd>
</dl> </dl>
</div> </div>
@ -29,22 +29,22 @@
<dl class="dl-horizontal"> <dl class="dl-horizontal">
<dt translate>Instance ID</dt> <dt translate>Instance ID</dt>
<dd> <dd>
<a href="/admin/instances/{$ ctrl.node['instance_uuid'] $}/detail"> <a href="/admin/instances/{$ ctrl.node.instance_uuid $}/detail">
{$ ctrl.node['instance_uuid'] | noValue $} {$ ctrl.node.instance_uuid | noValue $}
</a> </a>
</dd> </dd>
<dt translate>Power State</dt> <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> <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> <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> <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> <dt translate>Last Error</dt>
<dd>{$ ctrl.node['last_error'] | noValue $}</dd> <dd>{$ ctrl.node.last_error | noValue $}</dd>
<dt translate>Updated At</dt> <dt translate>Updated At</dt>
<dd>{$ ctrl.node['updated_at'] | date: 'medium' | noValue $}</dd> <dd>{$ ctrl.node.updated_at | date: 'medium' | noValue $}</dd>
</dl> </dl>
</div> </div>
</div> </div>

View File

@ -24,6 +24,7 @@
IronicNodeListController.$inject = [ IronicNodeListController.$inject = [
'$scope', '$scope',
'$rootScope', '$rootScope',
'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.actions', 'horizon.dashboard.admin.ironic.actions',
@ -34,6 +35,7 @@
function IronicNodeListController($scope, function IronicNodeListController($scope,
$rootScope, $rootScope,
toastService,
ironic, ironic,
ironicEvents, ironicEvents,
actions, actions,
@ -43,7 +45,7 @@
var ctrl = this; var ctrl = this;
ctrl.nodes = []; ctrl.nodes = [];
ctrl.nodeSrc = []; ctrl.nodesSrc = [];
ctrl.basePath = basePath; ctrl.basePath = basePath;
ctrl.actions = actions; ctrl.actions = actions;
@ -52,6 +54,7 @@
ctrl.removeNodeFromMaintenanceMode = removeNodeFromMaintenanceMode; ctrl.removeNodeFromMaintenanceMode = removeNodeFromMaintenanceMode;
ctrl.removeNodesFromMaintenanceMode = removeNodesFromMaintenanceMode; ctrl.removeNodesFromMaintenanceMode = removeNodesFromMaintenanceMode;
ctrl.enrollNode = enrollNode; ctrl.enrollNode = enrollNode;
ctrl.refresh = refresh;
/** /**
* Filtering - client-side MagicSearch * Filtering - client-side MagicSearch
@ -132,11 +135,19 @@
} }
function onGetNodes(response) { function onGetNodes(response) {
ctrl.nodesSrc = response.data.items; angular.forEach(response.data.items, function (node) {
ctrl.nodesSrc.forEach(function (node) {
node.id = node.uuid; node.id = node.uuid;
retrievePorts(node); 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) { function retrievePorts(node) {
@ -166,6 +177,10 @@
function enrollNode() { function enrollNode() {
enrollNodeService.modal(); enrollNodeService.modal();
} }
function refresh() {
init();
}
} }
})(); })();

View File

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