Added functionality to enroll a node, and delete node(s)

These changes add functionality to enroll a node into the Ironic database,
and delete node(s) from the database.

Change-Id: Id0837b75e946ff702e81a47b9f8c3656beb5a783
This commit is contained in:
Peter Piela 2016-04-06 13:03:36 -04:00
parent fd86177443
commit 3a8ff7b3c8
12 changed files with 1205 additions and 94 deletions

View File

@ -117,3 +117,52 @@ def node_set_maintenance(request, node_id, state, maint_reason=None):
node_id, node_id,
state, state,
maint_reason=maint_reason) maint_reason=maint_reason)
def node_create(request, params):
"""Create a node
:param request: HTTP request.
:param params: Dictionary of node parameters
"""
node_manager = ironicclient(request).node
node = node_manager.create(**params)
field_list = ['chassis_uuid',
'driver',
'driver_info',
'properties',
'extra',
'uuid',
'name']
return dict([(f, getattr(node, f, '')) for f in field_list])
def node_delete(request, node_id):
"""Delete a node from inventory.
:param request: HTTP request.
:param node_id: The UUID of the node.
:return: node.
http://docs.openstack.org/developer/python-ironicclient/api/ironicclient.v1.node.html#ironicclient.v1.node.NodeManager.delete
"""
return ironicclient(request).node.delete(node_id)
def driver_list(request):
"""Retrieve a list of drivers.
:param request: HTTP request.
:return: A list of drivers.
"""
return ironicclient(request).driver.list()
def driver_properties(request, driver_name):
"""Retrieve the properties of a specified driver
:param request: HTTP request
:param driver_name: Name of the driver
:return: Property list
"""
return ironicclient(request).driver.properties(driver_name)

View File

@ -40,6 +40,24 @@ class Nodes(generic.View):
'items': [i.to_dict() for i in items], 'items': [i.to_dict() for i in items],
} }
@rest_utils.ajax(data_required=True)
def post(self, request):
"""Create an Ironic node
:param request: HTTP request
"""
params = request.DATA.get('node')
return ironic.node_create(request, params)
@rest_utils.ajax(data_required=True)
def delete(self, request):
"""Delete an Ironic node from inventory
:param request: HTTP request
"""
params = request.DATA.get('node')
return ironic.node_delete(request, params)
@urls.register @urls.register
class Node(generic.View): class Node(generic.View):
@ -122,3 +140,37 @@ class Maintenance(generic.View):
:return: Return code :return: Return code
""" """
return ironic.node_set_maintenance(request, node_id, 'off') return ironic.node_set_maintenance(request, node_id, 'off')
@urls.register
class Drivers(generic.View):
url_regex = r'ironic/drivers/$'
@rest_utils.ajax()
def get(self, request):
"""Get the list of drivers
:param request: HTTP request
:return: drivers
"""
items = ironic.driver_list(request)
return {
'items': [i.to_dict() for i in items]
}
@urls.register
class DriverProperties(generic.View):
url_regex = r'ironic/drivers/(?P<driver_name>[0-9a-zA-Z_-]+)/properties$'
@rest_utils.ajax()
def get(self, request, driver_name):
"""Get the properties associated with a specified driver
:param request: HTTP request
:param driver_name: Driver name
:return: Dictionary of properties
"""
return ironic.driver_properties(request, driver_name)

View File

@ -0,0 +1,179 @@
/*
* Copyright 2016 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';
/**
* Controller used to enroll a node in the Ironic database
*/
angular
.module('horizon.dashboard.admin.ironic')
.controller('EnrollNodeController', EnrollNodeController);
EnrollNodeController.$inject = [
'$rootScope',
'$modalInstance',
'horizon.app.core.openstack-service-api.ironic',
'horizon.dashboard.admin.ironic.enroll-node.service',
'$log'
];
function EnrollNodeController($rootScope,
$modalInstance,
ironic,
enrollNodeService,
$log) {
var ctrl = this;
ctrl.drivers = null;
ctrl.loadingDriverProperties = false;
// Object containing the set of properties associated with the currently
// selected driver
ctrl.driverProperties = null;
// Paramater object that defines the node to be enrolled
ctrl.node = {
name: null,
driver: null,
driver_info: {},
properties: {},
extra: {}
};
init();
function init() {
loadDrivers();
}
/**
* Get the list of currently active Ironic drivers
*
* @return {void}
*/
function loadDrivers() {
ironic.getDrivers().then(function(response) {
ctrl.drivers = response.data.items;
});
}
/**
* Get the properties associated with a specified driver
*
* @param {string} driverName - Name of driver
* @return {void}
*/
ctrl.loadDriverProperties = function(driverName) {
ctrl.node.driver = driverName;
ctrl.node.driver_info = {};
ctrl.loadingDriverProperties = true;
ctrl.driverProperties = null;
ironic.getDriverProperties(driverName).then(function(response) {
ctrl.driverProperties = {};
angular.forEach(response.data, function(desc, property) {
ctrl.driverProperties[property] =
new enrollNodeService.DriverProperty(property,
desc,
ctrl.driverProperties);
});
ctrl.loadingDriverProperties = false;
});
};
/**
* Cancel the node enrollment process
*
* @return {void}
*/
ctrl.cancel = function() {
$modalInstance.dismiss('cancel');
};
/**
* Enroll the defined node
*
* @return {void}
*/
ctrl.enroll = function() {
$log.debug(">> EnrollNodeController.enroll()");
angular.forEach(ctrl.driverProperties, function(property, name) {
$log.debug(name +
", required = " + property.isRequired() +
", active = " + property.isActive() +
", input value = " + property.inputValue);
if (property.isActive() && property.inputValue) {
$log.debug("Setting driver property " + name + " to " +
property.inputValue);
ctrl.node.driver_info[name] = property.inputValue;
}
});
ironic.createNode(ctrl.node).then(
function() {
$modalInstance.close();
$rootScope.$emit('ironic-ui:new-node');
},
function() {
// No additional error processing for now
});
$log.debug("<< EnrollNodeController.enroll()");
};
/**
* Delete a node property
*
* @param {string} propertyName - Name of the property
* @return {void}
*/
ctrl.deleteProperty = function(propertyName) {
delete ctrl.node.properties[propertyName];
};
/**
* Check whether the specified node property already exists
*
* @param {string} propertyName - Name of the property
* @return {boolean} True if the property already exists,
* otherwise false
*/
ctrl.checkPropertyUnique = function(propertyName) {
return !(propertyName in ctrl.node.properties);
};
/**
* Delete a node metadata property
*
* @param {string} propertyName - Name of the property
* @return {void}
*/
ctrl.deleteExtra = function(propertyName) {
delete ctrl.node.extra[propertyName];
};
/**
* Check whether the specified node metadata property already exists
*
* @param {string} propertyName - Name of the metadata property
* @return {boolean} True if the property already exists,
* otherwise false
*/
ctrl.checkExtraUnique = function(propertyName) {
return !(propertyName in ctrl.node.extra);
};
}
})();

