Checks min requirements before trying to launch an instance
Server side version. An image can have properties min_ram/min_disk that prevents to launch instance with a flavor doesn't have enough resources. Notes: Adds in create_instance.py a solution to check requirements Adds in glance_data.py ram/disk properties for and image (default:0) Fix in tests.py, some ut needed to be updated Adds in tests.py two new tests to check min_disk and min_ram Change-Id: Ia4a9e4ed729321f70de93296e3257898a52c8e32 Implements: blueprint image-flavor-minimun-requirements
This commit is contained in:
parent
c1fbaa8ee5
commit
a63cfc102b
@ -1190,6 +1190,8 @@ class InstanceTests(test.TestCase):
|
|||||||
admin_pass=u'')
|
admin_pass=u'')
|
||||||
quotas.tenant_quota_usages(IsA(http.HttpRequest)) \
|
quotas.tenant_quota_usages(IsA(http.HttpRequest)) \
|
||||||
.AndReturn(quota_usages)
|
.AndReturn(quota_usages)
|
||||||
|
api.nova.flavor_list(IsA(http.HttpRequest)) \
|
||||||
|
.AndReturn(self.flavors.list())
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
@ -1661,6 +1663,8 @@ class InstanceTests(test.TestCase):
|
|||||||
.AndRaise(self.exceptions.keystone)
|
.AndRaise(self.exceptions.keystone)
|
||||||
quotas.tenant_quota_usages(IsA(http.HttpRequest)) \
|
quotas.tenant_quota_usages(IsA(http.HttpRequest)) \
|
||||||
.AndReturn(quota_usages)
|
.AndReturn(quota_usages)
|
||||||
|
api.nova.flavor_list(IsA(http.HttpRequest)) \
|
||||||
|
.AndReturn(self.flavors.list())
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
@ -1754,6 +1758,8 @@ class InstanceTests(test.TestCase):
|
|||||||
.AndReturn(self.limits['absolute'])
|
.AndReturn(self.limits['absolute'])
|
||||||
quotas.tenant_quota_usages(IsA(http.HttpRequest)) \
|
quotas.tenant_quota_usages(IsA(http.HttpRequest)) \
|
||||||
.AndReturn(quota_usages)
|
.AndReturn(quota_usages)
|
||||||
|
api.nova.flavor_list(IsA(http.HttpRequest)) \
|
||||||
|
.AndReturn(self.flavors.list())
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
@ -1776,6 +1782,111 @@ class InstanceTests(test.TestCase):
|
|||||||
|
|
||||||
self.assertContains(res, "greater than or equal to 1")
|
self.assertContains(res, "greater than or equal to 1")
|
||||||
|
|
||||||
|
@test.create_stubs({api.glance: ('image_list_detailed',),
|
||||||
|
api.neutron: ('network_list',
|
||||||
|
'profile_list',),
|
||||||
|
api.nova: ('extension_supported',
|
||||||
|
'flavor_list',
|
||||||
|
'keypair_list',
|
||||||
|
'tenant_absolute_limits',
|
||||||
|
'availability_zone_list',),
|
||||||
|
api.network: ('security_group_list',),
|
||||||
|
cinder: ('volume_list',
|
||||||
|
'volume_snapshot_list',),
|
||||||
|
quotas: ('tenant_quota_usages',)})
|
||||||
|
def _test_launch_form_instance_requirement_error(self, image, flavor):
|
||||||
|
keypair = self.keypairs.first()
|
||||||
|
server = self.servers.first()
|
||||||
|
volume = self.volumes.first()
|
||||||
|
sec_group = self.security_groups.first()
|
||||||
|
avail_zone = self.availability_zones.first()
|
||||||
|
customization_script = 'user data'
|
||||||
|
device_name = u'vda'
|
||||||
|
volume_choice = "%s:vol" % volume.id
|
||||||
|
quota_usages = self.quota_usages.first()
|
||||||
|
|
||||||
|
api.nova.extension_supported('BlockDeviceMappingV2Boot',
|
||||||
|
IsA(http.HttpRequest)) \
|
||||||
|
.AndReturn(True)
|
||||||
|
api.nova.flavor_list(IsA(http.HttpRequest)) \
|
||||||
|
.AndReturn(self.flavors.list())
|
||||||
|
api.nova.keypair_list(IsA(http.HttpRequest)) \
|
||||||
|
.AndReturn(self.keypairs.list())
|
||||||
|
api.network.security_group_list(IsA(http.HttpRequest)) \
|
||||||
|
.AndReturn(self.security_groups.list())
|
||||||
|
api.nova.availability_zone_list(IsA(http.HttpRequest)) \
|
||||||
|
.AndReturn(self.availability_zones.list())
|
||||||
|
api.glance.image_list_detailed(IsA(http.HttpRequest),
|
||||||
|
filters={'is_public': True,
|
||||||
|
'status': 'active'}) \
|
||||||
|
.AndReturn([self.images.list(), False])
|
||||||
|
api.glance.image_list_detailed(IsA(http.HttpRequest),
|
||||||
|
filters={'property-owner_id': self.tenant.id,
|
||||||
|
'status': 'active'}) \
|
||||||
|
.AndReturn([[], False])
|
||||||
|
api.neutron.network_list(IsA(http.HttpRequest),
|
||||||
|
tenant_id=self.tenant.id,
|
||||||
|
shared=False) \
|
||||||
|
.AndReturn(self.networks.list()[:1])
|
||||||
|
api.neutron.network_list(IsA(http.HttpRequest),
|
||||||
|
shared=True) \
|
||||||
|
.AndReturn(self.networks.list()[1:])
|
||||||
|
# TODO(absubram): Remove if clause and create separate
|
||||||
|
# test stubs for when profile_support is being used.
|
||||||
|
# Additionally ensure those are always run even in default setting
|
||||||
|
if api.neutron.is_port_profiles_supported():
|
||||||
|
policy_profiles = self.policy_profiles.list()
|
||||||
|
api.neutron.profile_list(IsA(http.HttpRequest),
|
||||||
|
'policy').AndReturn(policy_profiles)
|
||||||
|
cinder.volume_list(IsA(http.HttpRequest)) \
|
||||||
|
.AndReturn(self.volumes.list())
|
||||||
|
cinder.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([])
|
||||||
|
|
||||||
|
api.nova.flavor_list(IsA(http.HttpRequest)) \
|
||||||
|
.AndReturn(self.flavors.list())
|
||||||
|
api.nova.tenant_absolute_limits(IsA(http.HttpRequest)) \
|
||||||
|
.AndReturn(self.limits['absolute'])
|
||||||
|
quotas.tenant_quota_usages(IsA(http.HttpRequest)) \
|
||||||
|
.AndReturn(quota_usages)
|
||||||
|
api.nova.flavor_list(IsA(http.HttpRequest)) \
|
||||||
|
.AndReturn(self.flavors.list())
|
||||||
|
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
form_data = {'flavor': flavor.id,
|
||||||
|
'source_type': 'image_id',
|
||||||
|
'image_id': image.id,
|
||||||
|
'availability_zone': avail_zone.zoneName,
|
||||||
|
'keypair': keypair.name,
|
||||||
|
'name': server.name,
|
||||||
|
'customization_script': customization_script,
|
||||||
|
'project_id': self.tenants.first().id,
|
||||||
|
'user_id': self.user.id,
|
||||||
|
'groups': sec_group.name,
|
||||||
|
'volume_type': 'volume_id',
|
||||||
|
'volume_id': volume_choice,
|
||||||
|
'device_name': device_name,
|
||||||
|
'count': 1}
|
||||||
|
|
||||||
|
url = reverse('horizon:project:instances:launch')
|
||||||
|
res = self.client.post(url, form_data)
|
||||||
|
msg = "The flavor '%s' is too small" % flavor.name
|
||||||
|
self.assertContains(res, msg)
|
||||||
|
|
||||||
|
def test_launch_form_instance_requirement_error_disk(self):
|
||||||
|
flavor = self.flavors.first()
|
||||||
|
image = self.images.first()
|
||||||
|
image.min_ram = flavor.ram
|
||||||
|
image.min_disk = flavor.disk + 1
|
||||||
|
self._test_launch_form_instance_requirement_error(image, flavor)
|
||||||
|
|
||||||
|
def test_launch_form_instance_requirement_error_ram(self):
|
||||||
|
flavor = self.flavors.first()
|
||||||
|
image = self.images.first()
|
||||||
|
image.min_ram = flavor.ram + 1
|
||||||
|
image.min_disk = flavor.disk
|
||||||
|
self._test_launch_form_instance_requirement_error(image, flavor)
|
||||||
|
|
||||||
@test.create_stubs({api.nova: ('flavor_list', 'server_list',
|
@test.create_stubs({api.nova: ('flavor_list', 'server_list',
|
||||||
'tenant_absolute_limits',
|
'tenant_absolute_limits',
|
||||||
'extension_supported',),
|
'extension_supported',),
|
||||||
|
@ -46,6 +46,16 @@ from openstack_dashboard.dashboards.project.images_and_snapshots import utils
|
|||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _flavor_list(request):
|
||||||
|
"""Utility method to retrieve a list of flavor."""
|
||||||
|
try:
|
||||||
|
return api.nova.flavor_list(request)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(request,
|
||||||
|
_('Unable to retrieve instance flavors.'))
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
class SelectProjectUserAction(workflows.Action):
|
class SelectProjectUserAction(workflows.Action):
|
||||||
project_id = forms.ChoiceField(label=_("Project"))
|
project_id = forms.ChoiceField(label=_("Project"))
|
||||||
user_id = forms.ChoiceField(label=_("User"))
|
user_id = forms.ChoiceField(label=_("User"))
|
||||||
@ -132,6 +142,8 @@ class SetInstanceDetailsAction(workflows.Action):
|
|||||||
|
|
||||||
def __init__(self, request, context, *args, **kwargs):
|
def __init__(self, request, context, *args, **kwargs):
|
||||||
self._init_images_cache()
|
self._init_images_cache()
|
||||||
|
self.request = request
|
||||||
|
self.context = context
|
||||||
super(SetInstanceDetailsAction, self).__init__(
|
super(SetInstanceDetailsAction, self).__init__(
|
||||||
request, context, *args, **kwargs)
|
request, context, *args, **kwargs)
|
||||||
source_type_choices = [
|
source_type_choices = [
|
||||||
@ -183,6 +195,48 @@ class SetInstanceDetailsAction(workflows.Action):
|
|||||||
if not cleaned_data.get('image_id'):
|
if not cleaned_data.get('image_id'):
|
||||||
msg = _("You must select an image.")
|
msg = _("You must select an image.")
|
||||||
self._errors['image_id'] = self.error_class([msg])
|
self._errors['image_id'] = self.error_class([msg])
|
||||||
|
else:
|
||||||
|
# Prevents trying to launch an image needing more resources.
|
||||||
|
try:
|
||||||
|
image_id = cleaned_data.get('image_id')
|
||||||
|
# We want to retrieve details for a given image,
|
||||||
|
# however get_available_images uses a cache of image list,
|
||||||
|
# so it is used instead of image_get to reduce the number
|
||||||
|
# of API calls.
|
||||||
|
images = utils.get_available_images(
|
||||||
|
self.request,
|
||||||
|
self.context.get('project_id'),
|
||||||
|
self._images_cache)
|
||||||
|
image = [x for x in images if x.id == image_id][0]
|
||||||
|
except IndexError:
|
||||||
|
image = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
flavor_id = cleaned_data.get('flavor')
|
||||||
|
# We want to retrieve details for a given flavor,
|
||||||
|
# however flavor_list uses a memoized decorator
|
||||||
|
# so it is used instead of flavor_get to reduce the number
|
||||||
|
# of API calls.
|
||||||
|
flavors = _flavor_list(self.request)
|
||||||
|
flavor = [x for x in flavors if x.id == flavor_id][0]
|
||||||
|
except IndexError:
|
||||||
|
flavor = None
|
||||||
|
|
||||||
|
if image and flavor:
|
||||||
|
props_mapping = (("min_ram", "ram"), ("min_disk", "disk"))
|
||||||
|
for iprop, fprop in props_mapping:
|
||||||
|
if getattr(image, iprop) > 0 and \
|
||||||
|
getattr(image, iprop) > getattr(flavor, fprop):
|
||||||
|
msg = _("The flavor '%(flavor)s' is too small for "
|
||||||
|
"requested image.\n"
|
||||||
|
"Minimum requirements: "
|
||||||
|
"%(min_ram)s MB of RAM and "
|
||||||
|
"%(min_disk)s GB of Root Disk." %
|
||||||
|
{'flavor': flavor.name,
|
||||||
|
'min_ram': image.min_ram,
|
||||||
|
'min_disk': image.min_disk})
|
||||||
|
self._errors['image_id'] = self.error_class([msg])
|
||||||
|
break # Not necessary to continue the tests.
|
||||||
|
|
||||||
elif source_type == 'instance_snapshot_id':
|
elif source_type == 'instance_snapshot_id':
|
||||||
if not cleaned_data['instance_snapshot_id']:
|
if not cleaned_data['instance_snapshot_id']:
|
||||||
@ -236,24 +290,18 @@ class SetInstanceDetailsAction(workflows.Action):
|
|||||||
'"ram" instead.', sort_key)
|
'"ram" instead.', sort_key)
|
||||||
return getattr(flavor, 'ram')
|
return getattr(flavor, 'ram')
|
||||||
|
|
||||||
try:
|
flavors = _flavor_list(request)
|
||||||
flavors = api.nova.flavor_list(request)
|
if flavors:
|
||||||
flavor_sort = getattr(settings, 'CREATE_INSTANCE_FLAVOR_SORT', {})
|
flavor_sort = getattr(settings, 'CREATE_INSTANCE_FLAVOR_SORT', {})
|
||||||
rev = flavor_sort.get('reverse', False)
|
rev = flavor_sort.get('reverse', False)
|
||||||
sort_key = flavor_sort.get('key', 'ram')
|
sort_key = flavor_sort.get('key', 'ram')
|
||||||
|
|
||||||
if not callable(sort_key):
|
if not callable(sort_key):
|
||||||
key = lambda flavor: get_key(flavor, sort_key)
|
key = lambda flavor: get_key(flavor, sort_key)
|
||||||
else:
|
else:
|
||||||
key = sort_key
|
key = sort_key
|
||||||
|
return [(flavor.id, "%s" % flavor.name)
|
||||||
flavor_list = [(flavor.id, "%s" % flavor.name)
|
for flavor in sorted(flavors, key=key, reverse=rev)]
|
||||||
for flavor in sorted(flavors, key=key, reverse=rev)]
|
return []
|
||||||
except Exception:
|
|
||||||
flavor_list = []
|
|
||||||
exceptions.handle(request,
|
|
||||||
_('Unable to retrieve instance flavors.'))
|
|
||||||
return flavor_list
|
|
||||||
|
|
||||||
def populate_availability_zone_choices(self, request, context):
|
def populate_availability_zone_choices(self, request, context):
|
||||||
try:
|
try:
|
||||||
@ -277,12 +325,11 @@ class SetInstanceDetailsAction(workflows.Action):
|
|||||||
try:
|
try:
|
||||||
extra['usages'] = api.nova.tenant_absolute_limits(self.request)
|
extra['usages'] = api.nova.tenant_absolute_limits(self.request)
|
||||||
extra['usages_json'] = json.dumps(extra['usages'])
|
extra['usages_json'] = json.dumps(extra['usages'])
|
||||||
flavors = json.dumps([f._info for f in
|
flavors = json.dumps([f._info for f in _flavor_list(self.request)])
|
||||||
api.nova.flavor_list(self.request)])
|
|
||||||
extra['flavors'] = flavors
|
extra['flavors'] = flavors
|
||||||
images = utils.get_available_images(self.request,
|
images = utils.get_available_images(self.request,
|
||||||
self.initial['project_id'],
|
self.initial['project_id'],
|
||||||
self._images_cache)
|
self._images_cache)
|
||||||
if images is not None:
|
if images is not None:
|
||||||
attrs = [{'id': i.id,
|
attrs = [{'id': i.id,
|
||||||
'min_disk': getattr(i, 'min_disk', 0),
|
'min_disk': getattr(i, 'min_disk', 0),
|
||||||
@ -664,6 +711,7 @@ class LaunchInstance(workflows.Workflow):
|
|||||||
msg = (_('Port not created for profile-id (%s).') %
|
msg = (_('Port not created for profile-id (%s).') %
|
||||||
context['profile_id'])
|
context['profile_id'])
|
||||||
exceptions.handle(request, msg)
|
exceptions.handle(request, msg)
|
||||||
|
|
||||||
if port and port.id:
|
if port and port.id:
|
||||||
nics = [{"port-id": port.id}]
|
nics = [{"port-id": port.id}]
|
||||||
|
|
||||||
|
@ -481,6 +481,7 @@ class VolumeViewTests(test.TestCase):
|
|||||||
'volumesUsed': len(self.volumes.list()),
|
'volumesUsed': len(self.volumes.list()),
|
||||||
'maxTotalVolumes': 6}
|
'maxTotalVolumes': 6}
|
||||||
image = self.images.get(name="protected_images")
|
image = self.images.get(name="protected_images")
|
||||||
|
image.min_disk = 30
|
||||||
formData = {'name': u'A Volume I Am Making',
|
formData = {'name': u'A Volume I Am Making',
|
||||||
'description': u'This is a volume I am making for a test.',
|
'description': u'This is a volume I am making for a test.',
|
||||||
'method': u'CreateForm',
|
'method': u'CreateForm',
|
||||||
|
@ -63,7 +63,8 @@ def data(TEST):
|
|||||||
'container_format': 'novaImage',
|
'container_format': 'novaImage',
|
||||||
'properties': {'image_type': u'image'},
|
'properties': {'image_type': u'image'},
|
||||||
'is_public': True,
|
'is_public': True,
|
||||||
'protected': False}
|
'protected': False,
|
||||||
|
'min_ram': 0}
|
||||||
public_image = images.Image(images.ImageManager(None), image_dict)
|
public_image = images.Image(images.ImageManager(None), image_dict)
|
||||||
|
|
||||||
image_dict = {'id': 'a001c047-22f8-47d0-80a1-8ec94a9524fe',
|
image_dict = {'id': 'a001c047-22f8-47d0-80a1-8ec94a9524fe',
|
||||||
@ -74,7 +75,8 @@ def data(TEST):
|
|||||||
'owner': TEST.tenant.id,
|
'owner': TEST.tenant.id,
|
||||||
'container_format': 'aki',
|
'container_format': 'aki',
|
||||||
'is_public': False,
|
'is_public': False,
|
||||||
'protected': False}
|
'protected': False,
|
||||||
|
'min_ram': 0}
|
||||||
private_image = images.Image(images.ImageManager(None), image_dict)
|
private_image = images.Image(images.ImageManager(None), image_dict)
|
||||||
|
|
||||||
image_dict = {'id': 'd6936c86-7fec-474a-85c5-5e467b371c3c',
|
image_dict = {'id': 'd6936c86-7fec-474a-85c5-5e467b371c3c',
|
||||||
@ -86,7 +88,8 @@ def data(TEST):
|
|||||||
'container_format': 'novaImage',
|
'container_format': 'novaImage',
|
||||||
'properties': {'image_type': u'image'},
|
'properties': {'image_type': u'image'},
|
||||||
'is_public': True,
|
'is_public': True,
|
||||||
'protected': True}
|
'protected': True,
|
||||||
|
'min_ram': 0}
|
||||||
protected_image = images.Image(images.ImageManager(None), image_dict)
|
protected_image = images.Image(images.ImageManager(None), image_dict)
|
||||||
|
|
||||||
image_dict = {'id': '278905a6-4b52-4d1e-98f9-8c57bb25ba32',
|
image_dict = {'id': '278905a6-4b52-4d1e-98f9-8c57bb25ba32',
|
||||||
@ -98,7 +101,8 @@ def data(TEST):
|
|||||||
'container_format': 'novaImage',
|
'container_format': 'novaImage',
|
||||||
'properties': {'image_type': u'image'},
|
'properties': {'image_type': u'image'},
|
||||||
'is_public': True,
|
'is_public': True,
|
||||||
'protected': False}
|
'protected': False,
|
||||||
|
'min_ram': 0}
|
||||||
public_image2 = images.Image(images.ImageManager(None), image_dict)
|
public_image2 = images.Image(images.ImageManager(None), image_dict)
|
||||||
|
|
||||||
image_dict = {'id': '710a1acf-a3e3-41dd-a32d-5d6b6c86ea10',
|
image_dict = {'id': '710a1acf-a3e3-41dd-a32d-5d6b6c86ea10',
|
||||||
@ -109,7 +113,8 @@ def data(TEST):
|
|||||||
'owner': TEST.tenant.id,
|
'owner': TEST.tenant.id,
|
||||||
'container_format': 'aki',
|
'container_format': 'aki',
|
||||||
'is_public': False,
|
'is_public': False,
|
||||||
'protected': False}
|
'protected': False,
|
||||||
|
'min_ram': 0}
|
||||||
private_image2 = images.Image(images.ImageManager(None), image_dict)
|
private_image2 = images.Image(images.ImageManager(None), image_dict)
|
||||||
|
|
||||||
image_dict = {'id': '7cd892fd-5652-40f3-a450-547615680132',
|
image_dict = {'id': '7cd892fd-5652-40f3-a450-547615680132',
|
||||||
@ -120,7 +125,8 @@ def data(TEST):
|
|||||||
'owner': TEST.tenant.id,
|
'owner': TEST.tenant.id,
|
||||||
'container_format': 'aki',
|
'container_format': 'aki',
|
||||||
'is_public': False,
|
'is_public': False,
|
||||||
'protected': False}
|
'protected': False,
|
||||||
|
'min_ram': 0}
|
||||||
private_image3 = images.Image(images.ImageManager(None), image_dict)
|
private_image3 = images.Image(images.ImageManager(None), image_dict)
|
||||||
|
|
||||||
# A shared image. Not public and not local tenant.
|
# A shared image. Not public and not local tenant.
|
||||||
@ -132,7 +138,8 @@ def data(TEST):
|
|||||||
'owner': 'someothertenant',
|
'owner': 'someothertenant',
|
||||||
'container_format': 'aki',
|
'container_format': 'aki',
|
||||||
'is_public': False,
|
'is_public': False,
|
||||||
'protected': False}
|
'protected': False,
|
||||||
|
'min_ram': 0}
|
||||||
shared_image1 = images.Image(images.ImageManager(None), image_dict)
|
shared_image1 = images.Image(images.ImageManager(None), image_dict)
|
||||||
|
|
||||||
# "Official" image. Public and tenant matches an entry
|
# "Official" image. Public and tenant matches an entry
|
||||||
@ -145,7 +152,8 @@ def data(TEST):
|
|||||||
'owner': 'officialtenant',
|
'owner': 'officialtenant',
|
||||||
'container_format': 'aki',
|
'container_format': 'aki',
|
||||||
'is_public': True,
|
'is_public': True,
|
||||||
'protected': False}
|
'protected': False,
|
||||||
|
'min_ram': 0}
|
||||||
official_image1 = images.Image(images.ImageManager(None), image_dict)
|
official_image1 = images.Image(images.ImageManager(None), image_dict)
|
||||||
|
|
||||||
image_dict = {'id': 'a67e7d45-fe1e-4c5c-bf08-44b4a4964822',
|
image_dict = {'id': 'a67e7d45-fe1e-4c5c-bf08-44b4a4964822',
|
||||||
|
Loading…
Reference in New Issue
Block a user