From 2161b912646b2d39e462c457602a5ef03f94237e Mon Sep 17 00:00:00 2001
From: liyingjun <yingjun.li@kylin-cloud.com>
Date: Tue, 19 Apr 2016 08:10:23 +0800
Subject: [PATCH] [Micro version] Support description for instance
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

In Nova Compute API microversion 2.19, you can specify a “description”
attribute when creating, rebuilding, or updating a server instance. This
description can be retrieved by getting server details, or list details
for servers, this patch adds support for this attribute for instance in
horizon.

Change-Id: Ic9217234021d236aee8295915f1a9c3c544396b0
Implement-blueprint: support-description-for-instance
---
 openstack_dashboard/api/microversions.py      |  3 ++-
 openstack_dashboard/api/nova.py               | 23 +++++++++++------
 openstack_dashboard/api/rest/nova.py          | 13 +++++++++-
 .../templates/instances/_detail_overview.html |  4 +++
 .../details/details.controller.js             | 12 +++++++--
 .../details/details.controller.spec.js        | 25 ++++++++++++++++---
 .../launch-instance/details/details.html      |  6 +++++
 .../launch-instance-model.service.js          |  1 +
 .../launch-instance-model.service.spec.js     |  6 ++++-
 .../openstack-service-api/nova.service.js     | 16 ++++++++++++
 .../nova.service.spec.js                      |  9 +++++++
 .../test/api_tests/nova_rest_tests.py         |  9 +++++++
 12 files changed, 112 insertions(+), 15 deletions(-)