View File

@ -0,0 +1,207 @@
<div class="modal-header">
<h3 class="modal-title" translate>Enroll Node</h3>
</div>
<div class="modal-body">
<h4 class="modal-title" translate>General</h4>
<form class="form-horizontal">
<div class="form-group">
<label for="name"
class="col-sm-3 control-label"
translate>Node Name</label>
<div class="col-sm-9">
<input type="text"
class="form-control"
ng-model="ctrl.node.name"
id="name"
name="name"
placeholder="{$ 'A unique node name. Optional.' | translate $}"/>
</div>
</div>
<div class="form-group">
<label for="driver"
class="col-sm-3 control-label"
translate>Node Driver</label>
<div class="col-sm-9">
<select id="driver"
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)">
<option value="" disabled selected translate>Select a Driver</option>
</select>
</div>
</div>
</form>
<div ng-if="!ctrl.loadingDriverProperties && ctrl.driverProperties">
<hr/>
<h4 class="modal-title" translate>Driver Info</h4>
</div>
<form class="form-horizontal"
name="DriverInfoForm"
id="DriverInfoForm">
<p class="text-center"
ng-if="ctrl.loadingDriverProperties">
<small><em><i class="fa fa-spin fa-refresh"></i></em></small>
</p>
<div class="form-group"
ng-repeat="(name, property) in ctrl.driverProperties"
ng-show="property.isActive()">
<label for="{$ name $}"
class="col-sm-3 control-label"
style="white-space: nowrap"
translate>
{$ name $}
<span class="help-icon"
data-container="body"
title=""
data-toggle="tooltip"
data-original-title="{$ property.getDescription() | translate $}">
<span class="fa fa-question-circle"></span>
</span>
</label>
<div ng-if="!property.getSelectOptions()" class="col-sm-9">
<input type="text"
class="form-control"
id="{$ name $}"
name="{$ name $}"
ng-model="property.inputValue"
placeholder="{$ property.getDescription() | translate $}"
ng-required="property.isRequired()"/>
</div>
<div ng-if="property.getSelectOptions()" class="col-sm-9">
<select id="{$ name $}"
class="form-control"
ng-options="opt for opt in property.getSelectOptions()"
ng-model="property.inputValue"
ng-required="property.isRequired()">
<option value=""
disabled
selected
translate>{$ property.getDescription() $}</option>
</select>
</div>
</div>
</form>
<hr/>
<h4 class="modal-title" translate>Properties</h4>
<form class="form-horizontal"
id="AddPropertyForm"
name="AddPropertyForm"
style="margin-bottom:10px;">
<div class="input-group input-group-sm">
<span class="input-group-addon"
style="width:25%;text-align:right">
Property</span>
<input class="form-control"
type="text"
ng-model="propertyName"
validate-unique="ctrl.checkPropertyUnique"
placeholder="{$ 'Property Name' | translate $}"/>
<span class="input-group-btn">
<button class="btn btn-primary"
type="button"
ng-disabled="!propertyName || AddPropertyForm.$invalid"
ng-click="ctrl.node.properties[propertyName] = null;
propertyName = null">
<span class="fa fa-plus"> </span>
</button>
</span>
</div>
</form>
<form class="form-horizontal"
id="PropertiesForm"
name="PropertiesForm">
<div class="input-group input-group-sm"
ng-repeat="(propertyName, propertyValue) in ctrl.node.properties">
<span class="input-group-addon"
style="width:25%;text-align:right">
{$ propertyName $}
</span>
<input class="form-control"
type="text"
name="{$ propertyName $}"
ng-model="ctrl.node.properties[propertyName]"
ng-required="true"/>
<div class="input-group-btn">
<a class="btn btn-default"
ng-click="ctrl.deleteProperty(propertyName)">
<span class="fa fa-minus"> </span>
</a>
</div>
</div>
</form>
<hr/>
<h4 class="modal-title" translate>Extra</h4>
<form class="form-horizontal"
id="AddExtraForm"
name="AddExtraForm"
style="margin-bottom:10px;">
<div class="input-group input-group-sm">
<span class="input-group-addon"
style="width:25%;text-align:right">
Extra</span>
<input class="form-control"
type="text"
ng-model="extraName"
validate-unique="ctrl.checkExtraUnique"
placeholder="{$ 'Property Name' | translate $}"/>
<span class="input-group-btn">
<button class="btn btn-primary"
type="button"
ng-disabled="!extraName || AddExtraForm.$invalid"
ng-click="ctrl.node.extra[extraName] = null;
extraName = null">
<span class="fa fa-plus"> </span>
</button>
</span>
</div>
</form>
<form class="form-horizontal"
id="ExtraForm"
name="ExtraForm">
<div class="input-group input-group-sm"
ng-repeat="(propertyName, propertyValue) in ctrl.node.extra">
<span class="input-group-addon"
style="width:25%;text-align:right">
{$ propertyName $}
</span>
<input class="form-control"
type="text"
name="{$ propertyName $}"
ng-model="ctrl.node.extra[propertyName]"
ng-required="true"/>
<div class="input-group-btn">
<a class="btn btn-default"
ng-click="ctrl.deleteExtra(propertyName)">
<span class="fa fa-minus"> </span>
</a>
</div>
</div>
</form>
</div>
<div class="modal-footer ng-scope">
<button class="btn btn-default"
ng-click="ctrl.cancel()">
<span class="fa fa-close"></span>
<span class="ng-scope" translate>Cancel</span>
</button>
<button type="submit"
ng-disabled="!ctrl.driverProperties ||
DriverInfoForm.$invalid ||
PropertiesForm.$invalid ||
ExtraForm.$invalid"
ng-click="ctrl.enroll()"
class="btn btn-primary"
translate>
Enroll Node
</button>
</div>

