Merge "[Micro version] Support description for instance"

This commit is contained in:
Zuul 2017-10-26 18:42:57 +00:00 committed by Gerrit Code Review
commit 2e1b80a1f5
12 changed files with 112 additions and 15 deletions

View File

@ -28,7 +28,8 @@ LOG = logging.getLogger(__name__)
# microversion_support.html # microversion_support.html
MICROVERSION_FEATURES = { MICROVERSION_FEATURES = {
"nova": { "nova": {
"locked_attribute": ["2.9", "2.42"] "locked_attribute": ["2.9", "2.42"],
"instance_description": ["2.19", "2.42"],
}, },
"cinder": { "cinder": {
"consistency_groups": ["2.0", "3.10"], "consistency_groups": ["2.0", "3.10"],

View File

@ -106,7 +106,7 @@ class Server(base.APIResourceWrapper):
Preserves the request info so image name can later be retrieved. 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', 'metadata', 'name', 'private_ip', 'public_ip', 'status', 'uuid',
'image_name', 'VirtualInterfaces', 'flavor', 'key_name', 'fault', 'image_name', 'VirtualInterfaces', 'flavor', 'key_name', 'fault',
'tenant_id', 'user_id', 'created', 'locked', '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, block_device_mapping_v2=None, nics=None,
availability_zone=None, instance_count=1, admin_pass=None, availability_zone=None, instance_count=1, admin_pass=None,
disk_config=None, config_drive=None, meta=None, disk_config=None, config_drive=None, meta=None,
scheduler_hints=None): scheduler_hints=None, description=None):
return Server(novaclient(request).servers.create( 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, name.strip(), image, flavor, userdata=user_data,
security_groups=security_groups, security_groups=security_groups,
key_name=key_name, block_device_mapping=block_device_mapping, 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, nics=nics, availability_zone=availability_zone,
min_count=instance_count, admin_pass=admin_pass, min_count=instance_count, admin_pass=admin_pass,
disk_config=disk_config, config_drive=config_drive, 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 @profiler.trace
@ -501,9 +504,14 @@ def get_novaclient_with_locked_status(request):
return novaclient(request, version=microversion) 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 @profiler.trace
def server_get(request, instance_id): 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) instance_id), request)
@ -591,8 +599,9 @@ def server_rebuild(request, instance_id, image_id, password=None,
@profiler.trace @profiler.trace
def server_update(request, instance_id, name): def server_update(request, instance_id, name, description=None):
return novaclient(request).servers.update(instance_id, name=name.strip()) return get_novaclient_with_instance_desc(request).servers.update(
instance_id, name=name.strip(), description=description)
@profiler.trace @profiler.trace

View File

@ -43,6 +43,17 @@ class Snapshots(generic.View):
return result 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 @urls.register
class Keypairs(generic.View): class Keypairs(generic.View):
"""API for nova keypairs.""" """API for nova keypairs."""
@ -300,7 +311,7 @@ class Servers(generic.View):
_optional_create = [ _optional_create = [
'block_device_mapping', 'block_device_mapping_v2', 'nics', 'meta', 'block_device_mapping', 'block_device_mapping_v2', 'nics', 'meta',
'availability_zone', 'instance_count', 'admin_pass', 'disk_config', 'availability_zone', 'instance_count', 'admin_pass', 'disk_config',
'config_drive', 'scheduler_hints' 'config_drive', 'scheduler_hints', 'description'
] ]
@rest_utils.ajax() @rest_utils.ajax()

View File

@ -4,6 +4,10 @@
<dl class="dl-horizontal"> <dl class="dl-horizontal">
<dt>{% trans "Name" %}</dt> <dt>{% trans "Name" %}</dt>
<dd data-display="{{ instance.name|default:instance.id }}">{{ instance.name }}</dd> <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> <dt>{% trans "ID" %}</dt>
<dd>{{ instance.id }}</dd> <dd>{{ instance.id }}</dd>
<dt>{% trans "Status" %}</dt> <dt>{% trans "Status" %}</dt>

View File

@ -32,15 +32,19 @@
LaunchInstanceDetailsController.$inject = [ LaunchInstanceDetailsController.$inject = [
'$scope', '$scope',
'horizon.framework.widgets.charts.donutChartSettings', '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, function LaunchInstanceDetailsController($scope,
donutChartSettings, donutChartSettings,
quotaChartDefaults quotaChartDefaults,
novaAPI
) { ) {
var ctrl = this; var ctrl = this;
novaAPI.isFeatureSupported(
'instance_description').then(isDescriptionSupported);
// Error text for invalid fields // Error text for invalid fields
ctrl.instanceNameError = gettext('A name is required for your instance.'); ctrl.instanceNameError = gettext('A name is required for your instance.');
@ -102,6 +106,10 @@
//////////////////// ////////////////////
function isDescriptionSupported(data) {
ctrl.isDescriptionSupported = data.data;
}
function getMaxInstances() { function getMaxInstances() {
return $scope.model.novaLimits.maxTotalInstances; return $scope.model.novaLimits.maxTotalInstances;
} }

View File

@ -23,15 +23,24 @@
beforeEach(module('horizon.dashboard.project')); beforeEach(module('horizon.dashboard.project'));
describe('LaunchInstanceDetailsController', function() { 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) { beforeEach(module(function($provide) {
$provide.value('horizon.framework.widgets.charts.donutChartSettings', noop); $provide.value('horizon.framework.widgets.charts.donutChartSettings', noop);
$provide.value('horizon.framework.widgets.charts.quotaChartDefaults', 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) { beforeEach(inject(function($injector, $controller, _$q_, _$rootScope_) {
scope = $rootScope.$new(); scope = _$rootScope_.$new();
$q = _$q_;
deferred = $q.defer(); deferred = $q.defer();
scope.initPromise = deferred.promise; scope.initPromise = deferred.promise;
@ -47,11 +56,21 @@
} }
}; };
novaAPI = $injector.get('horizon.app.core.openstack-service-api.nova');
ctrl = $controller('LaunchInstanceDetailsController', { $scope: scope }); ctrl = $controller('LaunchInstanceDetailsController', { $scope: scope });
scope.$apply(); 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() { it('should define error messages for invalid fields', function() {
expect(ctrl.instanceNameError).toBeDefined(); expect(ctrl.instanceNameError).toBeDefined();
expect(ctrl.instanceCountError).toBeDefined(); expect(ctrl.instanceCountError).toBeDefined();

View File

@ -19,6 +19,12 @@
</span> </span>
</div> </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"> <div class="form-group">
<label class="control-label" translate for="availability-zone">Availability Zone</label> <label class="control-label" translate for="availability-zone">Availability Zone</label>
<div class="horizon-loading-bar" ng-if="!model.loaded.availabilityZones"> <div class="horizon-loading-bar" ng-if="!model.loaded.availabilityZones">

View File

@ -181,6 +181,7 @@
availability_zone: null, availability_zone: null,
admin_pass: null, admin_pass: null,
config_drive: false, config_drive: false,
description: null,
// REQUIRED Server Key. Null allowed. // REQUIRED Server Key. Null allowed.
user_data: '', user_data: '',
disk_config: 'AUTO', disk_config: 'AUTO',

View File

@ -822,7 +822,7 @@
// This is here to ensure that as people add/change items, they // This is here to ensure that as people add/change items, they
// don't forget to implement tests for them. // don't forget to implement tests for them.
it('has the right number of properties', function() { 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() { it('sets availability zone to null', function() {
@ -833,6 +833,10 @@
expect(model.newInstanceSpec.admin_pass).toBeNull(); expect(model.newInstanceSpec.admin_pass).toBeNull();
}); });
it('sets description to null', function() {
expect(model.newInstanceSpec.description).toBeNull();
});
it('sets config drive to false', function() { it('sets config drive to false', function() {
expect(model.newInstanceSpec.config_drive).toBe(false); expect(model.newInstanceSpec.config_drive).toBe(false);
}); });

View File

@ -42,6 +42,7 @@
getConsoleInfo: getConsoleInfo, getConsoleInfo: getConsoleInfo,
getServerVolumes: getServerVolumes, getServerVolumes: getServerVolumes,
getServerSecurityGroups: getServerSecurityGroups, getServerSecurityGroups: getServerSecurityGroups,
isFeatureSupported: isFeatureSupported,
getKeypairs: getKeypairs, getKeypairs: getKeypairs,
createKeypair: createKeypair, createKeypair: createKeypair,
getKeypair: getKeypair, 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 // Nova Services
/** /**

View File

@ -40,6 +40,15 @@
}); });
var tests = [ var tests = [
{
"func": "isFeatureSupported",
"method": "get",
"path": "/api/nova/features/fake",
"error": "Unable to check the Nova service feature.",
"testInput": [
"fake"
]
},
{ {
"func": "getServices", "func": "getServices",
"method": "get", "method": "get",

View File

@ -969,3 +969,12 @@ class NovaRestTestCase(test.TestCase):
self.assertEqual(response.content.decode('utf-8'), self.assertEqual(response.content.decode('utf-8'),
'"Service Nova is disabled."') '"Service Nova is disabled."')
nc.tenant_quota_update.assert_not_called() 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')