diff --git a/openstack_dashboard/api/microversions.py b/openstack_dashboard/api/microversions.py index 2a770d8b17..795dee1580 100644 --- a/openstack_dashboard/api/microversions.py +++ b/openstack_dashboard/api/microversions.py @@ -30,7 +30,8 @@ MICROVERSION_FEATURES = { "nova": { "locked_attribute": ["2.9", "2.42"], "instance_description": ["2.19", "2.42"], - "remote_console_mks": ["2.8", "2.53"] + "remote_console_mks": ["2.8", "2.53"], + "servergroup_soft_policies": ["2.15", "2.60"] }, "cinder": { "consistency_groups": ["2.0", "3.10"], diff --git a/openstack_dashboard/dashboards/project/server_groups/__init__.py b/openstack_dashboard/dashboards/project/server_groups/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/project/server_groups/panel.py b/openstack_dashboard/dashboards/project/server_groups/panel.py new file mode 100644 index 0000000000..557bf5fe31 --- /dev/null +++ b/openstack_dashboard/dashboards/project/server_groups/panel.py @@ -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. + +import logging + +from django.utils.translation import ugettext_lazy as _ + +import horizon + + +LOG = logging.getLogger(__name__) + + +class ServerGroups(horizon.Panel): + name = _("Server Groups") + slug = "server_groups" + permissions = ('openstack.services.compute',) + policy_rules = (("compute", "os_compute_api:os-server-groups:index"),) + + def allowed(self, context): + request = context['request'] + try: + return ( + super(ServerGroups, self).allowed(context) + and request.user.has_perms(self.permissions) + ) + except Exception: + LOG.exception("Call to list enabled services failed. This is " + "likely due to a problem communicating with the " + "Nova endpoint. Server Groups panel will not be " + "displayed.") + return False diff --git a/openstack_dashboard/dashboards/project/server_groups/urls.py b/openstack_dashboard/dashboards/project/server_groups/urls.py new file mode 100644 index 0000000000..27e06913c9 --- /dev/null +++ b/openstack_dashboard/dashboards/project/server_groups/urls.py @@ -0,0 +1,22 @@ +# 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. + +from django.conf.urls import url +from django.utils.translation import ugettext_lazy as _ + +from horizon.browsers import views + + +title = _("Server Groups") +urlpatterns = [ + url(r'^$', views.AngularIndexView.as_view(title=title), name='index'), +] diff --git a/openstack_dashboard/enabled/_1110_project_server_groups_panel.py b/openstack_dashboard/enabled/_1110_project_server_groups_panel.py new file mode 100644 index 0000000000..fb4b125081 --- /dev/null +++ b/openstack_dashboard/enabled/_1110_project_server_groups_panel.py @@ -0,0 +1,10 @@ +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'server_groups' +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'project' +# The slug of the panel group the PANEL is associated with. +PANEL_GROUP = 'compute' + +# Python panel class of the PANEL to be added. +ADD_PANEL = ('openstack_dashboard.dashboards.project.server_groups' + '.panel.ServerGroups') diff --git a/openstack_dashboard/static/app/core/core.module.js b/openstack_dashboard/static/app/core/core.module.js index f05b8c2c33..fac2f3f136 100644 --- a/openstack_dashboard/static/app/core/core.module.js +++ b/openstack_dashboard/static/app/core/core.module.js @@ -40,6 +40,7 @@ 'horizon.app.core.metadata', 'horizon.app.core.network_qos', 'horizon.app.core.openstack-service-api', + 'horizon.app.core.server_groups', 'horizon.app.core.trunks', 'horizon.app.core.workflow', 'horizon.framework.conf', 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 909a9c78ab..b521d38e99 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,12 @@ "path": "/api/nova/servers/", "error": "Unable to retrieve instances." }, + { + "func": 'getServerGroups', + "method": 'get', + "path": '/api/nova/servergroups/', + "error": 'Unable to retrieve server groups.' + }, { "func": "getExtensions", "method": "get", diff --git a/openstack_dashboard/static/app/core/server_groups/panel.html b/openstack_dashboard/static/app/core/server_groups/panel.html new file mode 100644 index 0000000000..0ce877bc84 --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/panel.html @@ -0,0 +1,3 @@ + + + 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 new file mode 100644 index 0000000000..695e43ef01 --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/server-groups.module.js @@ -0,0 +1,115 @@ +/* + * 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 + * + * @description + * Provides all of the services and widgets required + * to support and display server groups related content. + */ + angular + .module('horizon.app.core.server_groups', [ + 'horizon.framework.conf', + 'horizon.app.core' + ]) + .constant('horizon.app.core.server_groups.resourceType', 'OS::Nova::ServerGroup') + .run(run) + .config(config); + + run.$inject = [ + 'horizon.app.core.server_groups.resourceType', + 'horizon.app.core.server_groups.service', + 'horizon.framework.conf.resource-type-registry.service' + ]; + + function run(serverGroupResourceType, + serverGroupsService, + registry) { + registry.getResourceType(serverGroupResourceType) + .setNames(gettext('Server Group'), gettext('Server Groups')) + .setProperties(serverGroupProperties()) + .setListFunction(serverGroupsService.getServerGroupsPromise) + .tableColumns + .append({ + id: 'name', + priority: 1, + sortDefault: true + }) + // The name is not unique, so we need to show the ID to + // distinguish. + .append({ + id: 'id', + priority: 1 + }) + .append({ + id: 'policy', + priority: 1 + }); + + registry.getResourceType(serverGroupResourceType).filterFacets + .append({ + label: gettext('Name'), + name: 'name', + singleton: true + }) + .append({ + label: gettext('ID'), + name: 'id', + singleton: true + }) + .append({ + label: gettext('Policy'), + name: 'policy', + singleton: true + }); + } + + /** + * @name serverGroupProperties + * @description resource properties for server group module + */ + function serverGroupProperties() { + return { + name: gettext('Name'), + id: gettext('ID'), + policy: gettext('Policy') + }; + } + + config.$inject = [ + '$windowProvider', + '$routeProvider' + ]; + + /** + * @name config + * @param {Object} $windowProvider + * @param {Object} $routeProvider + * @description Routes used by this module. + * @returns {undefined} Returns nothing + */ + function config($windowProvider, $routeProvider) { + var path = $windowProvider.$get().STATIC_URL + 'app/core/server_groups/'; + + $routeProvider.when('/project/server_groups', { + templateUrl: path + 'panel.html' + }); + } + +})(); diff --git a/openstack_dashboard/static/app/core/server_groups/server-groups.module.spec.js b/openstack_dashboard/static/app/core/server_groups/server-groups.module.spec.js new file mode 100644 index 0000000000..8135dc012a --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/server-groups.module.spec.js @@ -0,0 +1,48 @@ +/* + * 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', function () { + it('should exist', function () { + expect(angular.module('horizon.app.core.server_groups')).toBeDefined(); + }); + }); + + describe('loading the module', function () { + var registry; + + beforeEach(module('horizon.app.core.server_groups')); + beforeEach(inject(function($injector) { + registry = $injector.get('horizon.framework.conf.resource-type-registry.service'); + })); + + it('registers names', function() { + expect(registry.getResourceType('OS::Nova::ServerGroup').getName()).toBe("Server Groups"); + }); + + it('should set facets for search', function () { + var names = registry.getResourceType('OS::Nova::ServerGroup').filterFacets + .map(getName); + expect(names).toContain('name'); + expect(names).toContain('id'); + expect(names).toContain('policy'); + + function getName(x) { + return x.name; + } + }); + }); +})(); 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 new file mode 100644 index 0000000000..5471df200b --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/server-groups.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.service', serverGroupsService); + + serverGroupsService.$inject = [ + 'horizon.app.core.openstack-service-api.nova' + ]; + + /* + * @ngdoc factory + * @name horizon.app.core.server_groups.service + * + * @description + * This service provides functions that are used through the Server Groups + * features. These are primarily used in the module registrations + * but do not need to be restricted to such use. Each exposed function + * is documented below. + */ + function serverGroupsService(nova) { + return { + getServerGroupsPromise: getServerGroupsPromise + }; + + /* + * @ngdoc function + * @name getServerGroupPolicies + * @description + * Returns a list for the server group policies. + */ + function getServerGroupPolicies() { + return nova.isFeatureSupported('servergroup_soft_policies') + .then(isSoftPoliciesSupported); + + function isSoftPoliciesSupported(response) { + var policies = { + 'affinity': gettext('Affinity'), + 'anti-affinity': gettext('Anti Affinity') + }; + if (response.data) { + policies['soft-anti-affinity'] = gettext('Soft Anti Affinity'); + policies['soft-affinity'] = gettext('Soft Affinity'); + } + return policies; + } + } + + /* + * @ngdoc function + * @name getServerGroupsPromise + * @description + * Rreturns a promise for the matching server groups. + * This is used in displaying lists of Server Groups. + */ + function getServerGroupsPromise() { + return nova.getServerGroups().then(modifyResponse); + + function modifyResponse(response) { + return getServerGroupPolicies().then(modifyItems); + + function modifyItems(policies) { + response.data.items.map(function (item) { + // When creating a server group, the back-end limit + // server group can only have one policy. + item.policy = policies[item.policies[0]]; + }); + return {data: {items: response.data.items}}; + } + } + } + } + +})(); diff --git a/openstack_dashboard/static/app/core/server_groups/server-groups.service.spec.js b/openstack_dashboard/static/app/core/server_groups/server-groups.service.spec.js new file mode 100644 index 0000000000..543fcd3e1f --- /dev/null +++ b/openstack_dashboard/static/app/core/server_groups/server-groups.service.spec.js @@ -0,0 +1,62 @@ +/* + * 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 service', function() { + var $q, $timeout, nova, service; + + beforeEach(module('horizon.app.core.server_groups')); + beforeEach(inject(function($injector) { + $q = $injector.get('$q'); + $timeout = $injector.get('$timeout'); + nova = $injector.get('horizon.app.core.openstack-service-api.nova'); + service = $injector.get('horizon.app.core.server_groups.service'); + })); + + describe('getServerGroupsPromise', function() { + it("provides a promise when soft policies are supported", inject(function() { + var deferred = $q.defer(); + var deferredPolicies = $q.defer(); + spyOn(nova, 'getServerGroups').and.returnValue(deferred.promise); + spyOn(nova, 'isFeatureSupported').and.returnValue(deferredPolicies.promise); + var result = service.getServerGroupsPromise({}); + deferred.resolve({data: {items: [{id: '1', policies: ['affinity']}]}}); + deferredPolicies.resolve({data: true}); + $timeout.flush(); + + expect(nova.getServerGroups).toHaveBeenCalled(); + expect(nova.isFeatureSupported).toHaveBeenCalled(); + expect(result.$$state.value.data.items[0].id).toBe('1'); + })); + + it("provides a promise when soft policies are not supported", inject(function() { + var deferred = $q.defer(); + var deferredPolicies = $q.defer(); + spyOn(nova, 'getServerGroups').and.returnValue(deferred.promise); + spyOn(nova, 'isFeatureSupported').and.returnValue(deferredPolicies.promise); + var result = service.getServerGroupsPromise({}); + deferred.resolve({data: {items: [{id: '1', policies: ['affinity']}]}}); + deferredPolicies.resolve({data: false}); + $timeout.flush(); + + expect(nova.getServerGroups).toHaveBeenCalled(); + expect(nova.isFeatureSupported).toHaveBeenCalled(); + expect(result.$$state.value.data.items[0].id).toBe('1'); + })); + }); + }); + +})(); diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py index ad5df5240d..4b6b2311f1 100644 --- a/openstack_dashboard/test/settings.py +++ b/openstack_dashboard/test/settings.py @@ -302,6 +302,11 @@ TEST_GLOBAL_MOCKS_ON_PANELS = { '.domains.panel.Domains.can_access'), 'return_value': True, }, + 'server_groups': { + 'method': ('openstack_dashboard.dashboards.project' + '.server_groups.panel.ServerGroups.can_access'), + 'return_value': True, + }, 'trunk-project': { 'method': ('openstack_dashboard.dashboards.project' '.trunks.panel.Trunks.can_access'), diff --git a/releasenotes/notes/bp-ng-server-groups-c60849796a273138.yaml b/releasenotes/notes/bp-ng-server-groups-c60849796a273138.yaml new file mode 100644 index 0000000000..c37627b803 --- /dev/null +++ b/releasenotes/notes/bp-ng-server-groups-c60849796a273138.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + [`blueprint ng-server-groups `_] + This blueprint add angular server groups panel below the + Project->Compute panel group. The panel turns on if Nova API + extension 'ServerGroups' is available. It displays information about + server groups.