View File

@ -0,0 +1,332 @@
/*
* Copyright 2016 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';
var REQUIRED = " " + gettext("Required") + ".";
var selectOptionsRegexp =
new RegExp(
gettext('(?:[Oo]ne of )(?!this)((?:(?:"[^"]+"|[^,\. ]+)(?:, |\.))+)'));
var defaultValueRegexp = new RegExp(gettext('default is ([^". ]+|"[^"]+")'));
var oneOfRegexp =
new RegExp(gettext('One of this, (.*) must be specified\.'));
var notInsideMatch = -1;
angular
.module('horizon.dashboard.admin.ironic')
.factory('horizon.dashboard.admin.ironic.enroll-node.service',
enrollNodeService);
enrollNodeService.$inject = [
'$modal',
'horizon.dashboard.admin.basePath',
'$log'
];
function enrollNodeService($modal, basePath, $log) {
var service = {
modal: modal,
DriverProperty: DriverProperty
};
function modal() {
var options = {
controller: 'EnrollNodeController as ctrl',
templateUrl: basePath + '/ironic/enroll-node/enroll-node.html'
};
return $modal.open(options);
}
/**
* Construct a new driver property
*
* @class DriverProperty
* @param {string} name - Name of property
* @param {string} desc - Description of property
* @param {object} propertySet - Set of properties to which this one belongs
*
* @property {string} defaultValue - Default value of the property
* @property {string[]} selectOptions - If the property is limited to a
* set of enumerted values then selectOptions will be an array of those
* values, otherwise null
* @property {boolean} required - Boolean value indicating whether a value
* must be supplied for this property if it is active
* @property {PostfixExpr} isActiveExpr - Null if this property is always
* active; otherwise, a boolean expression that when evaluated will
* return whether this variable is active
* @propery {string} inputValue User assigned value for this property
*/
function DriverProperty(name, desc, propertySet) {
this.name = name;
this.desc = desc;
this.propertySet = propertySet;
// Determine whether this property should be presented as a selection
this.selectOptions = this._analyzeSelectOptions();
this.required = null; // Initialize to unknown
// Expression to be evaluated to determine whether property is active.
// By default the property is considered active.
this.isActiveExpr = null;
var result = this._analyzeRequiredOnlyDependencies();
if (result) {
this.required = result[0];
this.isActiveExpr = result[1];
}
if (!this.isActiveExpr) {
result = this._analyzeOneOfDependencies();
if (result) {
this.required = result[0];
this.isActiveExpr = result[1];
}
}
if (this.required === null) {
this.required = desc.endsWith(REQUIRED);
}
this.defaultValue = this._getDefaultValue();
this.inputValue = null;
}
DriverProperty.prototype.isActive = function() {
if (!this.isActiveExpr) {
return true;
}
var active = this.isActiveExpr.evaluate(this.propertySet);
return active === null ? true : active;
};
/*
* Must a value be provided for this property
*
* @return {boolean} True if a value must be provided for this property
*/
DriverProperty.prototype.isRequired = function() {
return this.required && this.isActive();
};
DriverProperty.prototype._analyzeSelectOptions = function() {
var match = this.desc.match(selectOptionsRegexp);
if (!match) {
return null;
}
var matches = match[1].substring(0, match[1].length - 1).split(", ");
var options = [];
angular.forEach(matches, function(match) {
options.push(trimQuotes(match));
});
return options;
};
/**
* Get the list of select options for this property
*
* @return {string[]} null if this property is not selectable; else,
* an array of selectable options
*/
DriverProperty.prototype.getSelectOptions = function() {
return this.selectOptions;
};
/**
* Remove leading/trailing double-quotes from a string
*
* @param {string} str - String to be trimmed
* @return {string} trim'd string
*/
function trimQuotes(str) {
return str.charAt(0) === '"'
? str.substring(1, str.length - 1) : str;
}
/**
* Get the default value of this property
*
* @return {string} Default value of this property
*/
DriverProperty.prototype._getDefaultValue = function() {
var match = this.desc.match(defaultValueRegexp);
return match ? trimQuotes(match[1]) : null;
};
/**
* Get the actual value of this property
*
* @return {string} Get the actual value of this property. If
* an input value has not been specified, but a default value exists
* that will be returned.
*/
DriverProperty.prototype.getActualValue = function() {
return this.inputValue ? this.inputValue
: this.defaultValue ? this.defaultValue : null;
};
/**
* Get the description of this property
*
* @return {string} Description of this property
*/
DriverProperty.prototype.getDescription = function() {
return this.desc;
};
/**
* Use the property description to build an expression that will
* evaluate to a boolean result indicating whether the property is
* active
*
* @return {array} null if this property is not dependent on any others;
* otherwise,
* [0] boolean indicating whether if active a value must be
* supplied for this property.
* [1] an expression that when evaluated will return a boolean
* result indicating whether this property is active
*/
DriverProperty.prototype._analyzeRequiredOnlyDependencies = function() {
var re = /(Required|Used) only if ([^ ]+) is set to /g;
var match = re.exec(this.desc);
if (!match) {
return null;
}
// Build logical expression to describe under what conditions this
// property is active
var expr = new PostfixExpr();
var numAdds = 0;
var i = notInsideMatch;
var j = re.lastIndex;
while (j < this.desc.length) {
if (i === notInsideMatch && this.desc.charAt(j) === ".") {
break;
}
if (this.desc.charAt(j) === '"') {
if (i === notInsideMatch) {
i = j + 1;
} else {
expr.addProperty(match[2]);
expr.addValue(this.desc.substring(i, j));
expr.addOperator("==");
numAdds++;
if (numAdds > 1) {
expr.addOperator("or");
}
i = notInsideMatch;
}
}
j++;
}
$log.debug("_analyzeRequiredOnlyDependencies | " +
this.desc + " | " +
match[2] + ", " +
JSON.stringify(expr));
return [match[1] === "Required", expr];
};
DriverProperty.prototype._analyzeOneOfDependencies = function() {
var match = this.desc.match(oneOfRegexp);
if (!match) {
return null;
}
// Build logical expression to describe under what conditions this
// property is active
var expr = new PostfixExpr();
var parts = match[1].split(", or ");
expr.addProperty(parts[1]);
expr.addValue(null);
expr.addOperator("==");
parts = parts[0].split(", ");
for (var i = 0; i < parts.length; i++) {
expr.addProperty(parts[i]);
expr.addValue(null);
expr.addOperator("==");
expr.addOperator("and");
}
$log.debug("_analyzeOneOfDependencies | " +
this.desc + " | " +
JSON.stringify(match) + ", " +
JSON.stringify(expr));
return [true, expr];
};
/**
* PostFixExpr is a class primarily developed to support the
* evaluation of boolean expressions that determine whether a
* particular property is active.
*
* The expression is stored as a postfix sequence of operands and
* operators. Operands are currently limited to the literal values
* and the values of properties in a specified set. Currently
* supported operands are ==, or, and.
*
* @return {void}
*/
function PostfixExpr() {
this.elem = [];
}
PostfixExpr.prototype.addProperty = function(propertyName) {
this.elem.push({name: propertyName});
};
PostfixExpr.prototype.addValue = function(value) {
this.elem.push({value: value});
};
PostfixExpr.prototype.addOperator = function(opId) {
this.elem.push({op: opId});
};
/**
* Evaluate the experssion using property values from a specified
* set
*
* @param {object} propertySet - Dictionary of DriverProperty instances
*
* @return {value} Value of the expression. Null if the expression
* could not be successfully evaluated.
*/
PostfixExpr.prototype.evaluate = function(propertySet) {
var resultStack = [];
for (var i = 0, len = this.elem.length; i < len; i++) {
var elem = this.elem[i];
if (angular.isDefined(elem.name)) {
resultStack.push(propertySet[elem.name].getActualValue());
} else if (angular.isDefined(elem.value)) {
resultStack.push(elem.value);
} else if (angular.isDefined(elem.op)) {
if (elem.op === "==") {
resultStack.push(resultStack.pop() === resultStack.pop());
} else if (elem.op === "or") {
resultStack.push(resultStack.pop() || resultStack.pop());
} else if (elem.op === "and") {
resultStack.push(resultStack.pop() && resultStack.pop());
} else {
return null;
}
} else {
return null;
}
}
return resultStack.length === 1 ? resultStack.pop() : null;
};
return service;
}
})();

