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[^/]+)/$' + + @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 @@
{% trans "Name" %}
{{ instance.name }}
+ {% if instance.description != None %} +
{% trans "Description" %}
+
{{ instance.description }}
+ {% endif %}
{% trans "ID" %}
{{ instance.id }}
{% trans "Status" %}
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 @@ +
+ + +
+
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')