diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 466aa68a0a..22381c2348 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -938,6 +938,11 @@ def server_group_create(request, **kwargs): **kwargs) +@profiler.trace +def server_group_delete(request, servergroup_id): + novaclient(request).server_groups.delete(servergroup_id) + + @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 799fac0807..7d47da3b23 100644 --- a/openstack_dashboard/api/rest/nova.py +++ b/openstack_dashboard/api/rest/nova.py @@ -448,6 +448,19 @@ class ServerGroups(generic.View): ) +@urls.register +class ServerGroup(generic.View): + url_regex = r'nova/servergroups/(?P[^/]+)/$' + + @rest_utils.ajax() + def delete(self, request, servergroup_id): + """Delete a specific server group + + DELETE http://localhost/api/nova/servergroups/ + """ + api.nova.server_group_delete(request, servergroup_id) + + @urls.register class ServerMetadata(generic.View): """API for server metadata.""" 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 9a5da03e68..2253596919 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 @@ -54,6 +54,7 @@ getServers: getServers, getServerGroups: getServerGroups, createServerGroup: createServerGroup, + deleteServerGroup: deleteServerGroup, deleteServer: deleteServer, pauseServer: pauseServer, unpauseServer: unpauseServer, @@ -355,6 +356,28 @@ }); } + /** + * @name deleteServerGroup + * @description + * Delete a single server group by ID. + * + * @param {String} serverGroupId + * Server Group to delete + * + * @param {boolean} suppressError + * If passed in, this will not show the default error handling + * (horizon alert). + * + * @returns {Object} The result of the API call + */ + function deleteServerGroup(serverGroupId, suppressError) { + var promise = apiService.delete('/api/nova/servergroups/' + serverGroupId + '/'); + return suppressError ? promise : promise.error(function() { + var msg = gettext('Unable to delete the server group with id %(id)s'); + toastService.add('error', interpolate(msg, { id: serverGroupId }, true)); + }); + } + /* * @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 da4836819b..418021cc7e 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 @@ -312,6 +312,13 @@ "new server group" ] }, + { + "func": "deleteServerGroup", + "method": "delete", + "path": "/api/nova/servergroups/1/", + "error": "Unable to delete the server group with id 1", + "testInput": [1] + }, { "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 index a6226369ee..a1105e7f89 100644 --- a/openstack_dashboard/static/app/core/server_groups/actions/actions.module.js +++ b/openstack_dashboard/static/app/core/server_groups/actions/actions.module.js @@ -31,17 +31,39 @@ registerServerGroupActions.$inject = [ 'horizon.app.core.server_groups.actions.create.service', + 'horizon.app.core.server_groups.actions.delete.service', 'horizon.app.core.server_groups.resourceType', 'horizon.framework.conf.resource-type-registry.service' ]; function registerServerGroupActions( createService, + deleteService, serverGroupResourceTypeCode, registry ) { var serverGroupResourceType = registry.getResourceType(serverGroupResourceTypeCode); + serverGroupResourceType.itemActions + .append({ + id: 'deleteServerGroupAction', + service: deleteService, + template: { + type: 'delete', + text: gettext('Delete Server Group') + } + }); + + serverGroupResourceType.batchActions + .append({ + id: 'batchDeleteServerGroupAction', + service: deleteService, + template: { + type: 'delete-selected', + text: gettext('Delete Server Groups') + } + }); + serverGroupResourceType.globalActions .append({ id: 'createServerGroupAction', 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 index e5fab3d5a9..c7241f36cc 100644 --- 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 @@ -23,6 +23,16 @@ registry = $injector.get('horizon.framework.conf.resource-type-registry.service'); })); + it('registers Delete Server Group as an item action', function() { + var actions = registry.getResourceType('OS::Nova::ServerGroup').itemActions; + expect(actionHasId(actions, 'deleteServerGroupAction')).toBe(true); + }); + + it('registers Delete Server Group as a batch action', function() { + var actions = registry.getResourceType('OS::Nova::ServerGroup').batchActions; + expect(actionHasId(actions, 'batchDeleteServerGroupAction')).toBe(true); + }); + it('registers Create Server Group as a global action', function() { var actions = registry.getResourceType('OS::Nova::ServerGroup').globalActions; expect(actionHasId(actions, 'createServerGroupAction')).toBe(true); diff --git a/openstack_dashboard/static/app/core/server_groups/actions/delete.action.service.js b/openstack_dashboard/static/app/core/server_groups/actions/delete.action.service.js new file mode 100644 index 0000000000..0f3d4ce957 --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/actions/delete.action.service.js @@ -0,0 +1,108 @@ +/* + * 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.delete.service', deleteServerGroupService); + + deleteServerGroupService.$inject = [ + 'horizon.app.core.openstack-service-api.nova', + 'horizon.app.core.openstack-service-api.policy', + 'horizon.app.core.server_groups.resourceType', + 'horizon.framework.util.actions.action-result.service', + 'horizon.framework.util.i18n.ngettext', + 'horizon.framework.widgets.modal.deleteModalService' + ]; + + /* + * @ngdoc factory + * @name horizon.app.core.server_groups.actions.delete.service + * + * @Description + * Brings up the delete server groups confirmation modal dialog. + + * On submit, delete given server groups. + * On cancel, do nothing. + */ + function deleteServerGroupService( + nova, + policy, + serverGroupResourceType, + actionResultService, + ngettext, + deleteModal + ) { + return { + allowed: allowed, + perform: perform + }; + + ////////////// + + function allowed() { + return policy.ifAllowed( + {rules: [['compute', 'os_compute_api:os-server-groups:delete']]}); + } + + function perform(items, scope) { + var servergroups = angular.isArray(items) ? items : [items]; + var context = { + labels: labelize(servergroups.length), + deleteEntity: deleteServerGroup + }; + return deleteModal.open(scope, servergroups, context).then(deleteResult); + } + + function deleteResult(deleteModalResult) { + // To make the result of this action generically useful, reformat the return + // from the deleteModal into a standard form + var actionResult = actionResultService.getActionResult(); + deleteModalResult.pass.forEach(function markDeleted(item) { + actionResult.deleted(serverGroupResourceType, item.context.id); + }); + deleteModalResult.fail.forEach(function markFailed(item) { + actionResult.failed(serverGroupResourceType, item.context.id); + }); + return actionResult.result; + } + + function labelize(count) { + return { + title: ngettext( + 'Confirm Delete Server Group', + 'Confirm Delete Server Groups', count), + message: ngettext( + 'You have selected "%s". Deleted Server Group is not recoverable.', + 'You have selected "%s". Deleted Server Groups are not recoverable.', count), + submit: ngettext( + 'Delete Server Group', + 'Delete Server Groups', count), + success: ngettext( + 'Deleted Server Group: %s.', + 'Deleted Server Groups: %s.', count), + error: ngettext( + 'Unable to delete Server Group: %s.', + 'Unable to delete Server Groups: %s.', count) + }; + + } + + function deleteServerGroup(servergroup) { + return nova.deleteServerGroup(servergroup, true); + } + } +})(); diff --git a/openstack_dashboard/static/app/core/server_groups/actions/delete.action.service.spec.js b/openstack_dashboard/static/app/core/server_groups/actions/delete.action.service.spec.js new file mode 100644 index 0000000000..84e61f8be8 --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/actions/delete.action.service.spec.js @@ -0,0 +1,135 @@ +/* + * 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.delete.service', function() { + + var $scope, deferredModal, novaAPI, service; + var deleteModalService = { + open: function () { + deferredModal.resolve({ + pass: [{context: {id: 'pass'}}], + fail: [{context: {id: 'fail'}}] + }); + return deferredModal.promise; + } + }; + + /////////////////////// + + beforeEach(module('horizon.app.core')); + beforeEach(module('horizon.app.core.server_groups')); + beforeEach(module('horizon.framework')); + beforeEach(module('horizon.framework.widgets.modal', function($provide) { + $provide.value('horizon.framework.widgets.modal.deleteModalService', deleteModalService); + })); + beforeEach(inject(function($injector, _$rootScope_, $q) { + $scope = _$rootScope_.$new(); + deferredModal = $q.defer(); + novaAPI = $injector.get('horizon.app.core.openstack-service-api.nova'); + service = $injector.get('horizon.app.core.server_groups.actions.delete.service'); + })); + + describe('perform method', function() { + + beforeEach(function() { + spyOn(deleteModalService, 'open').and.callThrough(); + }); + + //////////// + + it('should open the delete modal and show correct labels, single object', testSingleLabels); + it('should open the delete modal and show correct labels, plural objects', testPluralLabels); + it('should open the delete modal with correct entities', testEntities); + it('should only delete server groups that are valid', testValids); + it('should pass in a function that deletes a server group', testNova); + it('should check the policy if the user is allowed to delete server group', testAllowed); + + //////////// + + function testSingleLabels() { + var servergroups = {name: 'sg'}; + service.perform(servergroups); + + $scope.$apply(); + + var labels = deleteModalService.open.calls.argsFor(0)[2].labels; + expect(deleteModalService.open).toHaveBeenCalled(); + angular.forEach(labels, function eachLabel(label) { + expect(label.toLowerCase()).toContain('server group'); + }); + } + + function testPluralLabels() { + var servergroups = [{name: 'sg1'}, {name: 'sg2'}]; + service.perform(servergroups); + + $scope.$apply(); + + var labels = deleteModalService.open.calls.argsFor(0)[2].labels; + expect(deleteModalService.open).toHaveBeenCalled(); + angular.forEach(labels, function eachLabel(label) { + expect(label.toLowerCase()).toContain('server groups'); + }); + } + + function testEntities() { + var servergroups = [{name: 'sg1'}, {name: 'sg2'}, {name: 'sg3'}]; + service.perform(servergroups); + + $scope.$apply(); + + var entities = deleteModalService.open.calls.argsFor(0)[1]; + expect(deleteModalService.open).toHaveBeenCalled(); + expect(entities.length).toEqual(servergroups.length); + } + + function testValids() { + var servergroups = [{name: 'sg1'}, {name: 'sg2'}, {name: 'sg3'}]; + service.perform(servergroups); + + $scope.$apply(); + + var entities = deleteModalService.open.calls.argsFor(0)[1]; + expect(deleteModalService.open).toHaveBeenCalled(); + expect(entities.length).toBe(servergroups.length); + expect(entities[0].name).toEqual('sg1'); + expect(entities[1].name).toEqual('sg2'); + expect(entities[2].name).toEqual('sg3'); + } + + function testNova() { + spyOn(novaAPI, 'deleteServerGroup').and.callFake(angular.noop); + var servergroups = [{id: 1, name: 'sg1'}, {id: 2, name: 'sg2'}]; + service.perform(servergroups); + + $scope.$apply(); + + var contextArg = deleteModalService.open.calls.argsFor(0)[2]; + var deleteFunction = contextArg.deleteEntity; + deleteFunction(servergroups[0].id); + expect(novaAPI.deleteServerGroup).toHaveBeenCalledWith(servergroups[0].id, true); + } + + function testAllowed() { + var allowed = service.allowed(); + expect(allowed).toBeTruthy(); + } + }); // end of delete modal + + }); // end of delete server group + +})(); diff --git a/openstack_dashboard/test/unit/api/rest/test_nova.py b/openstack_dashboard/test/unit/api/rest/test_nova.py index a029fed0a3..1070c7bcc5 100644 --- a/openstack_dashboard/test/unit/api/rest/test_nova.py +++ b/openstack_dashboard/test/unit/api/rest/test_nova.py @@ -390,6 +390,13 @@ class NovaRestTestCase(test.TestCase): self.mock_server_group_create.assert_called_once_with( request, **server_group_data) + @test.create_mocks({api.nova: ['server_group_delete']}) + def test_server_group_delete(self): + request = self.mock_rest_request() + self.mock_server_group_delete.return_value = None + nova.ServerGroup().delete(request, "1") + self.mock_server_group_delete.assert_called_once_with(request, "1") + # # Server Metadata # diff --git a/openstack_dashboard/test/unit/api/test_nova.py b/openstack_dashboard/test/unit/api/test_nova.py index e1e4e49e95..9a4d7120be 100644 --- a/openstack_dashboard/test/unit/api/test_nova.py +++ b/openstack_dashboard/test/unit/api/test_nova.py @@ -657,3 +657,13 @@ class ComputeApiTests(test.APIMockTestCase): self.assertEqual(servergroup.policies, ret_val.policies) novaclient.versions.get_current.assert_called_once_with() novaclient.server_groups.create.assert_called_once_with(**kwargs) + + def test_server_group_delete(self): + servergroup_id = self.server_groups.first().id + novaclient = self.stub_novaclient() + novaclient.server_groups.delete.return_value = None + + api_val = api.nova.server_group_delete(self.request, servergroup_id) + + self.assertIsNone(api_val) + novaclient.server_groups.delete.assert_called_once_with(servergroup_id) diff --git a/releasenotes/notes/bp-ng-server-groups-c60849796a273138.yaml b/releasenotes/notes/bp-ng-server-groups-c60849796a273138.yaml index eee7ec7036..e9784d38ba 100644 --- a/releasenotes/notes/bp-ng-server-groups-c60849796a273138.yaml +++ b/releasenotes/notes/bp-ng-server-groups-c60849796a273138.yaml @@ -6,4 +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. + Supported actions: create, delete.