From df857f00b572c12f9eb29f6c7b8185576d9f8978 Mon Sep 17 00:00:00 2001 From: "wei.ying" Date: Sat, 4 Nov 2017 19:07:32 +0800 Subject: [PATCH] Add angular create server group action This patch adds create server group action for angular server groups panel. Change-Id: Ia4354448dcb42bc5e53e2415084f0569a75a68a3 Partial-Implements: blueprint ng-server-groups --- openstack_dashboard/api/nova.py | 7 ++ openstack_dashboard/api/rest/nova.py | 16 ++++ .../openstack-service-api/nova.service.js | 23 +++++ .../nova.service.spec.js | 10 +++ .../server_groups/actions/actions.module.js | 56 ++++++++++++ .../actions/actions.module.spec.js | 41 +++++++++ .../actions/create.action.service.js | 88 +++++++++++++++++++ .../actions/create.action.service.spec.js | 79 +++++++++++++++++ .../actions/workflow/workflow.service.js | 86 ++++++++++++++++++ .../actions/workflow/workflow.service.spec.js | 51 +++++++++++ .../server_groups/server-groups.module.js | 3 +- .../server_groups/server-groups.service.js | 3 +- .../test/unit/api/rest/test_nova.py | 25 ++++++ .../test/unit/api/test_nova.py | 14 +++ .../bp-ng-server-groups-c60849796a273138.yaml | 1 + 15 files changed, 501 insertions(+), 2 deletions(-) create mode 100644 openstack_dashboard/static/app/core/server_groups/actions/actions.module.js create mode 100644 openstack_dashboard/static/app/core/server_groups/actions/actions.module.spec.js create mode 100644 openstack_dashboard/static/app/core/server_groups/actions/create.action.service.js create mode 100644 openstack_dashboard/static/app/core/server_groups/actions/create.action.service.spec.js create mode 100644 openstack_dashboard/static/app/core/server_groups/actions/workflow/workflow.service.js create mode 100644 openstack_dashboard/static/app/core/server_groups/actions/workflow/workflow.service.spec.js diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 4c4e53daf5..466aa68a0a 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -931,6 +931,13 @@ def server_group_list(request): return novaclient(request).server_groups.list() +@profiler.trace +def server_group_create(request, **kwargs): + microversion = get_microversion(request, "servergroup_soft_policies") + return novaclient(request, version=microversion).server_groups.create( + **kwargs) + + @profiler.trace def service_list(request, binary=None): return novaclient(request).services.list(binary=binary) diff --git a/openstack_dashboard/api/rest/nova.py b/openstack_dashboard/api/rest/nova.py index 5d1d9e353d..799fac0807 100644 --- a/openstack_dashboard/api/rest/nova.py +++ b/openstack_dashboard/api/rest/nova.py @@ -431,6 +431,22 @@ class ServerGroups(generic.View): result = api.nova.server_group_list(request) return {'items': [u.to_dict() for u in result]} + @rest_utils.ajax(data_required=True) + def post(self, request): + """Create a server group. + + Create a server group using parameters supplied in the POST + application/json object. The "name" (string) parameter is required + and the "policies" (array) parameter is required. + + This method returns the new server group object on success. + """ + new_servergroup = api.nova.server_group_create(request, **request.DATA) + return rest_utils.CreatedResponse( + '/api/nova/servergroups/%s' % utils_http.urlquote( + new_servergroup.id), new_servergroup.to_dict() + ) + @urls.register class ServerMetadata(generic.View): diff --git a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js index 0317997da1..9a5da03e68 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js @@ -53,6 +53,7 @@ getServer: getServer, getServers: getServers, getServerGroups: getServerGroups, + createServerGroup: createServerGroup, deleteServer: deleteServer, pauseServer: pauseServer, unpauseServer: unpauseServer, @@ -332,6 +333,28 @@ }); } + /** + * @name createServerGroup + * @description + * Create a new server group. This returns the new server group object on success. + * + * @param {Object} newServerGroup + * The server group to create. + * + * @param {string} newServerGroup.name + * The name of the new server group. Required. + * + * @param {array} newServerGroup.policies + * The policies of the new server group. Required. + * @returns {Object} The result of the API call + */ + function createServerGroup(newServerGroup) { + return apiService.post('/api/nova/servergroups/', newServerGroup) + .error(function () { + toastService.add('error', gettext('Unable to create the server group.')); + }); + } + /* * @name deleteServer * @description diff --git a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.spec.js b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.spec.js index b521d38e99..da4836819b 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.spec.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.spec.js @@ -302,6 +302,16 @@ "path": '/api/nova/servergroups/', "error": 'Unable to retrieve server groups.' }, + { + "func": "createServerGroup", + "method": "post", + "path": "/api/nova/servergroups/", + "data": "new server group", + "error": "Unable to create the server group.", + "testInput": [ + "new server group" + ] + }, { "func": "getExtensions", "method": "get", diff --git a/openstack_dashboard/static/app/core/server_groups/actions/actions.module.js b/openstack_dashboard/static/app/core/server_groups/actions/actions.module.js new file mode 100644 index 0000000000..a6226369ee --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/actions/actions.module.js @@ -0,0 +1,56 @@ +/* + * 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 overview + * @ngname horizon.app.core.server_groups.actions + * + * @description + * Provides all of the actions for server groups. + */ + angular + .module('horizon.app.core.server_groups.actions', [ + 'horizon.framework.conf', + 'horizon.app.core.server_groups' + ]) + .run(registerServerGroupActions); + + registerServerGroupActions.$inject = [ + 'horizon.app.core.server_groups.actions.create.service', + 'horizon.app.core.server_groups.resourceType', + 'horizon.framework.conf.resource-type-registry.service' + ]; + + function registerServerGroupActions( + createService, + serverGroupResourceTypeCode, + registry + ) { + var serverGroupResourceType = registry.getResourceType(serverGroupResourceTypeCode); + + serverGroupResourceType.globalActions + .append({ + id: 'createServerGroupAction', + service: createService, + template: { + type: 'create', + text: gettext('Create Server Group') + } + }); + } + +})(); diff --git a/openstack_dashboard/static/app/core/server_groups/actions/actions.module.spec.js b/openstack_dashboard/static/app/core/server_groups/actions/actions.module.spec.js new file mode 100644 index 0000000000..e5fab3d5a9 --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/actions/actions.module.spec.js @@ -0,0 +1,41 @@ +/* + * 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('server groups actions module', function() { + var registry; + beforeEach(module('horizon.app.core.server_groups.actions')); + + beforeEach(inject(function($injector) { + registry = $injector.get('horizon.framework.conf.resource-type-registry.service'); + })); + + it('registers Create Server Group as a global action', function() { + var actions = registry.getResourceType('OS::Nova::ServerGroup').globalActions; + expect(actionHasId(actions, 'createServerGroupAction')).toBe(true); + }); + + function actionHasId(list, value) { + return list.filter(matchesId).length === 1; + + function matchesId(action) { + return action.id === value; + } + } + + }); + +})(); diff --git a/openstack_dashboard/static/app/core/server_groups/actions/create.action.service.js b/openstack_dashboard/static/app/core/server_groups/actions/create.action.service.js new file mode 100644 index 0000000000..e46564b137 --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/actions/create.action.service.js @@ -0,0 +1,88 @@ +/* + * 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.app.core.server_groups') + .factory('horizon.app.core.server_groups.actions.create.service', createService); + + createService.$inject = [ + 'horizon.app.core.openstack-service-api.nova', + 'horizon.app.core.openstack-service-api.policy', + 'horizon.app.core.server_groups.actions.workflow.service', + 'horizon.app.core.server_groups.resourceType', + 'horizon.framework.widgets.form.ModalFormService', + 'horizon.framework.widgets.toast.service', + 'horizon.framework.util.actions.action-result.service', + 'horizon.framework.util.i18n.gettext' + ]; + + /** + * @ngDoc factory + * @name horizon.app.core.server_groups.actions.create.service + * @Description A service to handle the Create Server Group modal. + */ + function createService( + novaAPI, + policy, + workflow, + resourceType, + modalFormService, + toast, + actionResultService, + gettext + ) { + + var service = { + allowed: allowed, + perform: perform, + submit: submit + }; + + return service; + + ////////////// + + function allowed() { + return policy.ifAllowed( + {rules: [['compute', 'os_compute_api:os-server-groups:create']]}); + } + + function perform() { + var config = workflow.init(); + config.title = gettext("Create Server Group"); + return modalFormService.open(config).then(submit); + } + + function submit(context) { + var data = {name: context.model.name}; + // Nova limits only one policy associated with a server group. + data.policies = [context.model.policy]; + return novaAPI.createServerGroup(data).then(onSuccess); + } + + function onSuccess(response) { + var servergroup = response.data; + toast.add('success', interpolate( + gettext('Server Group %s was successfully created.'), [servergroup.name])); + + return actionResultService.getActionResult() + .created(resourceType, servergroup.id) + .result; + } + + } +})(); diff --git a/openstack_dashboard/static/app/core/server_groups/actions/create.action.service.spec.js b/openstack_dashboard/static/app/core/server_groups/actions/create.action.service.spec.js new file mode 100644 index 0000000000..9c2a66e341 --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/actions/create.action.service.spec.js @@ -0,0 +1,79 @@ +/* + * 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.app.core.server_groups.actions.create.service', function() { + + var $q, $scope, novaAPI, service, modalFormService, policyAPI, resType, toast; + + /////////////////////// + + beforeEach(module('horizon.framework')); + beforeEach(module('horizon.app.core')); + beforeEach(module('horizon.app.core.server_groups')); + + beforeEach(inject(function($injector, _$rootScope_, _$q_) { + $scope = _$rootScope_.$new(); + $q = _$q_; + service = $injector.get('horizon.app.core.server_groups.actions.create.service'); + toast = $injector.get('horizon.framework.widgets.toast.service'); + modalFormService = $injector.get('horizon.framework.widgets.form.ModalFormService'); + novaAPI = $injector.get('horizon.app.core.openstack-service-api.nova'); + policyAPI = $injector.get('horizon.app.core.openstack-service-api.policy'); + resType = $injector.get('horizon.app.core.server_groups.resourceType'); + })); + + it('should check the policy if the user is allowed to create server group', function() { + spyOn(policyAPI, 'ifAllowed').and.callThrough(); + var allowed = service.allowed(); + expect(allowed).toBeTruthy(); + expect(policyAPI.ifAllowed).toHaveBeenCalledWith( + { rules: [['compute', 'os_compute_api:os-server-groups:create']] }); + }); + + it('should open the modal', function() { + spyOn(modalFormService, 'open').and.returnValue($q.defer().promise); + spyOn(novaAPI, 'isFeatureSupported').and.returnValue($q.defer().promise); + + service.perform(); + $scope.$apply(); + + expect(modalFormService.open).toHaveBeenCalled(); + }); + + it('should submit create server group request to nova', function() { + var deferred = $q.defer(); + spyOn(novaAPI, 'createServerGroup').and.returnValue(deferred.promise); + spyOn(toast, 'add').and.callFake(angular.noop); + var handler = jasmine.createSpyObj('handler', ['success']); + + deferred.resolve({data: {name: 'name1', id: '1'}}); + service.submit({model: {name: 'karma', policy: 'affinity'}}).then(handler.success); + + $scope.$apply(); + + expect(novaAPI.createServerGroup).toHaveBeenCalledWith( + {name: 'karma', policies: ['affinity']}); + expect(toast.add).toHaveBeenCalledWith( + 'success', 'Server Group name1 was successfully created.'); + + expect(handler.success).toHaveBeenCalled(); + var result = handler.success.calls.first().args[0]; + expect(result.created).toEqual([{type: resType, id: '1'}]); + }); + + }); +})(); diff --git a/openstack_dashboard/static/app/core/server_groups/actions/workflow/workflow.service.js b/openstack_dashboard/static/app/core/server_groups/actions/workflow/workflow.service.js new file mode 100644 index 0000000000..6474615023 --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/actions/workflow/workflow.service.js @@ -0,0 +1,86 @@ +/* + * 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 factory + * @name horizon.app.core.server_groups.actions.workflow.service + * @ngController + * + * @description + * Workflow for creating server group + */ + angular + .module('horizon.app.core.server_groups.actions') + .factory('horizon.app.core.server_groups.actions.workflow.service', ServerGroupWorkflow); + + ServerGroupWorkflow.$inject = [ + 'horizon.app.core.server_groups.service' + ]; + + function ServerGroupWorkflow(serverGroupsService) { + + var workflow = { + init: init + }; + + function init() { + var schema = { + type: 'object', + properties: { + name: { + title: gettext('Name'), + type: 'string' + }, + policy: { + title: gettext('Policy'), + type: 'string' + } + }, + required: ['name', 'policy'] + }; + + var form = [ + "name", + { + key: "policy", + type: "select", + titleMap: [] + } + ]; + + var model = {}; + + var config = { + schema: schema, + form: form, + model: model + }; + + serverGroupsService.getServerGroupPolicies().then(modifyPolicies); + function modifyPolicies(policies) { + var policyField = config.form[1]; + angular.forEach(policies, function(k, v) { + this.push({name: k, value: v}); + }, policyField.titleMap); + } + + return config; + } + + return workflow; + } +})(); diff --git a/openstack_dashboard/static/app/core/server_groups/actions/workflow/workflow.service.spec.js b/openstack_dashboard/static/app/core/server_groups/actions/workflow/workflow.service.spec.js new file mode 100644 index 0000000000..eceadb2a34 --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/actions/workflow/workflow.service.spec.js @@ -0,0 +1,51 @@ +/* + * 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.app.core.server_groups.actions.workflow.service', function() { + + var $q, $scope, workflow, service; + + beforeEach(module('horizon.framework')); + beforeEach(module('horizon.app.core')); + beforeEach(module('horizon.app.core.server_groups')); + + beforeEach(inject(function($injector, _$rootScope_, _$q_) { + $scope = _$rootScope_.$new(); + $q = _$q_; + workflow = $injector.get('horizon.app.core.server_groups.actions.workflow.service'); + service = $injector.get('horizon.app.core.server_groups.service'); + })); + + function testInitWorkflow() { + var deferred = $q.defer(); + spyOn(service, 'getServerGroupPolicies').and.returnValue(deferred.promise); + deferred.resolve({'a1': 'n1'}); + + var config = workflow.init(); + $scope.$apply(); + + expect(config.schema).toBeDefined(); + expect(config.form).toBeDefined(); + expect(config.model).toBeDefined(); + return config; + } + + it('should be create workflow config for creation', function() { + testInitWorkflow(); + }); + }); +})(); diff --git a/openstack_dashboard/static/app/core/server_groups/server-groups.module.js b/openstack_dashboard/static/app/core/server_groups/server-groups.module.js index 695e43ef01..39a4077c9e 100644 --- a/openstack_dashboard/static/app/core/server_groups/server-groups.module.js +++ b/openstack_dashboard/static/app/core/server_groups/server-groups.module.js @@ -26,7 +26,8 @@ angular .module('horizon.app.core.server_groups', [ 'horizon.framework.conf', - 'horizon.app.core' + 'horizon.app.core', + 'horizon.app.core.server_groups.actions' ]) .constant('horizon.app.core.server_groups.resourceType', 'OS::Nova::ServerGroup') .run(run) diff --git a/openstack_dashboard/static/app/core/server_groups/server-groups.service.js b/openstack_dashboard/static/app/core/server_groups/server-groups.service.js index 5471df200b..c7f90ba5fd 100644 --- a/openstack_dashboard/static/app/core/server_groups/server-groups.service.js +++ b/openstack_dashboard/static/app/core/server_groups/server-groups.service.js @@ -34,7 +34,8 @@ */ function serverGroupsService(nova) { return { - getServerGroupsPromise: getServerGroupsPromise + getServerGroupsPromise: getServerGroupsPromise, + getServerGroupPolicies: getServerGroupPolicies }; /* diff --git a/openstack_dashboard/test/unit/api/rest/test_nova.py b/openstack_dashboard/test/unit/api/rest/test_nova.py index 51a9514f95..fd62884cbb 100644 --- a/openstack_dashboard/test/unit/api/rest/test_nova.py +++ b/openstack_dashboard/test/unit/api/rest/test_nova.py @@ -11,6 +11,8 @@ # 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. + +import json from json import loads as to_json from django.conf import settings @@ -363,6 +365,29 @@ class NovaRestTestCase(test.TestCase): response.json) nc.server_group_list.assert_called_once_with(request) + @test.create_mocks({api.nova: ['server_group_create']}) + def test_server_group_create(self): + req_data = json.dumps({ + 'name': 'server_group', 'policies': ['affinity']}) + + self.mock_server_group_create.return_value = mock.Mock(**{ + 'id': '123', + 'to_dict.return_value': {'id': '123', + 'name': 'server_group', + 'policies': ['affinity']} + }) + + server_group_data = {'name': 'server_group', + 'policies': ['affinity']} + request = self.mock_rest_request(body=req_data) + response = nova.ServerGroups().post(request) + + self.assertStatusCode(response, 201) + self.assertEqual('/api/nova/servergroups/123', response['location']) + + self.mock_server_group_create.assert_called_once_with( + request, **server_group_data) + # # Server Metadata # diff --git a/openstack_dashboard/test/unit/api/test_nova.py b/openstack_dashboard/test/unit/api/test_nova.py index f7310ade12..e1e4e49e95 100644 --- a/openstack_dashboard/test/unit/api/test_nova.py +++ b/openstack_dashboard/test/unit/api/test_nova.py @@ -643,3 +643,17 @@ class ComputeApiTests(test.APIMockTestCase): self.assertIsInstance(ret_val, list) self.assertEqual(len(ret_val), len(server_groups)) novaclient.server_groups.list.assert_called_once_with() + + def test_server_group_create(self): + servergroup = self.server_groups.first() + kwargs = {'name': servergroup.name, 'policies': servergroup.policies} + novaclient = self.stub_novaclient() + self._mock_current_version(novaclient, '2.45') + novaclient.server_groups.create.return_value = servergroup + + ret_val = api.nova.server_group_create(self.request, **kwargs) + + self.assertEqual(servergroup.name, ret_val.name) + self.assertEqual(servergroup.policies, ret_val.policies) + novaclient.versions.get_current.assert_called_once_with() + novaclient.server_groups.create.assert_called_once_with(**kwargs) diff --git a/releasenotes/notes/bp-ng-server-groups-c60849796a273138.yaml b/releasenotes/notes/bp-ng-server-groups-c60849796a273138.yaml index c37627b803..eee7ec7036 100644 --- a/releasenotes/notes/bp-ng-server-groups-c60849796a273138.yaml +++ b/releasenotes/notes/bp-ng-server-groups-c60849796a273138.yaml @@ -6,3 +6,4 @@ features: Project->Compute panel group. The panel turns on if Nova API extension 'ServerGroups' is available. It displays information about server groups. + Supported actions: create.