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.