From ce283b4a31cc5c3040d2abf37d76c6f1e24e046a Mon Sep 17 00:00:00 2001
From: "wei.ying" <wei.ying@easystack.cn>
Date: Sat, 4 Nov 2017 14:29:14 +0800
Subject: [PATCH] Add angular server groups panel

This patch adds angular server groups panel under the
project->compute panel group.

To be added in subsequent patches:
 - Create Action
 - Delete Action
 - Detais

To view the server groups panel only the 'ServerGroups' nova API
extension is available.

Change-Id: Ide9f54439c52cce9642c2dd417f9e7a8ad98e846
Implements: blueprint ng-server-groups
---
 openstack_dashboard/api/microversions.py      |   3 +-
 .../project/server_groups/__init__.py         |   0
 .../dashboards/project/server_groups/panel.py |  41 +++++++
 .../dashboards/project/server_groups/urls.py  |  22 ++++
 .../_1110_project_server_groups_panel.py      |  10 ++
 .../static/app/core/core.module.js            |   1 +
 .../nova.service.spec.js                      |   6 +
 .../static/app/core/server_groups/panel.html  |   3 +
 .../server_groups/server-groups.module.js     | 115 ++++++++++++++++++
 .../server-groups.module.spec.js              |  48 ++++++++
 .../server_groups/server-groups.service.js    |  88 ++++++++++++++
 .../server-groups.service.spec.js             |  62 ++++++++++
 openstack_dashboard/test/settings.py          |   5 +
 .../bp-ng-server-groups-c60849796a273138.yaml |   8 ++
 14 files changed, 411 insertions(+), 1 deletion(-)
 create mode 100644 openstack_dashboard/dashboards/project/server_groups/__init__.py
 create mode 100644 openstack_dashboard/dashboards/project/server_groups/panel.py
 create mode 100644 openstack_dashboard/dashboards/project/server_groups/urls.py
 create mode 100644 openstack_dashboard/enabled/_1110_project_server_groups_panel.py
 create mode 100644 openstack_dashboard/static/app/core/server_groups/panel.html
 create mode 100644 openstack_dashboard/static/app/core/server_groups/server-groups.module.js
 create mode 100644 openstack_dashboard/static/app/core/server_groups/server-groups.module.spec.js
 create mode 100644 openstack_dashboard/static/app/core/server_groups/server-groups.service.js
 create mode 100644 openstack_dashboard/static/app/core/server_groups/server-groups.service.spec.js
 create mode 100644 releasenotes/notes/bp-ng-server-groups-c60849796a273138.yaml

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 @@
+<hz-resource-panel resource-type-name="OS::Nova::ServerGroup">
+  <hz-resource-table resource-type-name="OS::Nova::ServerGroup"></hz-resource-table>
+</hz-resource-panel>
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 <https://blueprints.launchpad.net/horizon/+spec/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.