diff --git a/openstack_dashboard/api/microversions.py b/openstack_dashboard/api/microversions.py index 795dee1580..a8939510c6 100644 --- a/openstack_dashboard/api/microversions.py +++ b/openstack_dashboard/api/microversions.py @@ -31,7 +31,8 @@ MICROVERSION_FEATURES = { "locked_attribute": ["2.9", "2.42"], "instance_description": ["2.19", "2.42"], "remote_console_mks": ["2.8", "2.53"], - "servergroup_soft_policies": ["2.15", "2.60"] + "servergroup_soft_policies": ["2.15", "2.60"], + "servergroup_user_info": ["2.13", "2.60"] }, "cinder": { "consistency_groups": ["2.0", "3.10"], diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 22381c2348..f5b8ed63c6 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -943,6 +943,13 @@ def server_group_delete(request, servergroup_id): novaclient(request).server_groups.delete(servergroup_id) +@profiler.trace +def server_group_get(request, servergroup_id): + microversion = get_microversion(request, "servergroup_user_info") + return novaclient(request, version=microversion).server_groups.get( + 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 7d47da3b23..a5fbdca969 100644 --- a/openstack_dashboard/api/rest/nova.py +++ b/openstack_dashboard/api/rest/nova.py @@ -460,6 +460,14 @@ class ServerGroup(generic.View): """ api.nova.server_group_delete(request, servergroup_id) + @rest_utils.ajax() + def get(self, request, servergroup_id): + """Get a specific server group + + http://localhost/api/nova/servergroups/1 + """ + return api.nova.server_group_get(request, servergroup_id).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 2253596919..abd9aeeb0a 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 @@ -52,6 +52,7 @@ createServer: createServer, getServer: getServer, getServers: getServers, + getServerGroup: getServerGroup, getServerGroups: getServerGroups, createServerGroup: createServerGroup, deleteServerGroup: deleteServerGroup, @@ -318,6 +319,21 @@ }); } + /** + * @name getServerGroup + * @description + * Get a single server group by ID + * @param {string} id + * Specifies the id of the server group to request. + * @returns {Object} The result of the API call + */ + function getServerGroup(id) { + return apiService.get('/api/nova/servergroups/' + id) + .error(function () { + toastService.add('error', gettext('Unable to retrieve the server group.')); + }); + } + /** * @name getServerGroups * @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 418021cc7e..956b0823e0 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 @@ -296,6 +296,15 @@ "path": "/api/nova/servers/", "error": "Unable to retrieve instances." }, + { + "func": "getServerGroup", + "method": "get", + "path": "/api/nova/servergroups/17", + "error": "Unable to retrieve the server group.", + "testInput": [ + '17' + ] + }, { "func": 'getServerGroups', "method": 'get', diff --git a/openstack_dashboard/static/app/core/server_groups/details/details.module.js b/openstack_dashboard/static/app/core/server_groups/details/details.module.js new file mode 100644 index 0000000000..1afec60cb4 --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/details/details.module.js @@ -0,0 +1,54 @@ +/* + * 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.details + * + * @description + * Provides details features for server groups. + */ + angular + .module('horizon.app.core.server_groups.details', [ + 'horizon.framework.conf', + 'horizon.app.core' + ]) + .run(registerServerGroupDetails); + + registerServerGroupDetails.$inject = [ + 'horizon.app.core.server_groups.basePath', + 'horizon.app.core.server_groups.resourceType', + 'horizon.app.core.server_groups.service', + 'horizon.framework.conf.resource-type-registry.service' + ]; + + function registerServerGroupDetails( + basePath, + serverGroupResourceType, + serverGroupService, + registry + ) { + registry.getResourceType(serverGroupResourceType) + .setLoadFunction(serverGroupService.getServerGroupPromise) + .detailsViews.append({ + id: 'serverGroupDetailsOverview', + name: gettext('Overview'), + template: basePath + 'details/overview.html' + }); + } + +})(); diff --git a/openstack_dashboard/static/app/core/server_groups/details/details.module.spec.js b/openstack_dashboard/static/app/core/server_groups/details/details.module.spec.js new file mode 100644 index 0000000000..2ba56e9552 --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/details/details.module.spec.js @@ -0,0 +1,35 @@ +/* + * 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 group details module', function() { + it('should exist', function() { + expect(angular.module('horizon.app.core.server_groups.details')).toBeDefined(); + }); + + var registry, resource; + beforeEach(module('horizon.framework')); + beforeEach(module('horizon.app.core.server_groups')); + beforeEach(inject(function($injector) { + registry = $injector.get('horizon.framework.conf.resource-type-registry.service'); + })); + + it('should be loaded', function() { + resource = registry.getResourceType('OS::Nova::ServerGroup'); + expect(resource.detailsViews[0].id).toBe('serverGroupDetailsOverview'); + }); + }); +})(); diff --git a/openstack_dashboard/static/app/core/server_groups/details/overview.controller.js b/openstack_dashboard/static/app/core/server_groups/details/overview.controller.js new file mode 100644 index 0000000000..a2a08269f7 --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/details/overview.controller.js @@ -0,0 +1,107 @@ +/* + * 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') + .controller('ServerGroupOverviewController', ServerGroupOverviewController); + + ServerGroupOverviewController.$inject = [ + '$scope', + 'horizon.app.core.openstack-service-api.nova', + 'horizon.app.core.openstack-service-api.userSession', + 'horizon.app.core.server_groups.resourceType', + 'horizon.app.core.server_groups.service', + 'horizon.framework.conf.resource-type-registry.service' + ]; + + function ServerGroupOverviewController( + $scope, + nova, + userSession, + serverGroupResourceTypeCode, + serverGroupsService, + registry + ) { + var ctrl = this; + + nova.isFeatureSupported( + 'servergroup_user_info').then(onGetServerGroupProperties); + + function onGetServerGroupProperties(response) { + var properties = ['id', 'name', 'policy']; + if (response.data) { + properties.splice(2, 0, 'project_id', 'user_id'); + } + ctrl.properties = properties; + } + + ctrl.resourceType = registry.getResourceType(serverGroupResourceTypeCode); + ctrl.tableConfig = { + selectAll: false, + expand: false, + trackId: 'id', + columns: [ + { + id: 'name', + title: gettext('Instance Name'), + priority: 1, + sortDefault: true, + urlFunction: serverGroupsService.getInstanceDetailsPath + }, + { + id: 'id', + title: gettext('Instance ID'), + priority: 1 + } + ] + }; + + $scope.context.loadPromise.then(onGetServerGroup); + + function onGetServerGroup(servergroup) { + ctrl.servergroup = servergroup.data; + if (ctrl.servergroup.members.length) { + // The server group members only contains the instance id, + // does not contain other information of the instance, we + // need to get the list of instances and then get the specified + // instance from the list based on id. + nova.getServers().then(function getServer(servers) { + var members = []; + ctrl.servergroup.members.forEach(function(member) { + for (var i = 0; i < servers.data.items.length; i++) { + var server = servers.data.items[i]; + if (member === server.id) { + members.push(server); + break; + } + } + }); + ctrl.members = members; + }); + } else { + ctrl.members = []; + } + + userSession.get().then(setProject); + + function setProject(session) { + ctrl.projectId = session.project_id; + } + } + } + +})(); diff --git a/openstack_dashboard/static/app/core/server_groups/details/overview.controller.spec.js b/openstack_dashboard/static/app/core/server_groups/details/overview.controller.spec.js new file mode 100644 index 0000000000..83881e9289 --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/details/overview.controller.spec.js @@ -0,0 +1,80 @@ +/* + * 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 group overview controller', function() { + var $controller, $q, $timeout, nova, session; + var sessionObj = {project_id: '10'}; + + beforeEach(module('horizon.app.core.server_groups')); + beforeEach(module('horizon.framework.conf')); + beforeEach(inject(function($injector) { + $controller = $injector.get('$controller'); + $q = $injector.get('$q'); + $timeout = $injector.get('$timeout'); + session = $injector.get('horizon.app.core.openstack-service-api.userSession'); + nova = $injector.get('horizon.app.core.openstack-service-api.nova'); + })); + + it('sets ctrl.members when server group members length === 0', inject(function() { + var deferred = $q.defer(); + var serverGroupDeferred = $q.defer(); + var serversDeferred = $q.defer(); + var sessionDeferred = $q.defer(); + deferred.resolve({data: { data: true}}); + serverGroupDeferred.resolve({data: {members: ['1', '2']}}); + serversDeferred.resolve({data: { items: [{'id': '1'}, {'id': '2'}]}}); + sessionDeferred.resolve(sessionObj); + spyOn(nova, 'isFeatureSupported').and.returnValue(deferred.promise); + spyOn(nova, 'getServerGroup').and.returnValue(serverGroupDeferred.promise); + spyOn(nova, 'getServers').and.returnValue(serversDeferred.promise); + spyOn(session, 'get').and.returnValue(sessionDeferred.promise); + var ctrl = $controller('ServerGroupOverviewController', + { + '$scope': {context: {loadPromise: serverGroupDeferred.promise}} + } + ); + $timeout.flush(); + expect(ctrl.properties).toBeDefined(); + expect(ctrl.members[0]).toEqual({'id': '1'}); + expect(ctrl.projectId).toBe(sessionObj.project_id); + })); + + it('sets ctrl.members when server group members length > 0', inject(function() { + var deferred = $q.defer(); + var serverGroupDeferred = $q.defer(); + var serversDeferred = $q.defer(); + var sessionDeferred = $q.defer(); + deferred.resolve({data: { data: true}}); + serverGroupDeferred.resolve({data: {members: []}}); + serversDeferred.resolve({data: { items: [{'id': '1'}, {'id': '2'}]}}); + sessionDeferred.resolve(sessionObj); + spyOn(nova, 'isFeatureSupported').and.returnValue(deferred.promise); + spyOn(nova, 'getServerGroup').and.returnValue(serverGroupDeferred.promise); + spyOn(nova, 'getServers').and.returnValue(serversDeferred.promise); + spyOn(session, 'get').and.returnValue(sessionDeferred.promise); + var ctrl = $controller('ServerGroupOverviewController', + { + '$scope': {context: {loadPromise: serverGroupDeferred.promise}} + } + ); + $timeout.flush(); + expect(ctrl.properties).toBeDefined(); + expect(ctrl.members).toEqual([]); + expect(ctrl.projectId).toBe(sessionObj.project_id); + })); + }); +})(); diff --git a/openstack_dashboard/static/app/core/server_groups/details/overview.html b/openstack_dashboard/static/app/core/server_groups/details/overview.html new file mode 100644 index 0000000000..5fe51c557c --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/details/overview.html @@ -0,0 +1,17 @@ +