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_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"],

View File

@ -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

View File

@ -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()

View File

@ -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>

View File

@ -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;
}

View File

@ -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();

View File

@ -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">

View File

@ -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',

View File

@ -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);
});

View File

@ -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
/**

View File

@ -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",

View File

@ -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')