def test_reshape(self): """We set up the following scenario: BEFORE: single compute node setup A single compute node with: - VCPU, MEMORY_MB, DISK_GB inventory - Two instances consuming CPU, RAM and DISK from that compute node AFTER: hierarchical + shared storage setup A compute node parent provider with: - MEMORY_MB Two NUMA node child providers containing: - VCPU Shared storage provider with: - DISK_GB Both instances have their resources split among the providers and shared storage accordingly """ # First create our consumers i1_uuid = uuids.instance1 i1_consumer = consumer_obj.Consumer(self.ctx, uuid=i1_uuid, user=self.user_obj, project=self.project_obj) i1_consumer.create() i2_uuid = uuids.instance2 i2_consumer = consumer_obj.Consumer(self.ctx, uuid=i2_uuid, user=self.user_obj, project=self.project_obj) i2_consumer.create() cn1 = self._create_provider('cn1') tb.add_inventory(cn1, 'VCPU', 16) tb.add_inventory(cn1, 'MEMORY_MB', 32768) tb.add_inventory(cn1, 'DISK_GB', 1000) # Allocate both instances against the single compute node for consumer in (i1_consumer, i2_consumer): allocs = [ rp_obj.Allocation(self.ctx, resource_provider=cn1, resource_class='VCPU', consumer=consumer, used=2), rp_obj.Allocation(self.ctx, resource_provider=cn1, resource_class='MEMORY_MB', consumer=consumer, used=1024), rp_obj.Allocation(self.ctx, resource_provider=cn1, resource_class='DISK_GB', consumer=consumer, used=100), ] alloc_list = rp_obj.AllocationList(self.ctx, objects=allocs) alloc_list.replace_all() # Verify we have the allocations we expect for the BEFORE scenario before_allocs_i1 = rp_obj.AllocationList.get_all_by_consumer_id( self.ctx, i1_uuid) self.assertEqual(3, len(before_allocs_i1)) self.assertEqual(cn1.uuid, before_allocs_i1[0].resource_provider.uuid) before_allocs_i2 = rp_obj.AllocationList.get_all_by_consumer_id( self.ctx, i2_uuid) self.assertEqual(3, len(before_allocs_i2)) self.assertEqual(cn1.uuid, before_allocs_i2[2].resource_provider.uuid) # Before we issue the actual reshape() call, we need to first create # the child providers and sharing storage provider. These are actions # that the virt driver or external agent is responsible for performing # *before* attempting any reshape activity. cn1_numa0 = self._create_provider('cn1_numa0', parent=cn1.uuid) cn1_numa1 = self._create_provider('cn1_numa1', parent=cn1.uuid) ss = self._create_provider('ss') # OK, now emulate the call to POST /reshaper that will be triggered by # a virt driver wanting to replace the world and change its modeling # from a single provider to a nested provider tree along with a sharing # storage provider. after_inventories = { # cn1 keeps the RAM only cn1: rp_obj.InventoryList(self.ctx, objects=[ rp_obj.Inventory( self.ctx, resource_provider=cn1, resource_class='MEMORY_MB', total=32768, reserved=0, max_unit=32768, min_unit=1, step_size=1, allocation_ratio=1.0), ]), # each NUMA node gets half of the CPUs cn1_numa0: rp_obj.InventoryList(self.ctx, objects=[ rp_obj.Inventory( self.ctx, resource_provider=cn1_numa0, resource_class='VCPU', total=8, reserved=0, max_unit=8, min_unit=1, step_size=1, allocation_ratio=1.0), ]), cn1_numa1: rp_obj.InventoryList(self.ctx, objects=[ rp_obj.Inventory( self.ctx, resource_provider=cn1_numa1, resource_class='VCPU', total=8, reserved=0, max_unit=8, min_unit=1, step_size=1, allocation_ratio=1.0), ]), # The sharing provider gets a bunch of disk ss: rp_obj.InventoryList(self.ctx, objects=[ rp_obj.Inventory(self.ctx, resource_provider=ss, resource_class='DISK_GB', total=100000, reserved=0, max_unit=1000, min_unit=1, step_size=1, allocation_ratio=1.0), ]), } # We do a fetch from the DB for each instance to get its latest # generation. This would be done by the resource tracker or scheduler # report client before issuing the call to reshape() because the # consumers representing the two instances above will have had their # generations incremented in the original call to PUT # /allocations/{consumer_uuid} i1_consumer = consumer_obj.Consumer.get_by_uuid(self.ctx, i1_uuid) i2_consumer = consumer_obj.Consumer.get_by_uuid(self.ctx, i2_uuid) after_allocs = rp_obj.AllocationList( self.ctx, objects=[ # instance1 gets VCPU from NUMA0, MEMORY_MB from cn1 and DISK_GB # from the sharing storage provider rp_obj.Allocation(self.ctx, resource_provider=cn1_numa0, resource_class='VCPU', consumer=i1_consumer, used=2), rp_obj.Allocation(self.ctx, resource_provider=cn1, resource_class='MEMORY_MB', consumer=i1_consumer, used=1024), rp_obj.Allocation(self.ctx, resource_provider=ss, resource_class='DISK_GB', consumer=i1_consumer, used=100), # instance2 gets VCPU from NUMA1, MEMORY_MB from cn1 and DISK_GB # from the sharing storage provider rp_obj.Allocation(self.ctx, resource_provider=cn1_numa1, resource_class='VCPU', consumer=i2_consumer, used=2), rp_obj.Allocation(self.ctx, resource_provider=cn1, resource_class='MEMORY_MB', consumer=i2_consumer, used=1024), rp_obj.Allocation(self.ctx, resource_provider=ss, resource_class='DISK_GB', consumer=i2_consumer, used=100), ]) rp_obj.reshape(self.ctx, after_inventories, after_allocs) # Verify that the inventories have been moved to the appropriate # providers in the AFTER scenario # The root compute node should only have MEMORY_MB, nothing else cn1_inv = rp_obj.InventoryList.get_all_by_resource_provider( self.ctx, cn1) self.assertEqual(1, len(cn1_inv)) self.assertEqual('MEMORY_MB', cn1_inv[0].resource_class) self.assertEqual(32768, cn1_inv[0].total) # Each NUMA node should only have half the original VCPU, nothing else numa0_inv = rp_obj.InventoryList.get_all_by_resource_provider( self.ctx, cn1_numa0) self.assertEqual(1, len(numa0_inv)) self.assertEqual('VCPU', numa0_inv[0].resource_class) self.assertEqual(8, numa0_inv[0].total) numa1_inv = rp_obj.InventoryList.get_all_by_resource_provider( self.ctx, cn1_numa1) self.assertEqual(1, len(numa1_inv)) self.assertEqual('VCPU', numa1_inv[0].resource_class) self.assertEqual(8, numa1_inv[0].total) # The sharing storage provider should only have DISK_GB, nothing else ss_inv = rp_obj.InventoryList.get_all_by_resource_provider( self.ctx, ss) self.assertEqual(1, len(ss_inv)) self.assertEqual('DISK_GB', ss_inv[0].resource_class) self.assertEqual(100000, ss_inv[0].total) # Verify we have the allocations we expect for the AFTER scenario after_allocs_i1 = rp_obj.AllocationList.get_all_by_consumer_id( self.ctx, i1_uuid) self.assertEqual(3, len(after_allocs_i1)) # Our VCPU allocation should be in the NUMA0 node vcpu_alloc = alloc_for_rc(after_allocs_i1, 'VCPU') self.assertIsNotNone(vcpu_alloc) self.assertEqual(cn1_numa0.uuid, vcpu_alloc.resource_provider.uuid) # Our DISK_GB allocation should be in the sharing provider disk_alloc = alloc_for_rc(after_allocs_i1, 'DISK_GB') self.assertIsNotNone(disk_alloc) self.assertEqual(ss.uuid, disk_alloc.resource_provider.uuid) # And our MEMORY_MB should remain on the root compute node ram_alloc = alloc_for_rc(after_allocs_i1, 'MEMORY_MB') self.assertIsNotNone(ram_alloc) self.assertEqual(cn1.uuid, ram_alloc.resource_provider.uuid) after_allocs_i2 = rp_obj.AllocationList.get_all_by_consumer_id( self.ctx, i2_uuid) self.assertEqual(3, len(after_allocs_i2)) # Our VCPU allocation should be in the NUMA1 node vcpu_alloc = alloc_for_rc(after_allocs_i2, 'VCPU') self.assertIsNotNone(vcpu_alloc) self.assertEqual(cn1_numa1.uuid, vcpu_alloc.resource_provider.uuid) # Our DISK_GB allocation should be in the sharing provider disk_alloc = alloc_for_rc(after_allocs_i2, 'DISK_GB') self.assertIsNotNone(disk_alloc) self.assertEqual(ss.uuid, disk_alloc.resource_provider.uuid) # And our MEMORY_MB should remain on the root compute node ram_alloc = alloc_for_rc(after_allocs_i2, 'MEMORY_MB') self.assertIsNotNone(ram_alloc) self.assertEqual(cn1.uuid, ram_alloc.resource_provider.uuid)
def reshape(req): context = req.environ['placement.context'] want_version = req.environ[microversion.MICROVERSION_ENVIRON] context.can(policies.RESHAPE) data = util.extract_json(req.body, schema.POST_RESHAPER_SCHEMA) inventories = data['inventories'] allocations = data['allocations'] # We're going to create several InventoryList, by rp uuid. inventory_by_rp = {} # TODO(cdent): this has overlaps with inventory:set_inventories # and is a mess of bad names and lack of method extraction. for rp_uuid, inventory_data in inventories.items(): try: resource_provider = rp_obj.ResourceProvider.get_by_uuid( context, rp_uuid) except exception.NotFound as exc: raise webob.exc.HTTPBadRequest( _('Resource provider %(rp_uuid)s in inventories not found: ' '%(error)s') % {'rp_uuid': rp_uuid, 'error': exc}, comment=errors.RESOURCE_PROVIDER_NOT_FOUND) # Do an early generation check. generation = inventory_data['resource_provider_generation'] if generation != resource_provider.generation: raise webob.exc.HTTPConflict( _('resource provider generation conflict: ' 'actual: %(actual)s, given: %(given)s') % {'actual': resource_provider.generation, 'given': generation}, comment=errors.CONCURRENT_UPDATE) inv_list = [] for res_class, raw_inventory in inventory_data['inventories'].items(): inv_data = copy.copy(inventory.INVENTORY_DEFAULTS) inv_data.update(raw_inventory) inv_obj = inventory.make_inventory_object( resource_provider, res_class, **inv_data) inv_list.append(inv_obj) inventory_by_rp[resource_provider] = rp_obj.InventoryList( objects=inv_list) # Make the consumer objects associated with the allocations. consumers, new_consumers_created = allocation.inspect_consumers( context, allocations, want_version) # Nest exception handling so that any exception results in new consumer # objects being deleted, then reraise for translating to HTTP exceptions. try: try: # When these allocations are created they get resource provider # objects which are different instances (usually with the same # data) from those loaded above when creating inventory objects. # The reshape method below is responsible for ensuring that the # resource providers and their generations do not conflict. allocation_objects = allocation.create_allocation_list( context, allocations, consumers) rp_obj.reshape(context, inventory_by_rp, allocation_objects) except Exception: with excutils.save_and_reraise_exception(): allocation.delete_consumers(new_consumers_created) # Generation conflict is a (rare) possibility in a few different # places in reshape(). except exception.ConcurrentUpdateDetected as exc: raise webob.exc.HTTPConflict( _('update conflict: %(error)s') % {'error': exc}, comment=errors.CONCURRENT_UPDATE) # A NotFound here means a resource class that does not exist was named except exception.NotFound as exc: raise webob.exc.HTTPBadRequest( _('malformed reshaper data: %(error)s') % {'error': exc}) # Distinguish inventory in use (has allocations on it)... except exception.InventoryInUse as exc: raise webob.exc.HTTPConflict( _('update conflict: %(error)s') % {'error': exc}, comment=errors.INVENTORY_INUSE) # ...from allocations which won't fit for a variety of reasons. except exception.InvalidInventory as exc: raise webob.exc.HTTPConflict( _('Unable to allocate inventory: %(error)s') % {'error': exc}) req.response.status = 204 req.response.content_type = None return req.response
def reshape(req): context = req.environ['placement.context'] want_version = req.environ[microversion.MICROVERSION_ENVIRON] context.can(policies.RESHAPE) data = util.extract_json(req.body, schema.POST_RESHAPER_SCHEMA) inventories = data['inventories'] allocations = data['allocations'] # We're going to create several InventoryList, by rp uuid. inventory_by_rp = {} # TODO(cdent): this has overlaps with inventory:set_inventories # and is a mess of bad names and lack of method extraction. for rp_uuid, inventory_data in inventories.items(): try: resource_provider = rp_obj.ResourceProvider.get_by_uuid( context, rp_uuid) except exception.NotFound as exc: raise webob.exc.HTTPBadRequest( _('Resource provider %(rp_uuid)s in inventories not found: ' '%(error)s') % { 'rp_uuid': rp_uuid, 'error': exc }, comment=errors.RESOURCE_PROVIDER_NOT_FOUND) # Do an early generation check. generation = inventory_data['resource_provider_generation'] if generation != resource_provider.generation: raise webob.exc.HTTPConflict( _('resource provider generation conflict: ' 'actual: %(actual)s, given: %(given)s') % { 'actual': resource_provider.generation, 'given': generation }, comment=errors.CONCURRENT_UPDATE) inv_list = [] for res_class, raw_inventory in inventory_data['inventories'].items(): inv_data = copy.copy(inventory.INVENTORY_DEFAULTS) inv_data.update(raw_inventory) inv_obj = inventory.make_inventory_object(resource_provider, res_class, **inv_data) inv_list.append(inv_obj) inventory_by_rp[resource_provider] = rp_obj.InventoryList( objects=inv_list) # Make the consumer objects associated with the allocations. consumers, new_consumers_created = allocation.inspect_consumers( context, allocations, want_version) # Nest exception handling so that any exception results in new consumer # objects being deleted, then reraise for translating to HTTP exceptions. try: try: # When these allocations are created they get resource provider # objects which are different instances (usually with the same # data) from those loaded above when creating inventory objects. # The reshape method below is responsible for ensuring that the # resource providers and their generations do not conflict. allocation_objects = allocation.create_allocation_list( context, allocations, consumers) rp_obj.reshape(context, inventory_by_rp, allocation_objects) except Exception: with excutils.save_and_reraise_exception(): allocation.delete_consumers(new_consumers_created) # Generation conflict is a (rare) possibility in a few different # places in reshape(). except exception.ConcurrentUpdateDetected as exc: raise webob.exc.HTTPConflict(_('update conflict: %(error)s') % {'error': exc}, comment=errors.CONCURRENT_UPDATE) # A NotFound here means a resource class that does not exist was named except exception.NotFound as exc: raise webob.exc.HTTPBadRequest( _('malformed reshaper data: %(error)s') % {'error': exc}) # Distinguish inventory in use (has allocations on it)... except exception.InventoryInUse as exc: raise webob.exc.HTTPConflict(_('update conflict: %(error)s') % {'error': exc}, comment=errors.INVENTORY_INUSE) # ...from allocations which won't fit for a variety of reasons. except exception.InvalidInventory as exc: raise webob.exc.HTTPConflict( _('Unable to allocate inventory: %(error)s') % {'error': exc}) req.response.status = 204 req.response.content_type = None return req.response
def test_reshape(self): """We set up the following scenario: BEFORE: single compute node setup A single compute node with: - VCPU, MEMORY_MB, DISK_GB inventory - Two instances consuming CPU, RAM and DISK from that compute node AFTER: hierarchical + shared storage setup A compute node parent provider with: - MEMORY_MB Two NUMA node child providers containing: - VCPU Shared storage provider with: - DISK_GB Both instances have their resources split among the providers and shared storage accordingly """ # First create our consumers i1_uuid = uuids.instance1 i1_consumer = consumer_obj.Consumer( self.ctx, uuid=i1_uuid, user=self.user_obj, project=self.project_obj) i1_consumer.create() i2_uuid = uuids.instance2 i2_consumer = consumer_obj.Consumer( self.ctx, uuid=i2_uuid, user=self.user_obj, project=self.project_obj) i2_consumer.create() cn1 = self._create_provider('cn1') tb.add_inventory(cn1, 'VCPU', 16) tb.add_inventory(cn1, 'MEMORY_MB', 32768) tb.add_inventory(cn1, 'DISK_GB', 1000) # Allocate both instances against the single compute node for consumer in (i1_consumer, i2_consumer): allocs = [ rp_obj.Allocation( self.ctx, resource_provider=cn1, resource_class='VCPU', consumer=consumer, used=2), rp_obj.Allocation( self.ctx, resource_provider=cn1, resource_class='MEMORY_MB', consumer=consumer, used=1024), rp_obj.Allocation( self.ctx, resource_provider=cn1, resource_class='DISK_GB', consumer=consumer, used=100), ] alloc_list = rp_obj.AllocationList(self.ctx, objects=allocs) alloc_list.replace_all() # Verify we have the allocations we expect for the BEFORE scenario before_allocs_i1 = rp_obj.AllocationList.get_all_by_consumer_id( self.ctx, i1_uuid) self.assertEqual(3, len(before_allocs_i1)) self.assertEqual(cn1.uuid, before_allocs_i1[0].resource_provider.uuid) before_allocs_i2 = rp_obj.AllocationList.get_all_by_consumer_id( self.ctx, i2_uuid) self.assertEqual(3, len(before_allocs_i2)) self.assertEqual(cn1.uuid, before_allocs_i2[2].resource_provider.uuid) # Before we issue the actual reshape() call, we need to first create # the child providers and sharing storage provider. These are actions # that the virt driver or external agent is responsible for performing # *before* attempting any reshape activity. cn1_numa0 = self._create_provider('cn1_numa0', parent=cn1.uuid) cn1_numa1 = self._create_provider('cn1_numa1', parent=cn1.uuid) ss = self._create_provider('ss') # OK, now emulate the call to POST /reshaper that will be triggered by # a virt driver wanting to replace the world and change its modeling # from a single provider to a nested provider tree along with a sharing # storage provider. after_inventories = { # cn1 keeps the RAM only cn1.uuid: rp_obj.InventoryList(self.ctx, objects=[ rp_obj.Inventory( self.ctx, resource_provider=cn1, resource_class='MEMORY_MB', total=32768, reserved=0, max_unit=32768, min_unit=1, step_size=1, allocation_ratio=1.0), ]), # each NUMA node gets half of the CPUs cn1_numa0.uuid: rp_obj.InventoryList(self.ctx, objects=[ rp_obj.Inventory( self.ctx, resource_provider=cn1_numa0, resource_class='VCPU', total=8, reserved=0, max_unit=8, min_unit=1, step_size=1, allocation_ratio=1.0), ]), cn1_numa1.uuid: rp_obj.InventoryList(self.ctx, objects=[ rp_obj.Inventory( self.ctx, resource_provider=cn1_numa1, resource_class='VCPU', total=8, reserved=0, max_unit=8, min_unit=1, step_size=1, allocation_ratio=1.0), ]), # The sharing provider gets a bunch of disk ss.uuid: rp_obj.InventoryList(self.ctx, objects=[ rp_obj.Inventory( self.ctx, resource_provider=ss, resource_class='DISK_GB', total=100000, reserved=0, max_unit=1000, min_unit=1, step_size=1, allocation_ratio=1.0), ]), } # We do a fetch from the DB for each instance to get its latest # generation. This would be done by the resource tracker or scheduler # report client before issuing the call to reshape() because the # consumers representing the two instances above will have had their # generations incremented in the original call to PUT # /allocations/{consumer_uuid} i1_consumer = consumer_obj.Consumer.get_by_uuid(self.ctx, i1_uuid) i2_consumer = consumer_obj.Consumer.get_by_uuid(self.ctx, i2_uuid) after_allocs = rp_obj.AllocationList(self.ctx, objects=[ # instance1 gets VCPU from NUMA0, MEMORY_MB from cn1 and DISK_GB # from the sharing storage provider rp_obj.Allocation( self.ctx, resource_provider=cn1_numa0, resource_class='VCPU', consumer=i1_consumer, used=2), rp_obj.Allocation( self.ctx, resource_provider=cn1, resource_class='MEMORY_MB', consumer=i1_consumer, used=1024), rp_obj.Allocation( self.ctx, resource_provider=ss, resource_class='DISK_GB', consumer=i1_consumer, used=100), # instance2 gets VCPU from NUMA1, MEMORY_MB from cn1 and DISK_GB # from the sharing storage provider rp_obj.Allocation( self.ctx, resource_provider=cn1_numa1, resource_class='VCPU', consumer=i2_consumer, used=2), rp_obj.Allocation( self.ctx, resource_provider=cn1, resource_class='MEMORY_MB', consumer=i2_consumer, used=1024), rp_obj.Allocation( self.ctx, resource_provider=ss, resource_class='DISK_GB', consumer=i2_consumer, used=100), ]) rp_obj.reshape(self.ctx, after_inventories, after_allocs) # Verify that the inventories have been moved to the appropriate # providers in the AFTER scenario # The root compute node should only have MEMORY_MB, nothing else cn1_inv = rp_obj.InventoryList.get_all_by_resource_provider( self.ctx, cn1) self.assertEqual(1, len(cn1_inv)) self.assertEqual('MEMORY_MB', cn1_inv[0].resource_class) self.assertEqual(32768, cn1_inv[0].total) # Each NUMA node should only have half the original VCPU, nothing else numa0_inv = rp_obj.InventoryList.get_all_by_resource_provider( self.ctx, cn1_numa0) self.assertEqual(1, len(numa0_inv)) self.assertEqual('VCPU', numa0_inv[0].resource_class) self.assertEqual(8, numa0_inv[0].total) numa1_inv = rp_obj.InventoryList.get_all_by_resource_provider( self.ctx, cn1_numa1) self.assertEqual(1, len(numa1_inv)) self.assertEqual('VCPU', numa1_inv[0].resource_class) self.assertEqual(8, numa1_inv[0].total) # The sharing storage provider should only have DISK_GB, nothing else ss_inv = rp_obj.InventoryList.get_all_by_resource_provider( self.ctx, ss) self.assertEqual(1, len(ss_inv)) self.assertEqual('DISK_GB', ss_inv[0].resource_class) self.assertEqual(100000, ss_inv[0].total) # Verify we have the allocations we expect for the AFTER scenario after_allocs_i1 = rp_obj.AllocationList.get_all_by_consumer_id( self.ctx, i1_uuid) self.assertEqual(3, len(after_allocs_i1)) # Our VCPU allocation should be in the NUMA0 node vcpu_alloc = alloc_for_rc(after_allocs_i1, 'VCPU') self.assertIsNotNone(vcpu_alloc) self.assertEqual(cn1_numa0.uuid, vcpu_alloc.resource_provider.uuid) # Our DISK_GB allocation should be in the sharing provider disk_alloc = alloc_for_rc(after_allocs_i1, 'DISK_GB') self.assertIsNotNone(disk_alloc) self.assertEqual(ss.uuid, disk_alloc.resource_provider.uuid) # And our MEMORY_MB should remain on the root compute node ram_alloc = alloc_for_rc(after_allocs_i1, 'MEMORY_MB') self.assertIsNotNone(ram_alloc) self.assertEqual(cn1.uuid, ram_alloc.resource_provider.uuid) after_allocs_i2 = rp_obj.AllocationList.get_all_by_consumer_id( self.ctx, i2_uuid) self.assertEqual(3, len(after_allocs_i2)) # Our VCPU allocation should be in the NUMA1 node vcpu_alloc = alloc_for_rc(after_allocs_i2, 'VCPU') self.assertIsNotNone(vcpu_alloc) self.assertEqual(cn1_numa1.uuid, vcpu_alloc.resource_provider.uuid) # Our DISK_GB allocation should be in the sharing provider disk_alloc = alloc_for_rc(after_allocs_i2, 'DISK_GB') self.assertIsNotNone(disk_alloc) self.assertEqual(ss.uuid, disk_alloc.resource_provider.uuid) # And our MEMORY_MB should remain on the root compute node ram_alloc = alloc_for_rc(after_allocs_i2, 'MEMORY_MB') self.assertIsNotNone(ram_alloc) self.assertEqual(cn1.uuid, ram_alloc.resource_provider.uuid)