Merge "update scheduler to use image-traits"

This commit is contained in:
Zuul
2018-05-11 22:34:20 +00:00
committed by Gerrit Code Review
4 changed files with 391 additions and 33 deletions
+38 -2
View File
@@ -101,7 +101,7 @@ class ResourceRequest(object):
return
@classmethod
def from_extra_specs(cls, extra_specs):
def from_extra_specs(cls, extra_specs, req=None):
"""Processes resources and traits in numbered groupings in extra_specs.
Examines extra_specs for items of the following forms:
@@ -111,10 +111,16 @@ class ResourceRequest(object):
"trait$N:$TRAIT_NAME": "required"
:param extra_specs: The flavor extra_specs dict.
:param req: the ResourceRequest object to add the requirements to or
None to create a new ResourceRequest
:return: A ResourceRequest object representing the resources and
required traits in the extra_specs.
"""
ret = cls()
if req is not None:
ret = req
else:
ret = cls()
for key, val in extra_specs.items():
match = cls.XS_KEYPAT.match(key)
if not match:
@@ -135,6 +141,31 @@ class ResourceRequest(object):
return ret
@classmethod
def from_image_props(cls, image_meta_props, req=None):
"""Processes image properties and adds trait requirements to the
ResourceRequest
:param image_meta_props: The ImageMetaProps object.
:param req: the ResourceRequest object to add the requirements to or
None to create a new ResourceRequest
:return: A ResourceRequest object representing the required traits on
the image.
"""
if req is not None:
ret = req
else:
ret = cls()
if 'traits_required' in image_meta_props:
for trait in image_meta_props.traits_required:
# required traits from the image are always added to the
# unnumbered request group, granular request groups are not
# supported in image traits
ret._add_trait(None, trait, "required")
return ret
def resource_groups(self):
for rg in self._rg_by_id.values():
yield rg.resources
@@ -332,6 +363,11 @@ def resources_from_request_spec(spec_obj):
# Start with an empty one
res_req = ResourceRequest()
# Process any image properties
if 'image' in spec_obj and 'properties' in spec_obj.image:
res_req = ResourceRequest.from_image_props(spec_obj.image.properties,
req=res_req)
# Add the (remaining) items from the spec_resources to the sharing group
for rclass, amount in spec_resources.items():
res_req.get_request_group(None).resources[rclass] = amount
+16 -6
View File
@@ -1514,6 +1514,7 @@ class CinderFixtureNewAttachFlow(fixtures.Fixture):
# This represents a bootable image-backed volume to test
# boot-from-volume scenarios.
IMAGE_BACKED_VOL = '6ca404f3-d844-4169-bb96-bc792f37de98'
IMAGE_WITH_TRAITS_BACKED_VOL = '6194fc02-c60e-4a01-a8e5-600798208b5f'
def __init__(self, test):
super(CinderFixtureNewAttachFlow, self).__init__()
@@ -1592,14 +1593,23 @@ class CinderFixtureNewAttachFlow(fixtures.Fixture):
}
# Check for our special image-backed volume.
if volume_id == self.IMAGE_BACKED_VOL:
if (volume_id == self.IMAGE_BACKED_VOL or
volume_id == self.IMAGE_WITH_TRAITS_BACKED_VOL):
# Make it a bootable volume.
volume['bootable'] = True
# Add the image_id metadata.
volume['volume_image_metadata'] = {
# There would normally be more image metadata in here...
'image_id': '155d900f-4e14-4e4c-a73d-069cbf4541e6'
}
if volume_id == self.IMAGE_BACKED_VOL:
# Add the image_id metadata.
volume['volume_image_metadata'] = {
# There would normally be more image metadata in here.
'image_id': '155d900f-4e14-4e4c-a73d-069cbf4541e6'
}
elif volume_id == self.IMAGE_WITH_TRAITS_BACKED_VOL:
# Add the image_id metadata with traits.
volume['volume_image_metadata'] = {
'image_id': '155d900f-4e14-4e4c-a73d-069cbf4541e6',
"container_format": "ami",
"trait:HW_CPU_X86_SGX": "required",
}
return volume
+204 -15
View File
@@ -3706,36 +3706,225 @@ class TraitsBasedSchedulingTest(ProviderUsageBaseTestCase):
self.admin_api.post_extra_spec(
self.flavor_with_trait['id'],
{'extra_specs': {'trait:HW_CPU_X86_VMX': 'required'}})
self.flavor_without_trait = flavors[1]
# Note that we're using v2.35 explicitly as the api returns 404
# starting with 2.36
with nova.utils.temporary_mutation(self.api, microversion='2.35'):
images = self.api.get_images()
self.image_id_with_trait = images[0]['id']
self.api.api_put('/images/%s/metadata' % self.image_id_with_trait,
{'metadata': {
'trait:HW_CPU_X86_SGX': 'required'}})
self.image_id_without_trait = images[1]['id']
def _create_server_with_traits(self, flavor_id, image_id):
"""Create a server with given flavor and image id's
:param flavor_id: the flavor id
:param image_id: the image id
:return: create server response
"""
def _create_server(self):
# Create the server using the flavor with the required trait.
server_req = self._build_minimal_create_server_request(
self.api, 'trait-based-server',
image_uuid='76fa36fc-c930-4bf3-8c8a-ea2a2420deb6',
flavor_id=self.flavor_with_trait['id'], networks='none')
image_uuid=image_id,
flavor_id=flavor_id, networks='none')
return self.api.post_server({'server': server_req})
def test_traits_based_scheduling(self):
"""Tests that a server create request using a required trait ends
up on the single compute node resource provider that also has that
def _create_server_with_volume(self, flavor_id, volume_id):
"""Create a server with block device mapping(volume) with the given
flavor and volume id's
:param flavor_id: the flavor id
:param volume_id: the volume id
:return: create server response
"""
server_req_body = {
# There is no imageRef because this is boot from volume.
'server': {
'flavorRef': flavor_id,
'name': 'test_image_trait_on_volume_backed',
# We don't care about networking for this test. This
# requires
# microversion >= 2.37.
'networks': 'none',
'block_device_mapping_v2': [{
'boot_index': 0,
'uuid': volume_id,
'source_type': 'volume',
'destination_type': 'volume'
}]
}
}
server = self.api.post_server(server_req_body)
return server
def test_flavor_traits_based_scheduling(self):
"""Tests that a server create request using a required trait on flavor
ends up on the single compute node resource provider that also has that
trait in Placement.
"""
# Decorate the compute_with_trait resource provider with that same
# trait.
# Decorate compute1 resource provider with that same trait.
rp_uuid = self._get_provider_uuid_by_host(self.compute1.host)
self._set_provider_traits(rp_uuid, ['HW_CPU_X86_VMX'])
server = self._create_server()
# Create server using only flavor trait
server = self._create_server_with_traits(self.flavor_with_trait['id'],
self.image_id_without_trait)
server = self._wait_for_state_change(self.admin_api, server, 'ACTIVE')
# Assert the server ended up on the expected compute host that has
# the required trait.
self.assertEqual(self.compute1.host, server['OS-EXT-SRV-ATTR:host'])
def test_traits_based_scheduling_no_valid_host(self):
"""Tests that a server create request using a required trait
fails to find a valid host since no compute node resource providers
have the trait.
def test_image_traits_based_scheduling(self):
"""Tests that a server create request using a required trait on image
ends up on the single compute node resource provider that also has that
trait in Placement.
"""
server = self._create_server()
# Decorate compute2 resource provider with image trait.
rp_uuid = self._get_provider_uuid_by_host(self.compute2.host)
self._set_provider_traits(rp_uuid, ['HW_CPU_X86_SGX'])
# Create server using only image trait
server = self._create_server_with_traits(
self.flavor_without_trait['id'], self.image_id_with_trait)
server = self._wait_for_state_change(self.admin_api, server, 'ACTIVE')
# Assert the server ended up on the expected compute host that has
# the required trait.
self.assertEqual(self.compute2.host, server['OS-EXT-SRV-ATTR:host'])
def test_flavor_image_traits_based_scheduling(self):
"""Tests that a server create request using a required trait on flavor
AND a required trait on the image ends up on the single compute node
resource provider that also has that trait in Placement.
"""
# Decorate compute2 resource provider with both flavor and image trait.
rp_uuid = self._get_provider_uuid_by_host(self.compute2.host)
self._set_provider_traits(rp_uuid, ['HW_CPU_X86_VMX',
'HW_CPU_X86_SGX'])
# Create server using flavor and image trait
server = self._create_server_with_traits(
self.flavor_with_trait['id'], self.image_id_with_trait)
server = self._wait_for_state_change(self.admin_api, server, 'ACTIVE')
# Assert the server ended up on the expected compute host that has
# the required trait.
self.assertEqual(self.compute2.host, server['OS-EXT-SRV-ATTR:host'])
def test_image_trait_on_volume_backed_instance(self):
"""Tests that when trying to launch a volume-backed instance with a
required trait on the image, the instance ends up on the single compute
node resource provider that also has that trait in Placement.
"""
# Decorate compute2 resource provider with volume image metadata trait.
rp_uuid = self._get_provider_uuid_by_host(self.compute2.host)
self._set_provider_traits(rp_uuid, ['HW_CPU_X86_SGX'])
self.useFixture(nova_fixtures.CinderFixtureNewAttachFlow(self))
# Create our server with a volume containing the image meta data with a
# required trait
server = self._create_server_with_volume(
self.flavor_without_trait['id'],
nova_fixtures.CinderFixtureNewAttachFlow.
IMAGE_WITH_TRAITS_BACKED_VOL)
server = self._wait_for_state_change(self.admin_api, server, 'ACTIVE')
# Assert the server ended up on the expected compute host that has
# the required trait.
self.assertEqual(self.compute2.host, server['OS-EXT-SRV-ATTR:host'])
def test_flavor_image_trait_on_volume_backed_instance(self):
"""Tests that when trying to launch a volume-backed instance with a
required trait on flavor AND a required trait on the image,
the instance ends up on the single compute node resource provider that
also has those traits in Placement.
"""
# Decorate compute2 resource provider with volume image metadatz trait.
rp_uuid = self._get_provider_uuid_by_host(self.compute2.host)
self._set_provider_traits(rp_uuid, ['HW_CPU_X86_VMX',
'HW_CPU_X86_SGX'])
self.useFixture(nova_fixtures.CinderFixtureNewAttachFlow(self))
# Create our server with a flavor trait and a volume containing the
# image meta data with a required trait
server = self._create_server_with_volume(
self.flavor_with_trait['id'],
nova_fixtures.CinderFixtureNewAttachFlow.
IMAGE_WITH_TRAITS_BACKED_VOL)
server = self._wait_for_state_change(self.admin_api, server, 'ACTIVE')
# Assert the server ended up on the expected compute host that has
# the required trait.
self.assertEqual(self.compute2.host, server['OS-EXT-SRV-ATTR:host'])
def test_flavor_traits_based_scheduling_no_valid_host(self):
"""Tests that a server create request using a required trait expressed
in flavor fails to find a valid host since no compute node resource
providers have the trait.
"""
# Decorate compute1 resource provider with the image trait.
rp_uuid = self._get_provider_uuid_by_host(self.compute1.host)
self._set_provider_traits(rp_uuid, ['HW_CPU_X86_SGX'])
server = self._create_server_with_traits(self.flavor_with_trait['id'],
self.image_id_without_trait)
# The server should go to ERROR state because there is no valid host.
server = self._wait_for_state_change(self.admin_api, server, 'ERROR')
self.assertIsNone(server['OS-EXT-SRV-ATTR:host'])
# Make sure the failure was due to NoValidHost by checking the fault.
self.assertIn('fault', server)
self.assertIn('No valid host', server['fault']['message'])
def test_image_traits_based_scheduling_no_valid_host(self):
"""Tests that a server create request using a required trait expressed
in image fails to find a valid host since no compute node resource
providers have the trait.
"""
# Decorate compute1 resource provider with that flavor trait.
rp_uuid = self._get_provider_uuid_by_host(self.compute1.host)
self._set_provider_traits(rp_uuid, ['HW_CPU_X86_VMX'])
server = self._create_server_with_traits(
self.flavor_without_trait['id'], self.image_id_with_trait)
# The server should go to ERROR state because there is no valid host.
server = self._wait_for_state_change(self.admin_api, server, 'ERROR')
self.assertIsNone(server['OS-EXT-SRV-ATTR:host'])
# Make sure the failure was due to NoValidHost by checking the fault.
self.assertIn('fault', server)
self.assertIn('No valid host', server['fault']['message'])
def test_flavor_image_traits_based_scheduling_no_valid_host(self):
"""Tests that a server create request using a required trait expressed
in flavor AND a required trait expressed in the image fails to find a
valid host since no compute node resource providers have the trait.
"""
server = self._create_server_with_traits(
self.flavor_with_trait['id'], self.image_id_with_trait)
# The server should go to ERROR state because there is no valid host.
server = self._wait_for_state_change(self.admin_api, server, 'ERROR')
self.assertIsNone(server['OS-EXT-SRV-ATTR:host'])
# Make sure the failure was due to NoValidHost by checking the fault.
self.assertIn('fault', server)
self.assertIn('No valid host', server['fault']['message'])
def test_image_trait_on_volume_backed_instance_no_valid_host(self):
"""Tests that when trying to launch a volume-backed instance with a
required trait on the image fails to find a valid host since no compute
node resource providers have the trait.
"""
self.useFixture(nova_fixtures.CinderFixtureNewAttachFlow(self))
# Create our server with a volume
server = self._create_server_with_volume(
self.flavor_without_trait['id'],
nova_fixtures.CinderFixtureNewAttachFlow.
IMAGE_WITH_TRAITS_BACKED_VOL)
# The server should go to ERROR state because there is no valid host.
server = self._wait_for_state_change(self.admin_api, server, 'ERROR')
self.assertIsNone(server['OS-EXT-SRV-ATTR:host'])
+133 -10
View File
@@ -36,12 +36,25 @@ class TestUtils(test.NoDBTestCase):
for ident in ex_by_id:
self.assertEqual(vars(ex_by_id[ident]), vars(ob_by_id[ident]))
def _test_resources_from_request_spec(self, flavor, expected):
fake_spec = objects.RequestSpec(flavor=flavor)
@staticmethod
def _get_image_with_traits():
image_prop = {
'properties': {
'trait:CUSTOM_IMAGE_TRAIT1': 'required',
'trait:CUSTOM_IMAGE_TRAIT2': 'required',
},
'id': 'c8b1790e-a07d-4971-b137-44f2432936cd'
}
image = objects.ImageMeta.from_dict(image_prop)
return image
def _test_resources_from_request_spec(self, expected, flavor,
image=objects.ImageMeta()):
fake_spec = objects.RequestSpec(flavor=flavor, image=image)
resources = utils.resources_from_request_spec(fake_spec)
self.assertResourceRequestsEqual(expected, resources)
def test_resources_from_request_spec(self):
def test_resources_from_request_spec_flavor_only(self):
flavor = objects.Flavor(vcpus=1,
memory_mb=1024,
root_gb=10,
@@ -56,7 +69,36 @@ class TestUtils(test.NoDBTestCase):
'DISK_GB': 15,
}
)
self._test_resources_from_request_spec(flavor, expected_resources)
self._test_resources_from_request_spec(expected_resources, flavor)
def test_resources_from_request_spec_flavor_and_image_traits(self):
image = self._get_image_with_traits()
flavor = objects.Flavor(vcpus=1,
memory_mb=1024,
root_gb=10,
ephemeral_gb=5,
swap=0,
extra_specs={
'trait:CUSTOM_FLAVOR_TRAIT': 'required',
'trait:CUSTOM_IMAGE_TRAIT2': 'required'})
expected_resources = utils.ResourceRequest()
expected_resources._rg_by_id[None] = plib.RequestGroup(
use_same_provider=False,
resources={
'VCPU': 1,
'MEMORY_MB': 1024,
'DISK_GB': 15,
},
required_traits={
# trait:CUSTOM_IMAGE_TRAIT2 is defined in both extra_specs and
# image metadata. We get a union of both.
'CUSTOM_IMAGE_TRAIT1',
'CUSTOM_IMAGE_TRAIT2',
'CUSTOM_FLAVOR_TRAIT',
}
)
self._test_resources_from_request_spec(expected_resources, flavor,
image)
def test_resources_from_request_spec_with_no_disk(self):
flavor = objects.Flavor(vcpus=1,
@@ -72,7 +114,7 @@ class TestUtils(test.NoDBTestCase):
'MEMORY_MB': 1024,
}
)
self._test_resources_from_request_spec(flavor, expected_resources)
self._test_resources_from_request_spec(expected_resources, flavor)
def test_get_resources_from_request_spec_custom_resource_class(self):
flavor = objects.Flavor(vcpus=1,
@@ -91,7 +133,7 @@ class TestUtils(test.NoDBTestCase):
"CUSTOM_TEST_CLASS": 1,
}
)
self._test_resources_from_request_spec(flavor, expected_resources)
self._test_resources_from_request_spec(expected_resources, flavor)
def test_get_resources_from_request_spec_override_flavor_amounts(self):
flavor = objects.Flavor(vcpus=1,
@@ -112,7 +154,7 @@ class TestUtils(test.NoDBTestCase):
"DISK_GB": 99,
}
)
self._test_resources_from_request_spec(flavor, expected_resources)
self._test_resources_from_request_spec(expected_resources, flavor)
def test_get_resources_from_request_spec_remove_flavor_amounts(self):
flavor = objects.Flavor(vcpus=1,
@@ -130,7 +172,7 @@ class TestUtils(test.NoDBTestCase):
"MEMORY_MB": 1024,
}
)
self._test_resources_from_request_spec(flavor, expected_resources)
self._test_resources_from_request_spec(expected_resources, flavor)
def test_get_resources_from_request_spec_vgpu(self):
flavor = objects.Flavor(vcpus=1,
@@ -152,7 +194,7 @@ class TestUtils(test.NoDBTestCase):
"VGPU_DISPLAY_HEAD": 1,
}
)
self._test_resources_from_request_spec(flavor, expected_resources)
self._test_resources_from_request_spec(expected_resources, flavor)
def test_get_resources_from_request_spec_bad_std_resource_class(self):
flavor = objects.Flavor(vcpus=1,
@@ -228,7 +270,7 @@ class TestUtils(test.NoDBTestCase):
'SRIOV_NET_VF': 1,
}
)
self._test_resources_from_request_spec(flavor, expected_resources)
self._test_resources_from_request_spec(expected_resources, flavor)
def test_resources_from_request_spec_aggregates(self):
destination = objects.Destination()
@@ -442,6 +484,87 @@ class TestUtils(test.NoDBTestCase):
self.assertResourceRequestsEqual(
expected, utils.ResourceRequest.from_extra_specs(extra_specs))
def test_resource_request_from_extra_specs_append_request(self):
extra_specs = {
'resources:VCPU': '2',
'resources:MEMORY_MB': '2048',
'trait:HW_CPU_X86_AVX': 'required',
}
existing_req = utils.ResourceRequest()
existing_req._rg_by_id[None] = plib.RequestGroup(
use_same_provider=False,
required_traits={
'CUSTOM_MAGIC',
}
)
# Build up a ResourceRequest from the inside to compare against.
expected = utils.ResourceRequest()
expected._rg_by_id[None] = plib.RequestGroup(
use_same_provider=False,
resources={
'VCPU': 2,
'MEMORY_MB': 2048,
},
required_traits={
# In addition to traits from extra spec, we get traits from a
# previous existing resource request
'HW_CPU_X86_AVX',
'CUSTOM_MAGIC',
}
)
self.assertResourceRequestsEqual(
expected, utils.ResourceRequest.from_extra_specs(extra_specs,
req=existing_req))
def test_resource_request_from_image_props(self):
props = {'trait:CUSTOM_TRUSTED': 'required'}
image_meta_props = objects.ImageMetaProps.from_dict(props)
# Build up a ResourceRequest from the inside to compare against.
expected = utils.ResourceRequest()
expected._rg_by_id[None] = plib.RequestGroup(
use_same_provider=False,
required_traits={
'CUSTOM_TRUSTED',
}
)
self.assertResourceRequestsEqual(
expected, utils.ResourceRequest.from_image_props(image_meta_props))
def test_resource_request_from_image_props_append_request(self):
props = {'trait:CUSTOM_MAGIC': 'required'}
image_meta_props = objects.ImageMetaProps.from_dict(props)
existing_req = utils.ResourceRequest()
existing_req._rg_by_id[None] = plib.RequestGroup(
use_same_provider=False,
resources={
'VCPU': 2,
'MEMORY_MB': 2048,
},
required_traits={
'HW_CPU_X86_AVX',
}
)
# Build up a ResourceRequest from the inside to compare against.
expected = utils.ResourceRequest()
expected._rg_by_id[None] = plib.RequestGroup(
use_same_provider=False,
resources={
'VCPU': 2,
'MEMORY_MB': 2048,
},
required_traits={
# In addition to information already contained in the existing
# resource request, we add the traits from image properties
'HW_CPU_X86_AVX',
'CUSTOM_MAGIC',
}
)
self.assertResourceRequestsEqual(
expected, utils.ResourceRequest.from_image_props(image_meta_props,
req=existing_req))
def test_merge_resources(self):
resources = {
'VCPU': 1, 'MEMORY_MB': 1024,