diff --git a/openstack_dashboard/api/microversions.py b/openstack_dashboard/api/microversions.py
index ab08e37bca..58af79a56d 100644
--- a/openstack_dashboard/api/microversions.py
+++ b/openstack_dashboard/api/microversions.py
@@ -28,7 +28,8 @@ LOG = logging.getLogger(__name__)
 # microversion_support.html
 MICROVERSION_FEATURES = {
     "nova": {
-        "locked_attribute": ["2.9", "2.42"]
+        "locked_attribute": ["2.9", "2.42"],
+        "instance_description": ["2.19", "2.42"],
     },
     "cinder": {
         "consistency_groups": ["2.0", "3.10"],
diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py
index 13170d93b6..cf1952ecd4 100644
--- a/openstack_dashboard/api/nova.py
+++ b/openstack_dashboard/api/nova.py
@@ -106,7 +106,7 @@ class Server(base.APIResourceWrapper):
 
     Preserves the request info so image name can later be retrieved.
     """
-    _attrs = ['addresses', 'attrs', 'id', 'image', 'links',
+    _attrs = ['addresses', 'attrs', 'id', 'image', 'links', 'description',
               'metadata', 'name', 'private_ip', 'public_ip', 'status', 'uuid',
               'image_name', 'VirtualInterfaces', 'flavor', 'key_name', 'fault',
               'tenant_id', 'user_id', 'created', 'locked',
@@ -479,8 +479,11 @@ def server_create(request, name, image, flavor, key_name, user_data,
                   block_device_mapping_v2=None, nics=None,
                   availability_zone=None, instance_count=1, admin_pass=None,
                   disk_config=None, config_drive=None, meta=None,
-                  scheduler_hints=None):
-    return Server(novaclient(request).servers.create(
+                  scheduler_hints=None, description=None):
+    kwargs = {}
+    if description is not None:
+        kwargs['description'] = description
+    return Server(get_novaclient_with_instance_desc(request).servers.create(
         name.strip(), image, flavor, userdata=user_data,
         security_groups=security_groups,
         key_name=key_name, block_device_mapping=block_device_mapping,
@@ -488,7 +491,7 @@ def server_create(request, name, image, flavor, key_name, user_data,
         nics=nics, availability_zone=availability_zone,
         min_count=instance_count, admin_pass=admin_pass,
         disk_config=disk_config, config_drive=config_drive,
-        meta=meta, scheduler_hints=scheduler_hints), request)
+        meta=meta, scheduler_hints=scheduler_hints, **kwargs), request)
 
 
 @profiler.trace
@@ -501,9 +504,14 @@ def get_novaclient_with_locked_status(request):
     return novaclient(request, version=microversion)
 
 
+def get_novaclient_with_instance_desc(request):
+    microversion = get_microversion(request, "instance_description")
+    return novaclient(request, version=microversion)
+
+
 @profiler.trace
 def server_get(request, instance_id):
-    return Server(get_novaclient_with_locked_status(request).servers.get(
+    return Server(get_novaclient_with_instance_desc(request).servers.get(
         instance_id), request)
 
 
@@ -591,8 +599,9 @@ def server_rebuild(request, instance_id, image_id, password=None,
 
 
 @profiler.trace
-def server_update(request, instance_id, name):
-    return novaclient(request).servers.update(instance_id, name=name.strip())
+def server_update(request, instance_id, name, description=None):
+    return get_novaclient_with_instance_desc(request).servers.update(
+        instance_id, name=name.strip(), description=description)
 
 
 @profiler.trace
diff --git a/openstack_dashboard/api/rest/nova.py b/openstack_dashboard/api/rest/nova.py
index 10f67653e0..1fb111a4b8 100644
--- a/openstack_dashboard/api/rest/nova.py
+++ b/openstack_dashboard/api/rest/nova.py
@@ -43,6 +43,17 @@ class Snapshots(generic.View):
         return result
 
 
+@urls.register
+class Features(generic.View):
+    """API for check if a specified feature is supported."""
+    url_regex = r'nova/features/(?P<name>[^/]+)/$'
+
+    @rest_utils.ajax()
+    def get(self, request, name):
+        """Check if a specified feature is supported."""
+        return api.nova.is_feature_available(request, name)
+
+
 @urls.register
 class Keypairs(generic.View):
     """API for nova keypairs."""
@@ -300,7 +311,7 @@ class Servers(generic.View):
     _optional_create = [
         'block_device_mapping', 'block_device_mapping_v2', 'nics', 'meta',
         'availability_zone', 'instance_count', 'admin_pass', 'disk_config',
-        'config_drive', 'scheduler_hints'
+        'config_drive', 'scheduler_hints', 'description'
     ]
 
     @rest_utils.ajax()
diff --git a/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_overview.html b/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_overview.html
index 7def735ee9..6a37b9a76d 100644
--- a/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_overview.html
+++ b/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_overview.html
@@ -4,6 +4,10 @@
   <dl class="dl-horizontal">
     <dt>{% trans "Name" %}</dt>
     <dd data-display="{{ instance.name|default:instance.id }}">{{ instance.name }}</dd>
+    {% if instance.description != None %}
+    <dt>{% trans "Description" %}</dt>
+    <dd>{{ instance.description }}</dd>
+    {% endif %}
     <dt>{% trans "ID" %}</dt>
     <dd>{{ instance.id }}</dd>
     <dt>{% trans "Status" %}</dt>
diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/details/details.controller.js b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/details/details.controller.js
index 6aff16c422..29d5fc3f26 100644
--- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/details/details.controller.js
+++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/details/details.controller.js
@@ -32,15 +32,19 @@
   LaunchInstanceDetailsController.$inject = [
     '$scope',
     'horizon.framework.widgets.charts.donutChartSettings',
-    'horizon.framework.widgets.charts.quotaChartDefaults'
+    'horizon.framework.widgets.charts.quotaChartDefaults',
+    'horizon.app.core.openstack-service-api.nova'
   ];
 
   function LaunchInstanceDetailsController($scope,
     donutChartSettings,
-    quotaChartDefaults
+    quotaChartDefaults,
+    novaAPI
   ) {
 
     var ctrl = this;
+    novaAPI.isFeatureSupported(
+      'instance_description').then(isDescriptionSupported);
 
     // Error text for invalid fields
     ctrl.instanceNameError = gettext('A name is required for your instance.');
@@ -102,6 +106,10 @@
 
     ////////////////////
 
+    function isDescriptionSupported(data) {
+      ctrl.isDescriptionSupported = data.data;
+    }
+
     function getMaxInstances() {
       return $scope.model.novaLimits.maxTotalInstances;
     }
diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/details/details.controller.spec.js b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/details/details.controller.spec.js
index f16b6c0a8f..d25ee721ef 100644
--- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/details/details.controller.spec.js
+++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/details/details.controller.spec.js
@@ -23,15 +23,24 @@
     beforeEach(module('horizon.dashboard.project'));
 
     describe('LaunchInstanceDetailsController', function() {
-      var scope, ctrl, deferred;
+      var $q, scope, ctrl, deferred;
+      var novaAPI = {
+        isFeatureSupported: function() {
+          var deferred = $q.defer();
+          deferred.resolve({ data: true });
+          return deferred.promise;
+        }
+      };
 
       beforeEach(module(function($provide) {
         $provide.value('horizon.framework.widgets.charts.donutChartSettings', noop);
         $provide.value('horizon.framework.widgets.charts.quotaChartDefaults', noop);
+        $provide.value('horizon.app.core.openstack-service-api.nova', novaAPI);
       }));
 
-      beforeEach(inject(function($controller, $rootScope, $q) {
-        scope = $rootScope.$new();
+      beforeEach(inject(function($injector, $controller, _$q_, _$rootScope_) {
+        scope = _$rootScope_.$new();
+        $q = _$q_;
         deferred = $q.defer();
         scope.initPromise = deferred.promise;
 
@@ -47,11 +56,21 @@
           }
         };
 
+        novaAPI = $injector.get('horizon.app.core.openstack-service-api.nova');
         ctrl = $controller('LaunchInstanceDetailsController', { $scope: scope });
 
         scope.$apply();
       }));
 
+      it('should have isDescriptionSupported defined', function() {
+        spyOn(novaAPI, 'isFeatureSupported').and.callFake(function () {
+          var deferred = $q.defer();
+          deferred.resolve({ data: true });
+          return deferred.promise;
+        });
+        expect(ctrl.isDescriptionSupported).toBe(true);
+      });
+
       it('should define error messages for invalid fields', function() {
         expect(ctrl.instanceNameError).toBeDefined();
         expect(ctrl.instanceCountError).toBeDefined();
diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/details/details.html b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/details/details.html
index f78d099cc6..31f2b0684e 100644
--- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/details/details.html
+++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/details/details.html
@@ -19,6 +19,12 @@
           </span>
       </div>
 
+      <div ng-if="ctrl.isDescriptionSupported" class="form-group">
+        <label class="control-label" translate for="description">Description</label>
+        <input id="description" name="description" type="text" class="form-control"
+               ng-model="model.newInstanceSpec.description">
+      </div>
+
       <div class="form-group">
         <label class="control-label" translate for="availability-zone">Availability Zone</label>
         <div class="horizon-loading-bar" ng-if="!model.loaded.availabilityZones">
diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.js b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.js
index f6fab02ae0..a0a93e51d0 100644
--- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.js
+++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.js
@@ -181,6 +181,7 @@
         availability_zone: null,
         admin_pass: null,
         config_drive: false,
+        description: null,
         // REQUIRED Server Key.  Null allowed.
         user_data: '',
         disk_config: 'AUTO',
diff --git a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.spec.js b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.spec.js
index d96a09d9ee..715575ce82 100644
--- a/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.spec.js
+++ b/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/launch-instance-model.service.spec.js
@@ -822,7 +822,7 @@
         // This is here to ensure that as people add/change items, they
         // don't forget to implement tests for them.
         it('has the right number of properties', function() {
-          expect(Object.keys(model.newInstanceSpec).length).toBe(21);
+          expect(Object.keys(model.newInstanceSpec).length).toBe(22);
         });
 
         it('sets availability zone to null', function() {
@@ -833,6 +833,10 @@
           expect(model.newInstanceSpec.admin_pass).toBeNull();
         });
 
+        it('sets description to null', function() {
+          expect(model.newInstanceSpec.description).toBeNull();
+        });
+
         it('sets config drive to false', function() {
           expect(model.newInstanceSpec.config_drive).toBe(false);
         });
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 5771d5c642..84137d5f7c 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
@@ -42,6 +42,7 @@
       getConsoleInfo: getConsoleInfo,
       getServerVolumes: getServerVolumes,
       getServerSecurityGroups: getServerSecurityGroups,
+      isFeatureSupported: isFeatureSupported,
       getKeypairs: getKeypairs,
       createKeypair: createKeypair,
       getKeypair: getKeypair,
@@ -84,6 +85,21 @@
 
     ///////////
 
+    // Feature
+
+    /**
+     * @name isFeatureSupported
+     * @description
+     * Check if the feature is supported.
+     * @returns {Object} The result of the API call
+     */
+    function isFeatureSupported(feature) {
+      return apiService.get('/api/nova/features/' + feature)
+        .error(function () {
+          toastService.add('error', gettext('Unable to check the Nova service feature.'));
+        });
+    }
+
     // Nova Services
 
     /**
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 2ce8557940..20da7fdef0 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
@@ -40,6 +40,15 @@
     });
 
     var tests = [
+      {
+        "func": "isFeatureSupported",
+        "method": "get",
+        "path": "/api/nova/features/fake",
+        "error": "Unable to check the Nova service feature.",
+        "testInput": [
+          "fake"
+        ]
+      },
       {
         "func": "getServices",
         "method": "get",
diff --git a/openstack_dashboard/test/api_tests/nova_rest_tests.py b/openstack_dashboard/test/api_tests/nova_rest_tests.py
index 4929271f11..e166e07d2d 100644
--- a/openstack_dashboard/test/api_tests/nova_rest_tests.py
+++ b/openstack_dashboard/test/api_tests/nova_rest_tests.py
@@ -969,3 +969,12 @@ class NovaRestTestCase(test.TestCase):
         self.assertEqual(response.content.decode('utf-8'),
                          '"Service Nova is disabled."')
         nc.tenant_quota_update.assert_not_called()
+
+    @mock.patch.object(nova.api, 'nova')
+    def test_version_get(self, nc):
+        request = self.mock_rest_request()
+        nc.is_feature_available.return_value = True
+
+        response = nova.Features().get(request, 'fake')
+        self.assertStatusCode(response, 200)
+        self.assertEqual(response.content.decode('utf-8'), 'true')