From e69f2ca4552d4a4740ad82f580da28cd2fc5845e Mon Sep 17 00:00:00 2001 From: Peter Piela Date: Sun, 31 Jan 2016 18:25:56 -0500 Subject: [PATCH 1/2] Added Sean Chen's node-details, long-running service, and widgets.modal.prompt code Fixed minor issues --- .gitignore | 5 + .../util/long-running/long-running.service.js | 85 ++++++++ .../long-running/long-running.service.spec.js | 73 +++++++ horizon/static/framework/util/util.module.js | 1 + .../widgets/modal/prompt/prompt.html | 30 +++ .../widgets/modal/prompt/prompt.service.js | 94 +++++++++ .../modal/prompt/prompt.service.spec.js | 86 ++++++++ openstack_dashboard/api/ironic.py | 11 +- openstack_dashboard/api/rest/ironic.py | 28 +-- .../ironic/templates/ironic/node_detail.html | 1 + .../admin/ironic/node-actions.service.js | 90 +++++++-- .../admin/ironic/node-actions.service.spec.js | 185 ++++++++++++++++++ .../node-details/node-details.controller.js | 84 ++++++++ .../node-details.controller.spec.js | 84 ++++++++ .../ironic/node-details/node-details.html | 45 +++++ .../node-details/sections/configuration.html | 88 +++++++++ .../node-details/sections/overview.html | 46 +++++ .../ironic/node-list/node-list.controller.js | 7 +- .../admin/ironic/node-list/node-list.html | 9 +- .../openstack-service-api/ironic.service.js | 8 +- 20 files changed, 1018 insertions(+), 42 deletions(-) create mode 100644 .gitignore create mode 100644 horizon/static/framework/util/long-running/long-running.service.js create mode 100644 horizon/static/framework/util/long-running/long-running.service.spec.js create mode 100644 horizon/static/framework/widgets/modal/prompt/prompt.html create mode 100644 horizon/static/framework/widgets/modal/prompt/prompt.service.js create mode 100644 horizon/static/framework/widgets/modal/prompt/prompt.service.spec.js create mode 100644 openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-actions.service.spec.js create mode 100644 openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/node-details.controller.js create mode 100644 openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/node-details.controller.spec.js create mode 100644 openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/node-details.html create mode 100644 openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/sections/configuration.html create mode 100644 openstack_dashboard/dashboards/admin/static/dashboard/admin/ironic/node-details/sections/overview.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa80119 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.venv/ +*.pyc +static/dashboard/js/*.js +static/dashboard/css/*.css +openstack_dashboard/local/.secret_key_store \ 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 $} + + + + {$ 'Power off' | translate $} + + + {$ 'Maintenance on' | translate $} + + + {$ 'Maintenance off' | 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 @@ - + 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.')); }); From 470411f4eee72ed866a961cea1d9e990a3740538 Mon Sep 17 00:00:00 2001 From: Peter Piela Date: Sun, 31 Jan 2016 18:31:49 -0500 Subject: [PATCH 2/2] Added .secret_key_store and *.secret_key_store.lock --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index aa80119..bb57c6d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ *.pyc static/dashboard/js/*.js static/dashboard/css/*.css -openstack_dashboard/local/.secret_key_store \ No newline at end of file +*~ +.secret_key_store +*.secret_key_store.lock \ No newline at end of file
- + {$ node.instance_uuid $} {$ node.power_state $} +
+ {$ node.power_state $} +
+
{$ node.provision_state $} {$ node.maintenance $} {$ node.ports.length $}