View File

@ -0,0 +1,115 @@
/**
* Copyright 2016 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.enroll-node.service',
function() {
var service;
beforeEach(module('horizon.dashboard.admin'));
beforeEach(module('horizon.dashboard.admin.ironic'));
beforeEach(module(function($provide) {
$provide.value('$modal', jasmine.createSpy());
}));
beforeEach(inject(function($injector) {
service =
$injector.get('horizon.dashboard.admin.ironic.enroll-node.service');
}));
it('defines the service', function() {
expect(service).toBeDefined();
});
describe('DriverProperty', function() {
it('Base construction', function() {
var propertyName = 'propertyName';
var description = '';
var propertySet = [];
var property = new service.DriverProperty(propertyName,
description,
propertySet);
expect(property.name).toBe(propertyName);
expect(property.desc).toBe(description);
expect(property.propertySet).toBe(propertySet);
expect(property.getSelectOptions()).toBe(null);
expect(property.required).toBe(false);
expect(property.defaultValue).toBe(null);
expect(property.inputValue).toBe(null);
expect(property.getActualValue()).toBe(null);
expect(property.isActive()).toBe(true);
});
it('Required - ends with', function() {
var property = new service.DriverProperty('propertyName',
' Required.',
[]);
expect(property.required).toBe(true);
});
it('Not required - missing space', function() {
var property = new service.DriverProperty('propertyName',
'Required.',
[]);
expect(property.required).toBe(false);
});
it('Not required - missing period', function() {
var property = new service.DriverProperty('propertyName',
' Required',
[]);
expect(property.required).toBe(false);
});
it('Select options', function() {
var property = new service.DriverProperty(
'propertyName',
'One of "foo", bar.',
[]);
expect(property.getSelectOptions()).toEqual(['foo', 'bar']);
});
it('Select options - No single quotes', function() {
var property = new service.DriverProperty(
'propertyName',
"One of 'foo', bar.",
[]);
expect(property.getSelectOptions()).toEqual(["'foo'", 'bar']);
});
it('default - is string', function() {
var property = new service.DriverProperty(
'propertyName',
'default is "5.1".',
[]);
expect(property._getDefaultValue()).toEqual('5.1');
});
it('default - period processing', function() {
var property = new service.DriverProperty(
'propertyName',
'default is 5.1.',
[]);
expect(property._getDefaultValue()).toEqual('5');
});
});
});
})();

View File

@ -28,159 +28,254 @@
]; ];
/** /**
* @ngdoc service * Service that provides access to the Ironic client API
* @name horizon.app.core.openstack-service-api.ironic *
* @description Provides access to Ironic API * @param {object} apiService - HTTP service
* @param {object} toastService - User message service
* @return {object} Ironic API service
*/ */
function ironicAPI(apiService, toastService) { function ironicAPI(apiService, toastService) {
var service = { var service = {
getNodes: getNodes, createNode: createNode,
deleteNode: deleteNode,
getDrivers: getDrivers,
getDriverProperties: getDriverProperties,
getNode: getNode, getNode: getNode,
getNodes: getNodes,
getPortsWithNode: getPortsWithNode, getPortsWithNode: getPortsWithNode,
putNodeInMaintenanceMode: putNodeInMaintenanceMode, powerOffNode: powerOffNode,
removeNodeFromMaintenanceMode: removeNodeFromMaintenanceMode,
powerOnNode: powerOnNode, powerOnNode: powerOnNode,
powerOffNode: powerOffNode putNodeInMaintenanceMode: putNodeInMaintenanceMode,
removeNodeFromMaintenanceMode: removeNodeFromMaintenanceMode
}; };
return service; return service;
///////////
/** /**
* @name horizon.app.core.openstack-service-api.ironic.getNodes * Retrieve a list of nodes
* @description Retrieve a list of nodes
* http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-nodes * http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-nodes
* *
* @return Node collection in JSON * @return {promise} Node collection in JSON
* http://docs.openstack.org/developer/ironic/webapi/v1.html#NodeCollection * http://docs.openstack.org/developer/ironic/webapi/v1.html#NodeCollection
*/ */
function getNodes() { function getNodes() {
return apiService.get('/api/ironic/nodes/') return apiService.get('/api/ironic/nodes/')
.error(function() { .error(function() {
toastService.add('error', gettext('Unable to retrieve Ironic nodes.')); toastService.add('error',
gettext('Unable to retrieve Ironic nodes.'));
}); });
} }
/** /**
* @name horizon.app.core.openstack-service-api.ironic.getNode * Retrieve information about the given node.
* @description Retrieve information about the given node.
* *
* http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-nodes-(node_ident) * http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-
* nodes-(node_ident)
* *
* @param {string} uuid UUID or logical name of a node. * @param {string} uuid UUID or logical name of a node.
* @return {promise} Node
*/ */
function getNode(uuid) { function getNode(uuid) {
return apiService.get('/api/ironic/nodes/' + uuid).error(function() { return apiService.get('/api/ironic/nodes/' + uuid)
toastService.add('error', gettext('Unable to retrieve the Ironic node.')); .error(function(reason) {
}); var msg = gettext('Unable to retrieve the Ironic node: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
} }
/** /**
* @name horizon.app.core.openstack-service-api.ironic.getPortsWithNode * Retrieve a list of ports associated with a node.
* @description Retrieve a list of ports associated with a node.
* *
* http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-ports * http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-ports
* *
* @param {string} uuid UUID or logical name of a node. * @param {string} uuid UUID or logical name of a node.
* @return {promise} List of ports
*/ */
function getPortsWithNode(uuid) { function getPortsWithNode(uuid) {
var config = { var config = {
params : { params : {
node_id: uuid node_id: uuid
} }
}; };
return apiService.get('/api/ironic/ports/', config).error(function() { return apiService.get('/api/ironic/ports/', config)
toastService.add('error', gettext('Unable to retrieve the Ironic node ports.')); .error(function(reason) {
}); var msg = gettext(
'Unable to retrieve the Ironic node ports: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
} }
/** /**
* @name horizon.app.core.openstack-service-api.ironic.putNodeInMaintenanceMode * Put the node in maintenance mode.
* @description Put the node in maintenance mode.
* *
* \href{http://docs.openstack.org/developer/ironic/webapi/v1.html# * http://docs.openstack.org/developer/ironic/webapi/v1.html#
* put--v1-nodes-(node_ident)-maintenance} * put--v1-nodes-(node_ident)-maintenance
* *
* @param {string} uuid UUID or logical name of a node. * @param {string} uuid UUID or logical name of a node.
* @param {string} reason Reason for why node is being put into
* maintenance mode
* @return {promise} Promise
*/ */
function putNodeInMaintenanceMode(uuid, reason) { function putNodeInMaintenanceMode(uuid, reason) {
var data = { var data = {
maint_reason: reason ? reason : gettext("No maintenance reason given.") maint_reason: reason ? reason : gettext("No maintenance reason given.")
}; };
return apiService.patch('/api/ironic/nodes/' + uuid + '/maintenance', data).error(function() { return apiService.patch('/api/ironic/nodes/' + uuid + '/maintenance',
toastService.add('error', data)
gettext('Unable to put the Ironic node in maintenance mode.')); .error(function(reason) {
}); var msg = gettext(
'Unable to put the Ironic node in maintenance mode: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
} }
/** /**
* @name horizon.app.core.openstack-service-api.ironic.removeNodeFromMaintenanceMode * Remove the node from maintenance mode.
* @description Remove the node from maintenance mode.
* *
* \href{http://docs.openstack.org/developer/ironic/webapi/v1.html# * http://docs.openstack.org/developer/ironic/webapi/v1.html#
* delete--v1-nodes-(node_ident)-maintenance} * delete--v1-nodes-(node_ident)-maintenance
* *
* @param {string} uuid UUID or logical name of a node. * @param {string} uuid UUID or logical name of a node.
* @return {promise} Promise
*/ */
function removeNodeFromMaintenanceMode(uuid) { function removeNodeFromMaintenanceMode(uuid) {
return apiService.delete('/api/ironic/nodes/' + uuid + '/maintenance').error(function() { return apiService.delete('/api/ironic/nodes/' + uuid + '/maintenance')
toastService.add('error', .error(function(reason) {
gettext('Unable to remove the Ironic node from maintenance mode.')); var msg = gettext('Unable to remove the Ironic node ' +
}); 'from maintenance mode: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
} }
/** /**
* @name horizon.app.core.openstack-service-api.ironic.powerOnNode * Set the power state of the node.
* @description Set the power state of the node.
* *
* \href{http://docs.openstack.org/developer/ironic/webapi/v1.html# * http://docs.openstack.org/developer/ironic/webapi/v1.html#
* put--v1-nodes-(node_ident)-states-power} * put--v1-nodes-(node_ident)-states-power
* *
* @param {string} uuid UUID or logical name of a node. * @param {string} uuid UUID or logical name of a node.
* @return {promise} Promise
*/ */
function powerOnNode(uuid) { function powerOnNode(uuid) {
var data = { var data = {
state: 'on' state: 'on'
}; };
return apiService.patch('/api/ironic/nodes/' + uuid + '/states/power', data) return apiService.patch('/api/ironic/nodes/' + uuid + '/states/power',
.success(function () { data)
toastService.add('success', gettext('Refresh page to see updated power status')); .success(function() {
toastService.add('success',
gettext('Refresh page to see updated power status'));
}) })
.error(function () { .error(function(reason) {
toastService.add('error', gettext('Unable to power on the node')); var msg = gettext('Unable to power on the node: %s');
toastService.add('error', interpolate(msg, [reason], false));
}); });
} }
/** /**
* @name horizon.app.core.openstack-service-api.ironic.powerOffNode * Set the power state of the node.
* @description Set the power state of the node.
* *
* \href{http://docs.openstack.org/developer/ironic/webapi/v1.html# * http://docs.openstack.org/developer/ironic/webapi/v1.html#
* put--v1-nodes-(node_ident)-states-power} * put--v1-nodes-(node_ident)-states-power
* *
* @param {string} uuid UUID or logical name of a node. * @param {string} uuid UUID or logical name of a node.
* @return {promise} Promise
*/ */
function powerOffNode(uuid) { function powerOffNode(uuid) {
var data = { var data = {
state: 'off' state: 'off'
}; };
return apiService.patch('/api/ironic/nodes/' + uuid + '/states/power', data) return apiService.patch('/api/ironic/nodes/' + uuid + '/states/power',
.success(function () { data)
toastService.add('success', gettext('Refresh page to see updated power status')); .success(function() {
toastService.add('success',
gettext('Refresh page to see updated power status'));
}) })
.error(function () { .error(function(reason) {
toastService.add('error', gettext('Unable to power off the node')); var msg = gettext('Unable to power off the node: %s');
toastService.add('error', interpolate(msg, [reason], false));
}); });
} }
/**
* Create an Ironic node
*
* http://docs.openstack.org/developer/ironic/webapi/v1.html#post--v1-nodes
*
* @param {object} params Object containing parameters that define
* the node to be created
* @return {promise} Promise
*/
function createNode(params) {
var data = {
node: params
};
return apiService.post('/api/ironic/nodes/', data)
.success(function() {
})
.error(function(reason) {
var msg = gettext('Unable to create node: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
/**
* Delete the specified node from inventory
*
* http://docs.openstack.org/developer/ironic/webapi/v1.html#
* delete--v1-nodes
*
* @param {string} nodeIdent UUID or logical name of a node.
* @return {promise} Promise
*/
function deleteNode(nodeIdent) {
var data = {
node: nodeIdent
};
return apiService.delete('/api/ironic/nodes/', data)
.success(function() {
})
.error(function(reason) {
var msg = gettext('Unable to delete node %s: %s');
toastService.add(
'error',
interpolate(msg, [nodeIdent, reason], false));
});
}
/**
* Retrieve the list of Ironic drivers
*
* http://docs.openstack.org/developer/ironic/webapi/v1.html#get--v1-drivers
*
* @return {promise} Driver collection in JSON
* http://docs.openstack.org/developer/ironic/webapi/v1.html#DriverList
*/
function getDrivers() {
return apiService.get('/api/ironic/drivers/').error(function(reason) {
var msg = gettext('Unable to retrieve Ironic drivers: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
/**
* Retrieve properities of a specified driver
*
* http://docs.openstack.org/developer/ironic/webapi/v1.html#
* get--v1-drivers-properties
*
* @param {string} driverName - Driver name
* @returns {promise} Property list
*/
function getDriverProperties(driverName) {
return apiService.get(
'/api/ironic/drivers/' + driverName + '/properties').error(
function(reason) {
var msg = gettext(
'Unable to retrieve driver properties: %s');
toastService.add('error', interpolate(msg, [reason], false));
});
}
} }
}()); }());

View File

@ -15,7 +15,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-default secondary" <button class="btn btn-default secondary"
type="button" type="button"
ng-click="ctrl.cancel()" ng-click="ctrl.cancel()"
translate> translate>
@ -28,3 +28,4 @@
Put Node(s) Into Maintenance Mode Put Node(s) Into Maintenance Mode
</button> </button>
</div> </div>

View File

@ -20,6 +20,20 @@
var POWER_STATE_ON = 'power on'; var POWER_STATE_ON = 'power on';
var POWER_STATE_OFF = 'power off'; var POWER_STATE_OFF = 'power off';
var DELETE_NODE_TITLE = gettext("Delete Node");
var DELETE_NODE_MSG =
gettext('Are you sure you want to delete node "%s"? ' +
'This action cannot be undone.');
var DELETE_NODE_SUCCESS = gettext('Successfully deleted node "%s"');
var DELETE_NODE_ERROR = gettext('Unable to delete node "%s"');
var DELETE_NODES_TITLE = gettext("Delete Nodes");
var DELETE_NODES_MSG =
gettext('Are you sure you want to delete nodes "%s"? ' +
'This action cannot be undone.');
var DELETE_NODES_SUCCESS = gettext('Successfully deleted nodes "%s"');
var DELETE_NODES_ERROR = gettext('Error deleting nodes "%s"');
angular angular
.module('horizon.dashboard.admin.ironic') .module('horizon.dashboard.admin.ironic')
.factory('horizon.dashboard.admin.ironic.actions', actions); .factory('horizon.dashboard.admin.ironic.actions', actions);
@ -27,11 +41,15 @@
actions.$inject = [ actions.$inject = [
'horizon.app.core.openstack-service-api.ironic', 'horizon.app.core.openstack-service-api.ironic',
'horizon.framework.widgets.toast.service', 'horizon.framework.widgets.toast.service',
'$q' 'horizon.framework.widgets.modal.deleteModalService',
'$q',
'$rootScope'
]; ];
function actions(ironic, toastService, $q) { function actions(ironic, toastService, deleteModalService, $q, $rootScope) {
var service = { var service = {
deleteNode: deleteNode,
deleteNodes: deleteNodes,
powerOn: powerOn, powerOn: powerOn,
powerOff: powerOff, powerOff: powerOff,
powerOnAll: powerOnNodes, powerOnAll: powerOnNodes,
@ -44,33 +62,62 @@
return service; return service;
function deleteNode(node) {
var labels = {
title: DELETE_NODE_TITLE,
message: DELETE_NODE_MSG,
submit: DELETE_NODE_TITLE,
success: DELETE_NODE_SUCCESS,
error: DELETE_NODE_ERROR
};
var context = {
labels: labels,
deleteEntity: ironic.deleteNode,
successEvent: "ironic-ui:delete-node-success"
};
return deleteModalService.open($rootScope, [node], context);
}
function deleteNodes(nodes) {
var labels = {
title: DELETE_NODES_TITLE,
message: DELETE_NODES_MSG,
submit: DELETE_NODES_TITLE,
success: DELETE_NODES_SUCCESS,
error: DELETE_NODES_ERROR
};
var context = {
labels: labels,
deleteEntity: ironic.deleteNode,
successEvent: "ironic-ui:delete-node-success"
};
return deleteModalService.open($rootScope, nodes, context);
}
// power state // power state
function powerOn(node) { function powerOn(node) {
if (node.power_state !== POWER_STATE_OFF) { if (node.power_state !== POWER_STATE_OFF) {
return $q.reject(gettext("Node is not powered off.")); var msg = gettext("Node %s is not powered off.");
return $q.reject(interpolate(msg, [node], false));
} }
return ironic.powerOnNode(node.uuid).then( return ironic.powerOnNode(node.uuid).then(
function() { function() {
// Set power state to be indeterminate // Set power state to be indeterminate
node.power_state = null; node.power_state = null;
}, }
function(reason) { );
toastService.add('error', reason);
});
} }
function powerOff(node) { function powerOff(node) {
if (node.power_state !== POWER_STATE_ON) { if (node.power_state !== POWER_STATE_ON) {
return $q.reject(gettext("Node is not powered on.")); var msg = gettext("Node %s is not powered on.");
return $q.reject(interpolate(msg, [node], false));
} }
return ironic.powerOffNode(node.uuid).then( return ironic.powerOffNode(node.uuid).then(
function() { function() {
// Set power state to be indeterminate // Set power state to be indeterminate
node.power_state = null; node.power_state = null;
},
function(reason) {
toastService.add('error', reason);
} }
); );
} }
@ -87,30 +134,26 @@
function putInMaintenanceMode(node, maintReason) { function putInMaintenanceMode(node, maintReason) {
if (node.maintenance !== false) { if (node.maintenance !== false) {
return $q.reject(gettext("Node is already in maintenance mode.")); var msg = gettext("Node %s is already in maintenance mode.");
return $q.reject(interpolate(msg, [node], false));
} }
return ironic.putNodeInMaintenanceMode(node.uuid, maintReason).then( return ironic.putNodeInMaintenanceMode(node.uuid, maintReason).then(
function () { function () {
node.maintenance = true; node.maintenance = true;
node.maintenance_reason = maintReason; node.maintenance_reason = maintReason;
},
function(reason) {
toastService.add('error', reason);
} }
); );
} }
function removeFromMaintenanceMode(node) { function removeFromMaintenanceMode(node) {
if (node.maintenance !== true) { if (node.maintenance !== true) {
return $q.reject(gettext("Node is not in maintenance mode.")); var msg = gettext("Node %s is not in maintenance mode.");
return $q.reject(interpolate(msg, [node], false));
} }
return ironic.removeNodeFromMaintenanceMode(node.uuid).then( return ironic.removeNodeFromMaintenanceMode(node.uuid).then(
function () { function () {
node.maintenance = false; node.maintenance = false;
node.maintenance_reason = ""; node.maintenance_reason = "";
},
function (reason) {
toastService.add('error', reason);
} }
); );
} }

View File

@ -24,7 +24,7 @@
<dt ng-repeat-start="port in ctrl.ports"> <dt ng-repeat-start="port in ctrl.ports">
{$ 'MAC ' + (1 + $index) $}</dt> {$ 'MAC ' + (1 + $index) $}</dt>
<dd ng-if="vif_port_id = ctrl.getVifPortId(port)"> <dd ng-if="vif_port_id = ctrl.getVifPortId(port)">
<a href="/admin/networks/ports/{$ vif_port_id $}/detail"> <a href="/dashboard/admin/networks/ports/{$ vif_port_id $}/detail">
{$ port.address $} {$ port.address $}
</a> </a>
</dd> </dd>
@ -109,13 +109,13 @@
<dd>{$ ctrl.node.instance_info.display_name $}</dd> <dd>{$ ctrl.node.instance_info.display_name $}</dd>
<dt translate>Ramdisk</dt> <dt translate>Ramdisk</dt>
<dd> <dd>
<a href="/admin/images/{$ ctrl.node.instance_info.ramdisk $}/detail"> <a href="/dashboard/admin/images/{$ ctrl.node.instance_info.ramdisk $}/detail">
{$ ctrl.node.instance_info.ramdisk $} {$ ctrl.node.instance_info.ramdisk $}
</a> </a>
</dd> </dd>
<dt translate>Kernel</dt> <dt translate>Kernel</dt>
<dd> <dd>
<a href="/admin/images/{$ ctrl.node.instance_info.kernel $}/detail"> <a href="/dashboard/admin/images/{$ ctrl.node.instance_info.kernel $}/detail">
{$ ctrl.node.instance_info.kernel $} {$ ctrl.node.instance_info.kernel $}
</a> </a>
</dd> </dd>

View File

@ -22,16 +22,20 @@
.controller('IronicNodeListController', IronicNodeListController); .controller('IronicNodeListController', IronicNodeListController);
IronicNodeListController.$inject = [ IronicNodeListController.$inject = [
'$rootScope',
'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.basePath', 'horizon.dashboard.admin.basePath',
'horizon.dashboard.admin.ironic.maintenance.service' 'horizon.dashboard.admin.ironic.maintenance.service',
'horizon.dashboard.admin.ironic.enroll-node.service'
]; ];
function IronicNodeListController(ironic, function IronicNodeListController($rootScope,
ironic,
actions, actions,
basePath, basePath,
maintenanceService) { maintenanceService,
enrollNodeService) {
var ctrl = this; var ctrl = this;
ctrl.nodes = []; ctrl.nodes = [];
@ -43,6 +47,7 @@
ctrl.putNodesInMaintenanceMode = putNodesInMaintenanceMode; ctrl.putNodesInMaintenanceMode = putNodesInMaintenanceMode;
ctrl.removeNodeFromMaintenanceMode = removeNodeFromMaintenanceMode; ctrl.removeNodeFromMaintenanceMode = removeNodeFromMaintenanceMode;
ctrl.removeNodesFromMaintenanceMode = removeNodesFromMaintenanceMode; ctrl.removeNodesFromMaintenanceMode = removeNodesFromMaintenanceMode;
ctrl.enrollNode = enrollNode;
/** /**
* Filtering - client-side MagicSearch * Filtering - client-side MagicSearch
@ -81,6 +86,15 @@
} }
]; ];
// Listen for the creation of new nodes, and update the node list
$rootScope.$on('ironic-ui:new-node', function() {
init();
});
$rootScope.$on('ironic-ui:delete-node-success', function() {
init();
});
init(); init();
// RETRIVE NODES AND PORTS // RETRIVE NODES AND PORTS
@ -124,6 +138,10 @@
function removeNodesFromMaintenanceMode(nodes) { function removeNodesFromMaintenanceMode(nodes) {
maintenanceService.removeNodesFromMaintenanceMode(nodes); maintenanceService.removeNodesFromMaintenanceMode(nodes);
} }
function enrollNode() {
enrollNodeService.modal();
}
} }
})(); })();

View File

@ -15,7 +15,14 @@
</th> </th>
</tr> </tr>
<tr> <tr>
<th colspan="100" class="action-col"> <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>
</th>
<th class="action-col">
<action-list dropdown class="pull-right"> <action-list dropdown class="pull-right">
<action button-type="split-button" <action button-type="split-button"
action-classes="'btn btn-default btn-sm'" action-classes="'btn btn-default btn-sm'"
@ -43,6 +50,13 @@
disabled="tCtrl.selected.length === 0"> disabled="tCtrl.selected.length === 0">
{$ 'Maintenance off' | translate $} {$ 'Maintenance off' | translate $}
</action> </action>
<action button-type="menu-item"
callback="table.actions.deleteNodes"
item="tCtrl.selected"
disabled="tCtrl.selected.length === 0">
<span class="fa fa-trash"></span>
{$ 'Delete nodes' | translate $}
</action>
</menu> </menu>
</action-list> </action-list>
</th> </th>
@ -140,9 +154,15 @@
item="node"> item="node">
{$ 'Maintenance off' | translate $} {$ 'Maintenance off' | translate $}
</action> </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')"
item="node">
<span class="fa fa-trash"></span>
{$ 'Delete node' | translate $}
</action>
</menu> </menu>
</action-list> </action-list>
</td> </td>
</tr> </tr>
</tbody> </tbody>