diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bb57c6d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+.venv/
+*.pyc
+static/dashboard/js/*.js
+static/dashboard/css/*.css
+*~
+.secret_key_store
+*.secret_key_store.lock
\ No newline at end of file
diff --git a/horizon/static/framework/util/long-running/long-running.service.js b/horizon/static/framework/util/long-running/long-running.service.js
new file mode 100644
index 0000000..7ffb042
--- /dev/null
+++ b/horizon/static/framework/util/long-running/long-running.service.js
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015 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';
+
+ angular
+ .module('horizon.framework.util.long-running', [])
+ .factory('horizon.framework.util.long-running.service', serviceFactory);
+
+ /**
+ *
+ ```js
+
+ // inject 'hz.framework.long-running.service' as longRunning here
+
+ var interval = 3000;
+
+ longRunning(function () {
+ return someApi.someMethod(); // returns a promise
+ }, interval)
+
+ // repeats invoke the async long-time-running operation
+ // if it returned with a `response` object that does not
+ // meet a certain condition.
+
+ .until(function (response) {
+ return response.data.foo !== null; // returns boolean
+ })
+
+ // executes this function when the `until` function return `true`,
+ // pass in the same arguments as the one passed to until function.
+
+ .then(function (response) {
+ myDataModel.foo = response.data.foo;
+ });
+
+ ```
+ */
+ serviceFactory.$inject = ['$q', '$timeout'];
+
+ function serviceFactory($q, $timeout) {
+ return longRunning;
+
+ function longRunning(operation, interval) {
+ var conditionFn, deferred = $q.defer();
+ loop();
+ return {
+ until: until
+ };
+
+ function until(func) {
+ conditionFn = func;
+ return deferred.promise;
+ }
+
+ function loop() {
+ $timeout(function () {
+ operation().then(function (data) {
+ if (conditionFn(data)) {
+ deferred.resolve(data);
+ } else {
+ loop();
+ }
+ },
+ loop);
+ }, interval);
+ }
+ }
+ }
+
+})();
diff --git a/horizon/static/framework/util/long-running/long-running.service.spec.js b/horizon/static/framework/util/long-running/long-running.service.spec.js
new file mode 100644
index 0000000..e543977
--- /dev/null
+++ b/horizon/static/framework/util/long-running/long-running.service.spec.js
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2015 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';
+
+ describe('horizon.framework.util.long-running.service', function () {
+ var $q, $timeout, longRunning;
+
+ beforeEach(module('horizon.framework'));
+ beforeEach(inject(function ($injector) {
+ $q = $injector.get('$q');
+ $timeout = $injector.get('$timeout');
+ longRunning = $injector.get('horizon.framework.util.long-running.service');
+ }));
+
+ it('defines the service as a function.', function () {
+ expect(angular.isFunction(longRunning)).toBe(true);
+ });
+
+ it('works properly.', function () {
+ var complete = false;
+ var c = 0;
+
+ longRunning(function () {
+ return $q(function (resolve, reject) {
+ $timeout(function () {
+ if (c++ < 3) {
+ resolve({ data: {} });
+ } else {
+ resolve({ data: { foo: 'bar' } });
+ }
+ }, 200);
+ });
+ }, 3000)
+ .until(function (response) {
+ return response.data.foo === 'bar';
+ })
+ .then(function () {
+ complete = true;
+ });
+
+ $timeout.flush();
+ $timeout.flush();
+ expect(complete).toBe(false);
+
+ $timeout.flush();
+ $timeout.flush();
+ expect(complete).toBe(false);
+
+ $timeout.flush();
+ $timeout.flush();
+ expect(complete).toBe(false);
+
+ $timeout.flush();
+ $timeout.flush();
+ expect(complete).toBe(true);
+ });
+ });
+})();
diff --git a/horizon/static/framework/util/util.module.js b/horizon/static/framework/util/util.module.js
index 30ff44e..baf7042 100755
--- a/horizon/static/framework/util/util.module.js
+++ b/horizon/static/framework/util/util.module.js
@@ -7,6 +7,7 @@
'horizon.framework.util.filters',
'horizon.framework.util.http',
'horizon.framework.util.i18n',
+ 'horizon.framework.util.long-running',
'horizon.framework.util.promise-toggle',
'horizon.framework.util.tech-debt',
'horizon.framework.util.workflow',
diff --git a/horizon/static/framework/widgets/modal/prompt/prompt.html b/horizon/static/framework/widgets/modal/prompt/prompt.html
new file mode 100644
index 0000000..48406ca
--- /dev/null
+++ b/horizon/static/framework/widgets/modal/prompt/prompt.html
@@ -0,0 +1,30 @@
+
+
+
+
+
diff --git a/horizon/static/framework/widgets/modal/prompt/prompt.service.js b/horizon/static/framework/widgets/modal/prompt/prompt.service.js
new file mode 100644
index 0000000..feea7ac
--- /dev/null
+++ b/horizon/static/framework/widgets/modal/prompt/prompt.service.js
@@ -0,0 +1,94 @@
+/*
+ * © Copyright 2015 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.framework.widgets.modal.prompt
+ *
+ * @description
+ * Emulating browser's window.prompt function to provide a popup
+ * dialog for user to input data.
+ *
+ * @param {object} params, the specification object for the popup dialog.
+ * @param {object} callback, the callback function with user input.
+ *
+ * @example:
+ ```js
+
+ prompt({
+ text: {
+ title: gettext('Please input data'),
+ submit: gettext('Ok'),
+ cancel: gettext('Cancel'),
+ placeholder: gettext('Input data')
+ },
+ required: true
+ },
+ function (input) {
+ // ...
+ });
+
+ ```
+ */
+
+ angular
+ .module('horizon.framework.widgets.modal')
+ .factory('horizon.framework.widgets.modal.prompt', serviceFactory)
+ .controller('horizon.framework.widgets.modal.PromptController', PromptController);
+
+ serviceFactory.$inject = [
+ '$modal',
+ 'horizon.framework.widgets.basePath'
+ ];
+
+ function serviceFactory($modal, basePath) {
+ return prompt;
+
+ function prompt(params, callback) {
+ $modal.open({
+ controller: 'horizon.framework.widgets.modal.PromptController',
+ templateUrl: basePath + 'modal/prompt/prompt.html',
+ resolve: {
+ context: function() {
+ params.callback = callback;
+ return params;
+ }
+ }
+ });
+ }
+ }
+
+ PromptController.$inject = ['$scope', '$modalInstance', 'context'];
+
+ function PromptController($scope, $modalInstance, context) {
+ $scope.submit = submit;
+ $scope.cancel = cancel;
+ $scope.context = context || {};
+
+ function submit(input) {
+ context.callback(input);
+ $modalInstance.close();
+ }
+
+ function cancel() {
+ $modalInstance.dismiss('cancel');
+ }
+ }
+
+})();
diff --git a/horizon/static/framework/widgets/modal/prompt/prompt.service.spec.js b/horizon/static/framework/widgets/modal/prompt/prompt.service.spec.js
new file mode 100644
index 0000000..bd0bd6b
--- /dev/null
+++ b/horizon/static/framework/widgets/modal/prompt/prompt.service.spec.js
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2015 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';
+
+ describe('prompt service', function () {
+ var prompt, scope, ctrl, modalInstance, context;
+
+ beforeEach(module('templates'));
+ beforeEach(module('ui.bootstrap'));
+ beforeEach(module('horizon.framework'));
+
+ beforeEach(inject(function ($injector, $controller) {
+ prompt = $injector.get('horizon.framework.widgets.modal.prompt');
+ scope = $injector.get('$rootScope').$new();
+
+ modalInstance = {
+ close: function () {},
+ dismiss: function () {}
+ };
+
+ context = {
+ callback: function (input) {}
+ };
+
+ ctrl = $controller('horizon.framework.widgets.modal.PromptController', {
+ $scope: scope,
+ $modalInstance: modalInstance,
+ context: context
+ });
+ }));
+
+ it ('is defined as function', function () {
+ expect(angular.isFunction(prompt)).toBe(true);
+ });
+
+ it('establishes a controller', function () {
+ expect(ctrl).toBeDefined();
+ });
+
+ it('sets context on the scope', function () {
+ expect(scope.context).toBeDefined();
+ });
+
+ it('sets action functions', function () {
+ expect(scope.submit).toBeDefined();
+ expect(scope.cancel).toBeDefined();
+ });
+
+ it('makes submit invoke callback function', function () {
+ expect(scope.submit).toBeDefined();
+ spyOn(context, 'callback');
+ scope.submit('something');
+ expect(context.callback).toHaveBeenCalledWith('something');
+ });
+
+ it('makes submit close the modal instance', function () {
+ expect(scope.submit).toBeDefined();
+ spyOn(modalInstance, 'close');
+ scope.submit('something');
+ expect(modalInstance.close.calls.count()).toBe(1);
+ });
+
+ it('makes cancel close the modal instance', function () {
+ expect(scope.cancel).toBeDefined();
+ spyOn(modalInstance, 'dismiss');
+ scope.cancel();
+ expect(modalInstance.dismiss).toHaveBeenCalledWith('cancel');
+ });
+ });
+
+})();
diff --git a/openstack_dashboard/api/ironic.py b/openstack_dashboard/api/ironic.py
index 74d6c74..5a2b955 100755
--- a/openstack_dashboard/api/ironic.py
+++ b/openstack_dashboard/api/ironic.py
@@ -26,6 +26,7 @@
LOG = logging.getLogger(__name__)
+DEFAULT_IRONIC_API_VERSION = '1.6'
DEFAULT_INSECURE = False
DEFAULT_CACERT = None
@@ -40,7 +41,9 @@ def ironicclient(request):
cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', DEFAULT_CACERT)
ironic_url = base.url_for(request, IRONIC_CLIENT_CLASS_NAME)
- return client.Client(1, ironic_url,
+ return client.Client(1,
+ ironic_url,
+ os_ironic_api_version=DEFAULT_IRONIC_API_VERSION,
project_id=request.user.project_id,
token=request.user.token.id,
insecure=insecure,
@@ -96,7 +99,7 @@ def node_set_power_state(request, node_id, state):
return ironicclient(request).node.set_power_state(node_id, state)
-def node_set_maintenance(request, node_id, state):
+def node_set_maintenance(request, node_id, state, maint_reason=None):
"""Set the maintenance mode on a given node.
:param request: HTTP request.
@@ -106,4 +109,6 @@ def node_set_maintenance(request, node_id, state):
http://docs.openstack.org/developer/python-ironicclient/api/ironicclient.v1.node.html#ironicclient.v1.node.NodeManager.set_maintenance
"""
- return ironicclient(request).node.set_maintenance(node_id, state)
+ return ironicclient(request).node.set_maintenance(node_id,
+ state,
+ maint_reason=maint_reason)
diff --git a/openstack_dashboard/api/rest/ironic.py b/openstack_dashboard/api/rest/ironic.py
index d67e3fa..537bafa 100755
--- a/openstack_dashboard/api/rest/ironic.py
+++ b/openstack_dashboard/api/rest/ironic.py
@@ -12,8 +12,6 @@
# 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.
-"""API over the ironic service.
-"""
from django.views import generic
@@ -24,27 +22,21 @@
@urls.register
class Nodes(generic.View):
- """API for ironic nodes.
- """
url_regex = r'ironic/nodes/$'
@rest_utils.ajax()
def get(self, request):
"""Retrieve a list of nodes.
-
- The listing result is an object with property "items".
"""
- result = api.ironic.node_list(request)
+ items = api.ironic.node_list(request)
return {
- 'items': [i.to_dict() for i in result],
+ 'items': [i.to_dict() for i in items],
}
@urls.register
class Node(generic.View):
- """API for ironic node.
- """
url_regex = r'ironic/nodes/(?P[0-9a-f-]+)$'
@@ -57,8 +49,6 @@ def get(self, request, node_id):
@urls.register
class Ports(generic.View):
- """API for ironic ports.
- """
url_regex = r'ironic/ports/$'
@@ -67,16 +57,14 @@ def get(self, request):
"""Get a list of ports associated with a given node.
"""
node_id = request.GET.get('node_id')
- result = api.ironic.node_list_ports(request, node_id)
+ items = api.ironic.node_list_ports(request, node_id)
return {
- 'items': [i.to_dict() for i in result],
+ 'items': [i.to_dict() for i in items],
}
@urls.register
class StatesPower(generic.View):
- """API for power state of a node.
- """
url_regex = r'ironic/nodes/(?P[0-9a-f-]+)/states/power$'
@@ -90,14 +78,16 @@ def patch(self, request, node_id):
@urls.register
class Maintenance(generic.View):
- """API for maintenance state of a node.
- """
url_regex = r'ironic/nodes/(?P[0-9a-f-]+)/maintenance$'
@rest_utils.ajax()
def patch(self, request, node_id):
- return api.ironic.node_set_maintenance(request, node_id, 'on')
+ maint_reason = request.DATA.get('maint_reason')
+ return api.ironic.node_set_maintenance(request,
+ node_id,
+ 'on',
+ maint_reason=maint_reason)
@rest_utils.ajax()
def delete(self, request, node_id):
diff --git a/openstack_dashboard/dashboards/admin/ironic/templates/ironic/node_detail.html b/openstack_dashboard/dashboards/admin/ironic/templates/ironic/node_detail.html
index 3fb6c60..48c8c2c 100755
--- a/openstack_dashboard/dashboards/admin/ironic/templates/ironic/node_detail.html
+++ b/openstack_dashboard/dashboards/admin/ironic/templates/ironic/node_detail.html
@@ -7,4 +7,5 @@
{% endblock %}
{% block main %}
+
{% endblock %}
diff --git a/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-actions.service.js b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-actions.service.js
index 3ac7f96..df0fb15 100755
--- a/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-actions.service.js
+++ b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-actions.service.js
@@ -1,5 +1,6 @@
/*
- * © Copyright 2015 Hewlett Packard Enterprise Development Company LP
+ * Copyright 2015 Hewlett Packard Enterprise Development Company LP
+ * 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.
@@ -24,19 +25,25 @@
.factory('horizon.dashboard.admin.ironic.actions', actions);
actions.$inject = [
- 'horizon.app.core.openstack-service-api.ironic'
+ 'horizon.app.core.openstack-service-api.ironic',
+ 'horizon.framework.util.long-running.service',
+ 'horizon.framework.widgets.modal.prompt'
];
- function actions(ironic) {
+ function actions(ironic, longRunning, prompt) {
var service = {
powerOn: powerOn,
powerOff: powerOff,
powerOnAll: powerOnAll,
powerOffAll: powerOffAll,
- putNodeInMaintenanceMode: putNodeInMaintenanceMode,
+ promptForPutNodeInMaintenanceMode: promptForPutNodeInMaintenanceMode,
+ putNodeInMaintenanceMode: putNodeInMaintenanceMode, // expose for testing
removeNodeFromMaintenanceMode: removeNodeFromMaintenanceMode,
- putAllInMaintenanceMode: putAllInMaintenanceMode,
- removeAllFromMaintenanceMode: removeAllFromMaintenanceMode
+ promptForPutAllInMaintenanceMode: promptForPutAllInMaintenanceMode,
+ putAllInMaintenanceMode: putAllInMaintenanceMode, // expose for testing
+ removeAllFromMaintenanceMode: removeAllFromMaintenanceMode,
+ updateNode: updateNode,
+ operateAll: operateAll // expose for testing
};
return service;
@@ -49,7 +56,9 @@
}
node.power_state = null;
node.target_power_state = POWER_STATE_ON;
- return ironic.powerOnNode(node.uuid);
+ ironic.powerOnNode(node.uuid).then(function () {
+ updateNode(node);
+ });
}
function powerOff(node) {
@@ -58,27 +67,60 @@
}
node.power_state = null;
node.target_power_state = POWER_STATE_OFF;
- return ironic.powerOffNode(node.uuid);
+ ironic.powerOffNode(node.uuid).then(function () {
+ updateNode(node);
+ });
+ }
+
+ function updateNode(node) {
+ longRunning(function () {
+ return ironic.getNode(node.uuid);
+ }, 3000)
+ .until(function (response) {
+ return response.data.target_power_state === null;
+ })
+ .then(
+ function (response) {
+ node.power_state = response.data.power_state;
+ node.target_power_state = null;
+ }
+ );
}
function powerOnAll(selected) {
- operateAll(powerOn, selected);
+ service.operateAll(service.powerOn, selected);
}
function powerOffAll(selected) {
- operateAll(powerOff, selected);
+ service.operateAll(service.powerOff, selected);
}
// maintenance
- function putNodeInMaintenanceMode(node) {
+ function promptForPutNodeInMaintenanceMode(node) {
+ prompt({
+ text: {
+ title: gettext('(Optional) Why is this node under maintenance?'),
+ submit: gettext('Submit'),
+ cancel: gettext('Cancel'),
+ placeholder: gettext('Maintenance reason')
+ },
+ maxlength: 256
+ },
+ function (reason) {
+ putNodeInMaintenanceMode(node, reason);
+ });
+ }
+
+ function putNodeInMaintenanceMode(node, reason) {
if (node.maintenance !== false) {
return;
}
node.maintenance = null;
- ironic.putNodeInMaintenanceMode(node.uuid).then(
+ ironic.putNodeInMaintenanceMode(node.uuid, reason).then(
function () {
node.maintenance = true;
+ node.maintenance_reason = reason;
}
);
}
@@ -91,16 +133,34 @@
ironic.removeNodeFromMaintenanceMode(node.uuid).then(
function () {
node.maintenance = false;
+ node.maintenance_reason = null;
}
);
}
- function putAllInMaintenanceMode(selected) {
- operateAll(putNodeInMaintenanceMode, selected);
+ function promptForPutAllInMaintenanceMode(selected) {
+ prompt({
+ text: {
+ title: gettext('(Optional) Why are these nodes under maintenance?'),
+ submit: gettext('Submit'),
+ cancel: gettext('Cancel'),
+ placeholder: gettext('Maintenance reason')
+ },
+ maxlength: 256
+ },
+ function (reason) {
+ putAllInMaintenanceMode(selected, reason);
+ });
+ }
+
+ function putAllInMaintenanceMode(selected, reason) {
+ service.operateAll(function (node) {
+ service.putNodeInMaintenanceMode(node, reason);
+ }, selected);
}
function removeAllFromMaintenanceMode(selected) {
- operateAll(removeNodeFromMaintenanceMode, selected);
+ service.operateAll(service.removeNodeFromMaintenanceMode, selected);
}
function operateAll(fn, selected) {
diff --git a/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-actions.service.spec.js b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-actions.service.spec.js
new file mode 100644
index 0000000..4182cda
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-actions.service.spec.js
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2015 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';
+
+ describe('horizon.dashboard.admin.ironic.node-actions', function () {
+ var nodeActions, ironic, longRunningService, nodes, longRunningMock, prompt, promptMock, selected;
+
+ beforeEach(function () {
+ longRunningMock = jasmine.createSpy('longRunningMock')
+ .and
+ .returnValue({
+ until: function () {
+ return {
+ then: function () {}
+ };
+ }
+ });
+ promptMock = jasmine.createSpy('promptMock');
+
+ module('templates');
+ module('horizon.framework.widgets');
+ module('horizon.app.core.openstack-service-api');
+ module('horizon.dashboard.admin.ironic');
+ module(function ($provide) {
+ $provide.value('hz.framework.long-running.service', longRunningMock);
+ $provide.value('horizon.framework.widgets.modal.prompt', promptMock);
+ });
+ });
+
+ beforeEach(inject(function ($injector) {
+ nodeActions = $injector.get('horizon.dashboard.admin.ironic.actions');
+ spyOn(nodeActions, 'operateAll').and.callThrough();
+
+ ironic = $injector.get('hz.api.ironic');
+ spyOn(ironic, 'getNodes').and.returnValue({ then: function () {} });
+ spyOn(ironic, 'getNode').and.returnValue({ then: function () {} });
+ spyOn(ironic, 'getPortsWithNode').and.returnValue({ then: function () {} });
+ spyOn(ironic, 'putNodeInMaintenanceMode').and.returnValue({ then: function () {} });
+ spyOn(ironic, 'removeNodeFromMaintenanceMode').and.returnValue({ then: function () {} });
+ spyOn(ironic, 'powerOnNode').and.returnValue({ then: function () {} });
+ spyOn(ironic, 'powerOffNode').and.returnValue({ then: function () {} });
+
+ longRunningService = $injector.get('hz.framework.long-running.service');
+ prompt = $injector.get('horizon.framework.widgets.modal.prompt');
+
+ nodes = [
+ { id: 123, uuid: 123, power_state: 'power off' },
+ { id: 456, uuid: 456, power_state: 'power on' }
+ ];
+
+ selected = {
+ '123': {
+ item: nodes[0],
+ checked: true
+ },
+ '456': {
+ item: nodes[1],
+ checked: true
+ }
+ };
+
+ }));
+
+ it('should be defined.', function () {
+ expect(nodeActions).toBeDefined();
+ });
+
+ it('should have API `powerOn` defined and working.', function () {
+ expect(nodeActions.powerOn).toBeDefined();
+
+ nodeActions.powerOn(nodes[1]);
+ expect(ironic.powerOnNode).not.toHaveBeenCalledWith(nodes[1].uuid);
+
+ nodeActions.powerOn(nodes[0]);
+ expect(ironic.powerOnNode).toHaveBeenCalledWith(nodes[0].uuid);
+ expect(nodes[0].power_state).toBe(null);
+ expect(nodes[0].target_power_state).toBe('power on');
+ });
+
+ it('should have API `powerOff` defined and working.', function () {
+ expect(nodeActions.powerOff).toBeDefined();
+
+ nodeActions.powerOff(nodes[0]);
+ expect(ironic.powerOffNode).not.toHaveBeenCalledWith(nodes[0].uuid);
+
+ nodeActions.powerOff(nodes[1]);
+ expect(ironic.powerOffNode).toHaveBeenCalledWith(nodes[1].uuid);
+ expect(nodes[1].power_state).toBe(null);
+ expect(nodes[1].target_power_state).toBe('power off');
+ });
+
+ it('should have API `powerOnAll` defined and working.', function () {
+ spyOn(nodeActions, 'powerOn');
+ expect(nodeActions.powerOnAll).toBeDefined();
+ nodeActions.powerOnAll(selected);
+ expect(nodeActions.operateAll).toHaveBeenCalled();
+ expect(nodeActions.powerOn.calls.count()).toEqual(2);
+ });
+
+ it('should have API `powerOffAll` defined and working.', function () {
+ spyOn(nodeActions, 'powerOff');
+ expect(nodeActions.powerOffAll).toBeDefined();
+ nodeActions.powerOffAll(selected);
+ expect(nodeActions.operateAll).toHaveBeenCalled();
+ expect(nodeActions.powerOff.calls.count()).toEqual(2);
+ });
+
+ it('should have API `promptForPutNodeInMaintenanceMode` defined and working.', function () {
+ expect(nodeActions.promptForPutNodeInMaintenanceMode).toBeDefined();
+ nodeActions.promptForPutNodeInMaintenanceMode(nodes[0]);
+ expect(prompt).toHaveBeenCalled();
+ });
+
+ it('should have API `putNodeInMaintenanceMode` defined and working.', function () {
+ expect(nodeActions.putNodeInMaintenanceMode).toBeDefined();
+
+ var node = nodes[0];
+ node.maintenance = true;
+ nodeActions.putNodeInMaintenanceMode(node);
+ expect(ironic.putNodeInMaintenanceMode).not.toHaveBeenCalled();
+
+ node.maintenance = false;
+ nodeActions.putNodeInMaintenanceMode(node, 'some reason');
+ expect(ironic.putNodeInMaintenanceMode).toHaveBeenCalledWith(node.uuid, 'some reason');
+ expect(node.maintenance).toBe(null);
+ });
+
+ it('should have API `removeNodeFromMaintenanceMode` defined and working.', function () {
+ expect(nodeActions.removeNodeFromMaintenanceMode).toBeDefined();
+
+ var node = nodes[0];
+ node.maintenance = false;
+ nodeActions.removeNodeFromMaintenanceMode(node);
+ expect(ironic.removeNodeFromMaintenanceMode).not.toHaveBeenCalled();
+
+ node.maintenance = true;
+ nodeActions.removeNodeFromMaintenanceMode(node);
+ expect(ironic.removeNodeFromMaintenanceMode).toHaveBeenCalledWith(node.uuid);
+ expect(node.maintenance).toBe(null);
+ });
+
+ it('should have API `promptForPutAllInMaintenanceMode` defined and working.', function () {
+ expect(nodeActions.promptForPutNodeInMaintenanceMode).toBeDefined();
+ nodeActions.promptForPutAllInMaintenanceMode(nodes);
+ expect(prompt).toHaveBeenCalled();
+ });
+
+ it('should have API `putAllInMaintenanceMode` defined and working.', function () {
+ spyOn(nodeActions, 'putNodeInMaintenanceMode');
+ expect(nodeActions.putAllInMaintenanceMode).toBeDefined();
+ nodeActions.putAllInMaintenanceMode(selected);
+ expect(nodeActions.operateAll).toHaveBeenCalled();
+ expect(nodeActions.putNodeInMaintenanceMode.calls.count()).toEqual(2);
+ });
+
+ it('should have API `removeAllFromMaintenanceMode` defined and working.', function () {
+ spyOn(nodeActions, 'removeNodeFromMaintenanceMode');
+ expect(nodeActions.removeAllFromMaintenanceMode).toBeDefined();
+ nodeActions.removeAllFromMaintenanceMode(selected);
+ expect(nodeActions.operateAll).toHaveBeenCalled();
+ expect(nodeActions.removeNodeFromMaintenanceMode.calls.count()).toEqual(2);
+ });
+
+ it('should have API `updateNode` defined and working.', function () {
+ expect(nodeActions.updateNode).toBeDefined();
+ nodeActions.updateNode(nodes[0]);
+ expect(longRunningService).toHaveBeenCalled();
+ });
+ });
+
+})();
diff --git a/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/node-details.controller.js b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/node-details.controller.js
new file mode 100644
index 0000000..6759391
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/node-details.controller.js
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2015 Hewlett Packard Enterprise Development Company LP
+ * 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';
+
+ angular
+ .module('horizon.dashboard.admin.ironic')
+ .controller('horizon.dashboard.admin.ironic.NodeDetailsController',
+ IronicNodeDetailsController);
+
+ IronicNodeDetailsController.$inject = [
+ '$scope',
+ '$location',
+ 'horizon.app.core.openstack-service-api.ironic',
+ 'horizon.dashboard.admin.ironic.actions',
+ 'horizon.dashboard.admin.basePath'
+ ];
+
+ function IronicNodeDetailsController($scope, $location, ironic, actions, basePath) {
+ var ctrl = this;
+ var path = basePath + 'ironic/node-details/sections/';
+
+ $scope.None = gettext('None');
+ $scope.actions = actions;
+
+ ctrl.sections = [
+ {
+ heading: gettext('Overview'),
+ templateUrl: path + 'overview.html'
+ },
+ {
+ heading: gettext('Configuration'),
+ templateUrl: path + 'configuration.html'
+ }
+ ];
+
+ console.log("sections = " + ctrl.sections.map(function(item) {return item['templateUrl'];}))
+ ctrl.basePath = basePath;
+ ctrl.init = init;
+
+ ///////////////
+
+ function init() {
+ // Fetch the Node ID from the URL.
+ var pattern = /(.*\/admin\/ironic\/)(.+)\/(detail)?/;
+ var uuid = $location.absUrl().match(pattern)[2];
+
+ retrieveNode(uuid).then(function () {
+ retrievePorts(uuid);
+ });
+ }
+
+ function retrieveNode(uuid) {
+ return ironic.getNode(uuid).then(function (response) {
+ var node = response.data;
+ ctrl.node = $scope.node = node;
+ if (node['target_power_state']) {
+ actions.updateNode(node);
+ }
+ });
+ }
+
+ function retrievePorts(node_id) {
+ return ironic.getPortsWithNode(node_id).then(function (response) {
+ ctrl.ports = response.data.items;
+ });
+ }
+ }
+
+})();
diff --git a/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/node-details.controller.spec.js b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/node-details.controller.spec.js
new file mode 100644
index 0000000..1a1bc43
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/node-details.controller.spec.js
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2015 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';
+
+ describe('horizon.dashboard.admin.ironic.node-details', function () {
+ var scope, ctrl, $location;
+
+ beforeEach(module('horizon.dashboard.admin.ironic'));
+
+ beforeEach(inject(function ($controller, $rootScope, $q, _$location_) {
+ var basePath = '/static';
+
+ var getNode = function (uuid) {
+ var node = {name: 'herp', uuid: uuid};
+ var deferred = $q.defer();
+ deferred.resolve({data: node});
+ return deferred.promise;
+ };
+
+ var getPortsWithNode = function (uuid) {
+ var ports = [
+ uuid + '-port1',
+ uuid + '-port2'
+ ];
+
+ var deferred = $q.defer();
+ deferred.resolve({data: {items: ports}});
+ return deferred.promise;
+ };
+
+ scope = $rootScope.$new();
+ $location = _$location_;
+ $location.path('http://localhost/admin/ironic/1234/');
+
+ ctrl = $controller('horizon.dashboard.admin.ironic.NodeDetailsController', {
+ $scope: scope,
+ $location: $location,
+ 'hz.api.ironic': {
+ getNode: getNode,
+ getPortsWithNode: getPortsWithNode
+ },
+ 'horizon.dashboard.admin.ironic.actions': {},
+ 'horizon.dashboard.admin.basePath': basePath
+ });
+
+ scope.$apply();
+ }));
+
+ it('should be defined', function () {
+ expect(ctrl).toBeDefined();
+ });
+
+ it('should have a basePath', function () {
+ expect(ctrl.basePath).toBeDefined();
+ expect(ctrl.basePath).toEqual('/static');
+ });
+
+ it('should have a node', function () {
+ expect(ctrl.node).toBeDefined();
+ expect(ctrl.node).toEqual({name: 'herp', uuid: '1234'})
+ });
+
+ it('should have ports', function () {
+ expect(ctrl.ports).toBeDefined();
+ expect(ctrl.ports).toEqual(['1234-port1', '1234-port2']);
+ });
+
+ });
+
+})();
diff --git a/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/node-details.html b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/node-details.html
new file mode 100644
index 0000000..264f8f2
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/node-details.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+ {$ 'Power on' | translate $}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/sections/configuration.html b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/sections/configuration.html
new file mode 100644
index 0000000..a9a6249
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/sections/configuration.html
@@ -0,0 +1,88 @@
+
+
+
+
+
General
+
+
+ - ID
+ - {$ ctrl.node['uuid'] || None $}
+ - Chassis ID
+ - {$ ctrl.node['chassis_uuid'] || None $}
+ - Created At
+ - {$ ctrl.node['created_at'] || None $}
+ - Extra
+ - {$ ctrl.node['extra'] || None $}
+
+
+
+
+
+
Ports
+
+
+ -
+ {$ 'MAC ' + (1 + $index) $}
+ -
+ {$ port['address'] $}
+
+
+
+
+
+
+
+
+
Properties
+
+
+ - Memory
+ - {$ ctrl.node['properties']['memory_mb'] + ' MB' $}
+ - CPU Arch
+ - {$ ctrl.node['properties']['cpu_arch'] $}
+ - Local GB
+ - {$ ctrl.node['properties']['local_gb'] $}
+ - CPUs
+ - {$ ctrl.node['properties']['cpus'] $}
+
+
+
+
+
+
Driver Info
+
+
+ - Driver
+ - {$ ctrl.node['driver'] $}
+ - CPU Arch
+ - {$ ctrl.node['driver_info']['ssh_port'] $}
+ - SSH Username
+ - {$ ctrl.node['driver_info']['ssh_username'] $}
+ - Deploy Kernel
+ - {$ ctrl.node['driver_info']['deploy_kernel'] $}
+ - Deploy Ramdisk
+ - {$ ctrl.node['driver_info']['deploy_ramdisk'] $}
+
+
+
+
+
+
+
+
Capabilities
+
+
+ - {$ ctrl.node['capabilities'] || None $}
+
+
+
+
+
+
Instance Info
+
+
+ - Name
+ - {$ ctrl.node['instance_info']['instance_uuid'] || None $}
+
+
+
diff --git a/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/sections/overview.html b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/sections/overview.html
new file mode 100644
index 0000000..4e49b55
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/sections/overview.html
@@ -0,0 +1,46 @@
+
+
+
+
+
General
+
+
+ - Name
+ - {$ ctrl.node['name'] || None $}
+ - Maintenance
+ - {$ ctrl.node['maintenance'] ? 'True' : 'False' $}
+ - Maintenance Reason
+ - {$ ctrl.node['maintenance_reason'] || None $}
+ - Inspection Started At
+ - {$ ctrl.node['inspection_started_at'] || None $}
+ - Inspection Finished At
+ - {$ ctrl.node['inspection_finished_at'] || None $}
+ - Reservation
+ - {$ ctrl.node['reservation'] || None $}
+ - Console Enabled
+ - {$ ctrl.node['console_enabled'] ? 'True' : 'False' $}
+
+
+
+
+
+
Provisioning Status
+
+
+ - Instance UUID
+ - {$ ctrl.node['instance_uuid'] || None $}
+ - Power State
+ - {$ ctrl.node['power_state'] $}
+ - Target Power State
+ - {$ ctrl.node['target_power_state'] || None $}
+ - Provision State
+ - {$ ctrl.node['provision_state'] $}
+ - Target Provision State
+ - {$ ctrl.node['target_provision_state'] || None $}
+ - Last Error
+ - {$ ctrl.node['last_error'] || None $}
+ - Updated At
+ - {$ ctrl.node['updated_at'] $}
+
+
+
diff --git a/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-list/node-list.controller.js b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-list/node-list.controller.js
index 1271cab..d36c527 100755
--- a/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-list/node-list.controller.js
+++ b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-list/node-list.controller.js
@@ -48,7 +48,7 @@
}
});
- init();
+ ctrl.init = init;
///////////////
@@ -57,14 +57,19 @@
}
function retrieveNodes() {
+ console.log("node-list.controller: Get nodes")
return ironic.getNodes().then(onGetNotes);
}
function onGetNotes(response) {
+ console.log("node-list.controller: onGetNodes")
$scope.nodes = ctrl.nodesSrc = response.data.items;
ctrl.nodesSrc.forEach(function (node) {
node.id = node.uuid;
retrievePorts(node);
+ if (node['target_power_state']) {
+ actions.updateNode(node);
+ }
});
}
diff --git a/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-list/node-list.html b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-list/node-list.html
index cf417aa..f3923f9 100755
--- a/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-list/node-list.html
+++ b/openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-list/node-list.html
@@ -1,4 +1,5 @@
|
-
+
{$ node.instance_uuid $}
|
- {$ node.power_state $} |
+
+
+ {$ node.power_state $}
+
+ |
{$ node.provision_state $} |
{$ node.maintenance $} |
{$ node.ports.length $} |
diff --git a/openstack_dashboard/static/app/core/openstack-service-api/ironic.service.js b/openstack_dashboard/static/app/core/openstack-service-api/ironic.service.js
index dade40a..ce943e7 100755
--- a/openstack_dashboard/static/app/core/openstack-service-api/ironic.service.js
+++ b/openstack_dashboard/static/app/core/openstack-service-api/ironic.service.js
@@ -57,6 +57,7 @@
*/
function getNodes() {
+ console.log("openstack_dashbord/static/app/core/openstack-service-api/ironic-service.js:getNodes")
return apiService.get('/api/ironic/nodes/')
.error(function() {
toastService.add('error', gettext('Unable to retrieve Ironic nodes.'));
@@ -107,8 +108,11 @@
* @param {string} uuid – UUID or logical name of a node.
*/
- function putNodeInMaintenanceMode(uuid) {
- return apiService.patch('/api/ironic/nodes/' + uuid + '/maintenance').error(function() {
+ function putNodeInMaintenanceMode(uuid, reason) {
+ var data = {
+ maint_reason: reason
+ };
+ return apiService.patch('/api/ironic/nodes/' + uuid + '/maintenance', data).error(function() {
toastService.add('error',
gettext('Unable to put the Ironic node in maintenance mode.'));
});