Merge "[Micro version] Support description for instance"
This commit is contained in:
commit
2e1b80a1f5
@ -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"],
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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">
|
||||||
|
@ -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',
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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",
|
||||||
|
@ -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')
|
||||||
|
Loading…
Reference in New Issue
Block a user