def update_resource_class(req): """PUT to create or validate the existence of single resource class. On a successful create return 201. Return 204 if the class already exists. If the resource class is not a custom resource class, return a 400. 409 might be a better choice, but 400 aligns with previous code. """ name = util.wsgi_path_item(req.environ, 'name') context = req.environ['placement.context'] context.can(policies.UPDATE) # Use JSON validation to validation resource class name. util.extract_json('{"name": "%s"}' % name, schema.PUT_RC_SCHEMA_V1_2) status = 204 try: rc = rc_obj.ResourceClass.get_by_name(context, name) except exception.NotFound: try: rc = rc_obj.ResourceClass(context, name=name) rc.create() status = 201 # We will not see ResourceClassCannotUpdateStandard because # that was already caught when validating the {name}. except exception.ResourceClassExists: # Someone just now created the class, so stick with 204 pass req.response.status = status req.response.content_type = None req.response.location = util.resource_class_url(req.environ, rc) return req.response
def set_aggregates(req): context = req.environ['placement.context'] context.can(policies.UPDATE) want_version = req.environ[microversion.MICROVERSION_ENVIRON] consider_generation = want_version.matches( min_version=_INCLUDE_GENERATION_VERSION) put_schema = schema.PUT_AGGREGATES_SCHEMA_V1_1 if consider_generation: put_schema = schema.PUT_AGGREGATES_SCHEMA_V1_19 uuid = util.wsgi_path_item(req.environ, 'uuid') resource_provider = rp_obj.ResourceProvider.get_by_uuid( context, uuid) data = util.extract_json(req.body, put_schema) if consider_generation: # Check for generation conflict rp_gen = data['resource_provider_generation'] if resource_provider.generation != rp_gen: raise webob.exc.HTTPConflict( _("Resource provider's generation already changed. Please " "update the generation and try again."), comment=errors.CONCURRENT_UPDATE) aggregate_uuids = data['aggregates'] else: aggregate_uuids = data _set_aggregates(resource_provider, aggregate_uuids, increment_generation=consider_generation) return _send_aggregates(req, resource_provider, aggregate_uuids)
def set_aggregates(req): context = req.environ['placement.context'] want_version = req.environ[microversion.MICROVERSION_ENVIRON] consider_generation = want_version.matches( min_version=_INCLUDE_GENERATION_VERSION) put_schema = schema.PUT_AGGREGATES_SCHEMA_V1_1 if consider_generation: put_schema = schema.PUT_AGGREGATES_SCHEMA_V1_19 uuid = util.wsgi_path_item(req.environ, 'uuid') resource_provider = rp_obj.ResourceProvider.get_by_uuid(context, uuid) data = util.extract_json(req.body, put_schema) if consider_generation: # Check for generation conflict rp_gen = data['resource_provider_generation'] if resource_provider.generation != rp_gen: raise webob.exc.HTTPConflict( _("Resource provider's generation already changed. Please " "update the generation and try again.")) aggregate_uuids = data['aggregates'] else: aggregate_uuids = data try: resource_provider.set_aggregates( aggregate_uuids, increment_generation=consider_generation) except (exception.ConcurrentUpdateDetected, db_exc.DBDuplicateEntry) as exc: raise webob.exc.HTTPConflict( _('Update conflict: %(error)s') % {'error': exc}) return _send_aggregates(req, resource_provider, aggregate_uuids)
def update_traits_for_resource_provider(req): context = req.environ['placement.context'] want_version = req.environ[microversion.MICROVERSION_ENVIRON] uuid = util.wsgi_path_item(req.environ, 'uuid') data = util.extract_json(req.body, schema.SET_TRAITS_FOR_RP_SCHEMA) rp_gen = data['resource_provider_generation'] traits = data['traits'] resource_provider = rp_obj.ResourceProvider.get_by_uuid(context, uuid) if resource_provider.generation != rp_gen: raise webob.exc.HTTPConflict(_( "Resource provider's generation already changed. Please update " "the generation and try again."), json_formatter=util.json_error_formatter) trait_objs = rp_obj.TraitList.get_all(context, filters={'name_in': traits}) traits_name = set([obj.name for obj in trait_objs]) non_existed_trait = set(traits) - set(traits_name) if non_existed_trait: raise webob.exc.HTTPBadRequest( _("No such trait %s") % ', '.join(non_existed_trait)) resource_provider.set_traits(trait_objs) response_body, last_modified = _serialize_traits(trait_objs, want_version) response_body[ 'resource_provider_generation'] = resource_provider.generation if want_version.matches((1, 15)): req.response.last_modified = last_modified req.response.cache_control = 'no-cache' req.response.status = 200 req.response.body = encodeutils.to_utf8(jsonutils.dumps(response_body)) req.response.content_type = 'application/json' return req.response
def update_resource_class(req): """PUT to update a single resource class. On success return a 200 response with a representation of the updated resource class. """ name = util.wsgi_path_item(req.environ, 'name') context = req.environ['placement.context'] context.can(policies.UPDATE) data = util.extract_json(req.body, schema.PUT_RC_SCHEMA_V1_2) # The containing application will catch a not found here. rc = rc_obj.ResourceClass.get_by_name(context, name) rc.name = data['name'] try: rc.save() except exception.ResourceClassExists: raise webob.exc.HTTPConflict( 'Resource class already exists: %(name)s' % {'name': rc.name}) except exception.ResourceClassCannotUpdateStandard: raise webob.exc.HTTPBadRequest( 'Cannot update standard resource class %(rp_name)s' % {'rp_name': name}) req.response.body = encodeutils.to_utf8( jsonutils.dumps(_serialize_resource_class(req.environ, rc))) req.response.status = 200 req.response.content_type = 'application/json' return req.response
def create_resource_class(req): """POST to create a resource class. On success return a 201 response with an empty body and a location header pointing to the newly created resource class. """ context = req.environ['placement.context'] context.can(policies.CREATE) data = util.extract_json(req.body, schema.POST_RC_SCHEMA_V1_2) try: rc = rc_obj.ResourceClass(context, name=data['name']) rc.create() except exception.ResourceClassExists: raise webob.exc.HTTPConflict( 'Conflicting resource class already exists: %(name)s' % {'name': data['name']}) except exception.MaxDBRetriesExceeded: raise webob.exc.HTTPConflict( 'Max retries of DB transaction exceeded attempting ' 'to create resource class: %(name)s, please ' 'try again.' % {'name': data['name']}) req.response.location = util.resource_class_url(req.environ, rc) req.response.status = 201 req.response.content_type = None return req.response
def test_valid(self): data = util.extract_json( '{"name": "cow", ' '"uuid": "%s"}' % uuidsentinel.rp_uuid, self.schema) self.assertEqual('cow', data['name']) self.assertEqual(uuidsentinel.rp_uuid, data['uuid'])
def create_resource_tree(req): """This method accepts a nested dict that describes a tree-like relationship among resource providers. Each node on the tree should contain the following keys: name: the name given to the resource. If not supplied, no name is set. uuid: the resource's UUID. If not supplied, one will be generated. resources: a dict of resources that this node provides directly. Each member should be of the form `resource_class: amount` traits: a list of traits to apply to this node. children: a list of nodes representing the children of this node. Each child node should be the same format dict as described here. returns: the UUID of the root of the newly-created tree There is no limit to the level of nesting for child resource providers. """ context = req.environ["placement.context"] context.can(policies.UPDATE) schema = rp_schema.POST_RP_TREE want_version = req.environ[microversion.MICROVERSION_ENVIRON] data = util.extract_json(req.body, schema) root_rp = rp_obj.ResourceProvider.create_tree(context, data) response = req.response response.status = 200 req.response.location = util.resource_provider_url(req.environ, root_rp) req.response.body = encodeutils.to_utf8( jsonutils.dumps( _serialize_provider(req.environ, root_rp, want_version))) req.response.content_type = "application/json" req.response.cache_control = "no-cache"
def create_resource_provider(req): """POST to create a resource provider. On success return a 201 response with an empty body (microversions 1.0 - 1.19) or a 200 response with a payload representing the newly created resource provider (microversions 1.20 - latest), and a location header pointing to the resource provider. """ context = req.environ['placement.context'] context.can(policies.CREATE) schema = rp_schema.POST_RESOURCE_PROVIDER_SCHEMA want_version = req.environ[microversion.MICROVERSION_ENVIRON] if want_version.matches((1, 14)): schema = rp_schema.POST_RP_SCHEMA_V1_14 data = util.extract_json(req.body, schema) if data.get('uuid'): # Normalize UUID with no proper dashes into dashed one # with format {8}-{4}-{4}-{4}-{12} data['uuid'] = str(uuidlib.UUID(data['uuid'])) else: data['uuid'] = uuidutils.generate_uuid() data["generation"] = 0 try: resource_provider = rp_obj.ResourceProvider(context, **data) resource_provider.create() except db.ClientError as e: if "ConstraintValidationFailed" in str(e): if "property `uuid`" in str(e): duplicate = "uuid: %s" % data.get("uuid") else: duplicate = "name: %s" % data.get("name") raise webob.exc.HTTPConflict("Conflicting resource provider " "%(duplicate)s already exists." % {'duplicate': duplicate}, comment=errors.DUPLICATE_NAME) else: raise webob.exc.HTTPBadRequest("Unable to create resource " "provider '%(rp_uuid)s': %(error)s" % {"rp_uuid": data.get("uuid"), "error": e}) except exception.ObjectActionError as exc: raise webob.exc.HTTPBadRequest( 'Unable to create resource provider "%(name)s", %(rp_uuid)s: ' '%(error)s' % {'name': data['name'], 'rp_uuid': data['uuid'], 'error': exc}) req.response.location = util.resource_provider_url( req.environ, resource_provider) if want_version.matches(min_version=(1, 20)): req.response.body = encodeutils.to_utf8(jsonutils.dumps( _serialize_provider(req.environ, resource_provider, want_version))) req.response.content_type = 'application/json' modified = util.pick_last_modified(None, resource_provider) req.response.last_modified = modified req.response.cache_control = 'no-cache' else: req.response.status = 201 req.response.content_type = None return req.response
def _extract_inventory(body, schema): """Extract and validate inventory from JSON body.""" data = util.extract_json(body, schema) inventory_data = copy.copy(INVENTORY_DEFAULTS) inventory_data.update(data) return inventory_data
def create_resource_provider(req): """POST to create a resource provider. On success return a 201 response with an empty body (microversions 1.0 - 1.19) or a 200 response with a payload representing the newly created resource provider (microversions 1.20 - latest), and a location header pointing to the resource provider. """ context = req.environ['placement.context'] context.can(policies.CREATE) schema = rp_schema.POST_RESOURCE_PROVIDER_SCHEMA want_version = req.environ[microversion.MICROVERSION_ENVIRON] if want_version.matches((1, 14)): schema = rp_schema.POST_RP_SCHEMA_V1_14 data = util.extract_json(req.body, schema) try: if data.get('uuid'): # Normalize UUID with no proper dashes into dashed one # with format {8}-{4}-{4}-{4}-{12} data['uuid'] = str(uuidlib.UUID(data['uuid'])) else: data['uuid'] = uuidutils.generate_uuid() resource_provider = rp_obj.ResourceProvider(context, **data) resource_provider.create() except db_exc.DBDuplicateEntry as exc: # Whether exc.columns has one or two entries (in the event # of both fields being duplicates) appears to be database # dependent, so going with the complete solution here. duplicate = ', '.join( ['%s: %s' % (column, data[column]) for column in exc.columns]) raise webob.exc.HTTPConflict( 'Conflicting resource provider %(duplicate)s already exists.' % {'duplicate': duplicate}, comment=errors.DUPLICATE_NAME) except exception.ObjectActionError as exc: raise webob.exc.HTTPBadRequest( 'Unable to create resource provider "%(name)s", %(rp_uuid)s: ' '%(error)s' % {'name': data['name'], 'rp_uuid': data['uuid'], 'error': exc}) req.response.location = util.resource_provider_url( req.environ, resource_provider) if want_version.matches(min_version=(1, 20)): req.response.body = encodeutils.to_utf8(jsonutils.dumps( _serialize_provider(req.environ, resource_provider, want_version))) req.response.content_type = 'application/json' modified = util.pick_last_modified(None, resource_provider) req.response.last_modified = modified req.response.cache_control = 'no-cache' else: req.response.status = 201 req.response.content_type = None return req.response
def _set_allocations_for_consumer(req, schema): context = req.environ['placement.context'] consumer_uuid = util.wsgi_path_item(req.environ, 'consumer_uuid') data = util.extract_json(req.body, schema) allocation_data = data['allocations'] # Normalize allocation data to dict. want_version = req.environ[microversion.MICROVERSION_ENVIRON] if not want_version.matches((1, 12)): allocations_dict = {} # Allocation are list-ish, transform to dict-ish for allocation in allocation_data: resource_provider_uuid = allocation['resource_provider']['uuid'] allocations_dict[resource_provider_uuid] = { 'resources': allocation['resources'] } allocation_data = allocations_dict # If the body includes an allocation for a resource provider # that does not exist, raise a 400. allocation_objects = [] for resource_provider_uuid, allocation in allocation_data.items(): new_allocations = _new_allocations(context, resource_provider_uuid, consumer_uuid, allocation['resources'], data.get('project_id'), data.get('user_id')) allocation_objects.extend(new_allocations) allocations = rp_obj.AllocationList(context, objects=allocation_objects) try: allocations.create_all() LOG.debug("Successfully wrote allocations %s", allocations) # InvalidInventory is a parent for several exceptions that # indicate either that Inventory is not present, or that # capacity limits have been exceeded. except exception.NotFound as exc: raise webob.exc.HTTPBadRequest( _("Unable to allocate inventory for consumer " "%(consumer_uuid)s: %(error)s") % { 'consumer_uuid': consumer_uuid, 'error': exc }) except exception.InvalidInventory as exc: raise webob.exc.HTTPConflict( _('Unable to allocate inventory: %(error)s') % {'error': exc}) except exception.ConcurrentUpdateDetected as exc: raise webob.exc.HTTPConflict( _('Inventory changed while attempting to allocate: %(error)s') % {'error': exc}) req.response.status = 204 req.response.content_type = None return req.response
def set_allocations(req): context = req.environ['placement.context'] data = util.extract_json(req.body, schema.POST_ALLOCATIONS_V1_13) # Create a sequence of allocation objects to be used in an # AllocationList.create_all() call, which will mean all the changes # happen within a single transaction and with resource provider # generations check all in one go. allocation_objects = [] for consumer_uuid in data: project_id = data[consumer_uuid]['project_id'] user_id = data[consumer_uuid]['user_id'] allocations = data[consumer_uuid]['allocations'] if allocations: for resource_provider_uuid in allocations: resources = allocations[resource_provider_uuid]['resources'] new_allocations = _new_allocations(context, resource_provider_uuid, consumer_uuid, resources, project_id, user_id) allocation_objects.extend(new_allocations) else: # The allocations are empty, which means wipe them out. # Internal to the allocation object this is signalled by a # used value of 0. allocations = rp_obj.AllocationList.get_all_by_consumer_id( context, consumer_uuid) for allocation in allocations: allocation.used = 0 allocation_objects.append(allocation) allocations = rp_obj.AllocationList(context, objects=allocation_objects) try: allocations.create_all() LOG.debug("Successfully wrote allocations %s", allocations) except exception.NotFound as exc: raise webob.exc.HTTPBadRequest( _("Unable to allocate inventory %(error)s") % {'error': exc}) except exception.InvalidInventory as exc: # InvalidInventory is a parent for several exceptions that # indicate either that Inventory is not present, or that # capacity limits have been exceeded. raise webob.exc.HTTPConflict( _('Unable to allocate inventory: %(error)s') % {'error': exc}) except exception.ConcurrentUpdateDetected as exc: raise webob.exc.HTTPConflict( _('Inventory changed while attempting to allocate: %(error)s') % {'error': exc}) req.response.status = 204 req.response.content_type = None return req.response
def _extract_inventories(body, schema): """Extract and validate multiple inventories from JSON body.""" data = util.extract_json(body, schema) inventories = {} for res_class, raw_inventory in data['inventories'].items(): inventory_data = copy.copy(INVENTORY_DEFAULTS) inventory_data.update(raw_inventory) inventories[res_class] = inventory_data data['inventories'] = inventories return data
def update_resource_provider(req): """PUT to update a single resource provider. On success return a 200 response with a representation of the updated resource provider. """ uuid = util.wsgi_path_item(req.environ, 'uuid') context = req.environ['placement.context'] context.can(policies.UPDATE) want_version = req.environ[microversion.MICROVERSION_ENVIRON] # The containing application will catch a not found here. resource_provider = rp_obj.ResourceProvider.get_by_uuid( context, uuid) schema = rp_schema.PUT_RESOURCE_PROVIDER_SCHEMA if want_version.matches((1, 14)): schema = rp_schema.PUT_RP_SCHEMA_V1_14 data = util.extract_json(req.body, schema) for field in rp_obj.ResourceProvider.SETTABLE_FIELDS: if field in data: setattr(resource_provider, field, data[field]) try: resource_provider.save() except db.ClientError as e: if "ConstraintValidationFailed" in e.message: raise webob.exc.HTTPConflict( 'Conflicting resource provider %(name)s already exists.' % {'name': data['name']}, comment=errors.DUPLICATE_NAME) except db_exc.DBDuplicateEntry: raise webob.exc.HTTPConflict( 'Conflicting resource provider %(name)s already exists.' % {'name': data['name']}, comment=errors.DUPLICATE_NAME) except exception.ObjectActionError as exc: raise webob.exc.HTTPBadRequest( 'Unable to save resource provider %(rp_uuid)s: %(error)s' % {'rp_uuid': uuid, 'error': exc}) response = req.response response.status = 200 response.body = encodeutils.to_utf8(jsonutils.dumps( _serialize_provider(req.environ, resource_provider, want_version))) response.content_type = 'application/json' if want_version.matches((1, 15)): response.last_modified = resource_provider.updated_at response.cache_control = 'no-cache' return response
def set_allocations(req): context = req.environ['placement.context'] context.can(policies.ALLOC_MANAGE) want_version = req.environ[microversion.MICROVERSION_ENVIRON] want_schema = schema.POST_ALLOCATIONS_V1_13 if want_version.matches((1, 28)): want_schema = schema.POST_ALLOCATIONS_V1_28 if want_version.matches((1, 34)): want_schema = schema.POST_ALLOCATIONS_V1_34 data = util.extract_json(req.body, want_schema) consumers, new_consumers_created = inspect_consumers( context, data, want_version) # Create a sequence of allocation objects to be used in one # alloc_obj.replace_all() call, which will mean all the changes # happen within a single transaction and with resource provider # and consumer generations (if applicable) check all in one go. allocations = create_allocation_list(context, data, consumers) def _create_allocations(alloc_list): try: alloc_obj.replace_all(context, alloc_list) LOG.debug("Successfully wrote allocations %s", alloc_list) except Exception: with excutils.save_and_reraise_exception(): delete_consumers(new_consumers_created) try: _create_allocations(allocations) except exception.NotFound as exc: raise webob.exc.HTTPBadRequest( "Unable to allocate inventory %(error)s" % {'error': exc}) except exception.InvalidInventory as exc: # InvalidInventory is a parent for several exceptions that # indicate either that Inventory is not present, or that # capacity limits have been exceeded. raise webob.exc.HTTPConflict( 'Unable to allocate inventory: %(error)s' % {'error': exc}) except exception.ConcurrentUpdateDetected as exc: raise webob.exc.HTTPConflict( 'Inventory and/or allocations changed while attempting to ' 'allocate: %(error)s' % {'error': exc}, comment=errors.CONCURRENT_UPDATE) req.response.status = 204 req.response.content_type = None return req.response
def create_resource_provider(req): """POST to create a resource provider. On success return a 201 response with an empty body and a location header pointing to the newly created resource provider. """ context = req.environ['placement.context'] schema = rp_schema.POST_RESOURCE_PROVIDER_SCHEMA want_version = req.environ[microversion.MICROVERSION_ENVIRON] if want_version.matches((1, 14)): schema = rp_schema.POST_RP_SCHEMA_V1_14 data = util.extract_json(req.body, schema) try: uuid = data.setdefault('uuid', uuidutils.generate_uuid()) resource_provider = rp_obj.ResourceProvider(context, **data) resource_provider.create() except db_exc.DBDuplicateEntry as exc: # Whether exc.columns has one or two entries (in the event # of both fields being duplicates) appears to be database # dependent, so going with the complete solution here. duplicate = ', '.join(['%s: %s' % (column, data[column]) for column in exc.columns]) raise webob.exc.HTTPConflict( _('Conflicting resource provider %(duplicate)s already exists.') % {'duplicate': duplicate}) except exception.ObjectActionError as exc: raise webob.exc.HTTPBadRequest( _('Unable to create resource provider "%(name)s", %(rp_uuid)s: ' '%(error)s') % {'name': data['name'], 'rp_uuid': uuid, 'error': exc}) req.response.location = util.resource_provider_url( req.environ, resource_provider) if want_version.matches(min_version=(1, 20)): req.response.body = encodeutils.to_utf8(jsonutils.dumps( _serialize_provider(req.environ, resource_provider, want_version))) req.response.content_type = 'application/json' modified = util.pick_last_modified(None, resource_provider) req.response.last_modified = modified req.response.cache_control = 'no-cache' else: req.response.status = 201 req.response.content_type = None return req.response
def associate(req): """Associate a resource provider with one or more other resource providers. This is commonly used to create a sharing relationship among resource providers. On success return a 204 and an empty body. """ uuid = util.wsgi_path_item(req.environ, "uuid") context = req.environ["placement.context"] context.can(policies.UPDATE) schema = rp_schema.POST_RPS_ASSOCIATE want_version = req.environ[microversion.MICROVERSION_ENVIRON] data = util.extract_json(req.body, schema) target_uuids = data.get("targets", []) # The containing application will catch a not found here. resource_provider = rp_obj.ResourceProvider.get_by_uuid(context, uuid) rp_obj.associate(context, resource_provider, target_uuids) response = req.response response.status = 204
def _set_allocations_for_consumer(req, schema): context = req.environ['placement.context'] context.can(policies.ALLOC_UPDATE) consumer_uuid = util.wsgi_path_item(req.environ, 'consumer_uuid') if not uuidutils.is_uuid_like(consumer_uuid): raise webob.exc.HTTPBadRequest( _('Malformed consumer_uuid: %(consumer_uuid)s') % {'consumer_uuid': consumer_uuid}) consumer_uuid = str(uuid.UUID(consumer_uuid)) data = util.extract_json(req.body, schema) allocation_data = data['allocations'] # Normalize allocation data to dict. want_version = req.environ[microversion.MICROVERSION_ENVIRON] if not want_version.matches((1, 12)): allocations_dict = {} # Allocation are list-ish, transform to dict-ish for allocation in allocation_data: resource_provider_uuid = allocation['resource_provider']['uuid'] allocations_dict[resource_provider_uuid] = { 'resources': allocation['resources'] } allocation_data = allocations_dict allocation_objects = [] # Consumer object saved in case we need to delete the auto-created consumer # record consumer = None # Whether we created a new consumer record created_new_consumer = False if not allocation_data: # The allocations are empty, which means wipe them out. Internal # to the allocation object this is signalled by a used value of 0. # We still need to verify the consumer's generation, though, which # we do in _ensure_consumer() # NOTE(jaypipes): This will only occur 1.28+. The JSONSchema will # prevent an empty allocations object from being passed when there is # no consumer generation, so this is safe to do. util.ensure_consumer(context, consumer_uuid, data.get('project_id'), data.get('user_id'), data.get('consumer_generation'), want_version) allocations = rp_obj.AllocationList.get_all_by_consumer_id( context, consumer_uuid) for allocation in allocations: allocation.used = 0 allocation_objects.append(allocation) else: # If the body includes an allocation for a resource provider # that does not exist, raise a 400. rp_objs = _resource_providers_by_uuid(context, allocation_data.keys()) consumer, created_new_consumer = util.ensure_consumer( context, consumer_uuid, data.get('project_id'), data.get('user_id'), data.get('consumer_generation'), want_version) for resource_provider_uuid, allocation in allocation_data.items(): resource_provider = rp_objs[resource_provider_uuid] new_allocations = _new_allocations(context, resource_provider, consumer, allocation['resources']) allocation_objects.extend(new_allocations) allocations = rp_obj.AllocationList( context, objects=allocation_objects) def _create_allocations(alloc_list): try: alloc_list.replace_all() LOG.debug("Successfully wrote allocations %s", alloc_list) except Exception: if created_new_consumer: delete_consumers([consumer]) raise try: _create_allocations(allocations) # InvalidInventory is a parent for several exceptions that # indicate either that Inventory is not present, or that # capacity limits have been exceeded. except exception.NotFound as exc: raise webob.exc.HTTPBadRequest( _("Unable to allocate inventory for consumer " "%(consumer_uuid)s: %(error)s") % {'consumer_uuid': consumer_uuid, 'error': exc}) except exception.InvalidInventory as exc: raise webob.exc.HTTPConflict( _('Unable to allocate inventory: %(error)s') % {'error': exc}) except exception.ConcurrentUpdateDetected as exc: raise webob.exc.HTTPConflict( _('Inventory and/or allocations changed while attempting to ' 'allocate: %(error)s') % {'error': exc}, comment=errors.CONCURRENT_UPDATE) 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 lists of Inventory objects, keyed 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 for provider %(rp)s: ' 'actual: %(actual)s, given: %(given)s' % { 'rp': rp_uuid, '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_object = inventory.make_inventory_object( resource_provider, res_class, **inv_data) inv_list.append(inv_object) inventory_by_rp[resource_provider] = 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) reshaper.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 set_allocations(req): context = req.environ['placement.context'] context.can(policies.ALLOC_MANAGE) want_version = req.environ[microversion.MICROVERSION_ENVIRON] want_schema = schema.POST_ALLOCATIONS_V1_13 if want_version.matches((1, 28)): want_schema = schema.POST_ALLOCATIONS_V1_28 data = util.extract_json(req.body, want_schema) # First, ensure that all consumers referenced in the payload actually # exist. And if not, create them. Keep a record of auto-created consumers # so we can clean them up if the end allocation replace_all() fails. consumers = {} # dict of Consumer objects, keyed by consumer UUID new_consumers_created = [] for consumer_uuid in data: project_id = data[consumer_uuid]['project_id'] user_id = data[consumer_uuid]['user_id'] consumer_generation = data[consumer_uuid].get('consumer_generation') try: consumer, new_consumer_created = util.ensure_consumer( context, consumer_uuid, project_id, user_id, consumer_generation, want_version) if new_consumer_created: new_consumers_created.append(consumer) consumers[consumer_uuid] = consumer except Exception: # If any errors (for instance, a consumer generation conflict) # occur when ensuring consumer records above, make sure we delete # any auto-created consumers. _delete_consumers(new_consumers_created) raise # Create a sequence of allocation objects to be used in one # AllocationList.replace_all() call, which will mean all the changes # happen within a single transaction and with resource provider # and consumer generations (if applicable) check all in one go. allocations = create_allocation_list(context, data, consumers) def _create_allocations(alloc_list): try: alloc_list.replace_all() LOG.debug("Successfully wrote allocations %s", alloc_list) except Exception: _delete_consumers(new_consumers_created) raise try: _create_allocations(allocations) except exception.NotFound as exc: raise webob.exc.HTTPBadRequest( _("Unable to allocate inventory %(error)s") % {'error': exc}) except exception.InvalidInventory as exc: # InvalidInventory is a parent for several exceptions that # indicate either that Inventory is not present, or that # capacity limits have been exceeded. raise webob.exc.HTTPConflict( _('Unable to allocate inventory: %(error)s') % {'error': exc}) except exception.ConcurrentUpdateDetected as exc: raise webob.exc.HTTPConflict( _('Inventory and/or allocations changed while attempting to ' 'allocate: %(error)s') % {'error': exc}, comment=errors.CONCURRENT_UPDATE) req.response.status = 204 req.response.content_type = None return req.response
def create_resource_provider(req): """POST to create a resource provider. On success return a 201 response with an empty body (microversions 1.0 - 1.19) or a 200 response with a payload representing the newly created resource provider (microversions 1.20 - latest), and a location header pointing to the resource provider. """ context = req.environ['placement.context'] context.can(policies.CREATE) schema = rp_schema.POST_RESOURCE_PROVIDER_SCHEMA want_version = req.environ[microversion.MICROVERSION_ENVIRON] if want_version.matches((1, 14)): schema = rp_schema.POST_RP_SCHEMA_V1_14 data = util.extract_json(req.body, schema) try: if data.get('uuid'): # Normalize UUID with no proper dashes into dashed one # with format {8}-{4}-{4}-{4}-{12} data['uuid'] = str(uuidlib.UUID(data['uuid'])) else: data['uuid'] = uuidutils.generate_uuid() resource_provider = rp_obj.ResourceProvider(context, **data) resource_provider.create() except db_exc.DBDuplicateEntry as exc: # Whether exc.columns has one or two entries (in the event # of both fields being duplicates) appears to be database # dependent, so going with the complete solution here. duplicates = [] for column in exc.columns: # For MySQL, this is error 1062: # # Duplicate entry '%s' for key %d # # The 'key' value is captured in 'DBDuplicateEntry.columns'. # Despite the name, this isn't always a column name. While MySQL # 5.x does indeed use the name of the column, 8.x uses the name of # the constraint. oslo.db should probably fix this, but until that # happens we need to handle both cases if column == 'uniq_resource_providers0uuid': duplicates.append(f'uuid: {data["uuid"]}') elif column == 'uniq_resource_providers0name': duplicates.append(f'name: {data["name"]}') else: duplicates.append(f'{column}: {data[column]}') raise webob.exc.HTTPConflict( 'Conflicting resource provider %(duplicate)s already exists.' % {'duplicate': ', '.join(duplicates)}, comment=errors.DUPLICATE_NAME) except exception.ObjectActionError as exc: raise webob.exc.HTTPBadRequest( 'Unable to create resource provider "%(name)s", %(rp_uuid)s: ' '%(error)s' % { 'name': data['name'], 'rp_uuid': data['uuid'], 'error': exc }) req.response.location = util.resource_provider_url(req.environ, resource_provider) if want_version.matches(min_version=(1, 20)): req.response.body = encodeutils.to_utf8( jsonutils.dumps( _serialize_provider(req.environ, resource_provider, want_version))) req.response.content_type = 'application/json' modified = util.pick_last_modified(None, resource_provider) req.response.last_modified = modified req.response.cache_control = 'no-cache' else: req.response.status = 201 req.response.content_type = None return req.response
def set_allocations(req): context = req.environ['placement.context'] context.can(policies.ALLOC_MANAGE) want_version = req.environ[microversion.MICROVERSION_ENVIRON] want_schema = schema.POST_ALLOCATIONS_V1_13 if want_version.matches((1, 28)): want_schema = schema.POST_ALLOCATIONS_V1_28 if want_version.matches((1, 34)): want_schema = schema.POST_ALLOCATIONS_V1_34 if want_version.matches((1, 38)): want_schema = schema.POST_ALLOCATIONS_V1_38 data = util.extract_json(req.body, want_schema) consumers, new_consumers_created, requested_attrs = inspect_consumers( context, data, want_version) # Create a sequence of allocation objects to be used in one # alloc_obj.replace_all() call, which will mean all the changes happen # within a single transaction and with resource provider and consumer # generations (if applicable) check all in one go. allocations = create_allocation_list(context, data, consumers) @db_api.placement_context_manager.writer def _update_consumers_and_create_allocations(ctx): # Update consumer attributes if requested attributes are different. # NOTE(melwitt): This will not raise ConcurrentUpdateDetected, that # happens later in AllocationList.replace_all() data_util.update_consumers(consumers.values(), requested_attrs) alloc_obj.replace_all(ctx, allocations) LOG.debug("Successfully wrote allocations %s", allocations) def _create_allocations(): try: # NOTE(melwitt): Group the consumer and allocation database updates # in a single transaction so that updates get rolled back # automatically in the event of a consumer generation conflict. _update_consumers_and_create_allocations(context) except Exception: with excutils.save_and_reraise_exception(): delete_consumers(new_consumers_created) try: _create_allocations() except exception.NotFound as exc: raise webob.exc.HTTPBadRequest( "Unable to allocate inventory %(error)s" % {'error': exc}) except exception.InvalidInventory as exc: # InvalidInventory is a parent for several exceptions that # indicate either that Inventory is not present, or that # capacity limits have been exceeded. raise webob.exc.HTTPConflict( 'Unable to allocate inventory: %(error)s' % {'error': exc}) except exception.ConcurrentUpdateDetected as exc: raise webob.exc.HTTPConflict( 'Inventory and/or allocations changed while attempting to ' 'allocate: %(error)s' % {'error': exc}, comment=errors.CONCURRENT_UPDATE) 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'] # Get the current inventory for all the RPs included. Compare it to the # passed inventories; if they don't have the same resources, then something # is off, and reject the reshape. curr_resources = [] proposed_resources = [] for rp_uuid, inv_data in inventories.items(): curr_resources += list( rp_obj.get_current_inventory_resources(context, rp_uuid, include_total=True)) for rc_name, rc_vals in inv_data["inventories"].items(): proposed_resources.append((rc_name, rc_vals["total"])) curr_resources.sort() proposed_resources.sort() if not curr_resources == proposed_resources: raise webob.exc.HTTPBadRequest("Proposed reshaped inventory does not " "match existing inventory.") # Now check the allocations to make sure that they match curr_allocs = defaultdict(int) proposed_allocs = defaultdict(int) alloc_data = [itm["allocations"] for itm in allocations.values()] # First, get all the unique RPs rp_dict_keys = [itm.keys() for itm in alloc_data] # Convert dict_keys data to lists rp_lists = [list(itm) for itm in rp_dict_keys] # Collapse the lists of lists to a single list, and get the unique values rp_set = set(list(itertools.chain(*rp_lists))) # Get the current allocations for each RP for rp in rp_set: alloc_dict = rp_obj.get_allocated_inventory(context, rp) for key, val in alloc_dict.items(): curr_allocs[key] += val # Now sum up the proposed allocations. The RC name and amount are in a # deeply-nested dict. for rdict in alloc_data: for _, rsrc in rdict.items(): for _, rc in rsrc.items(): for rc_name, amt in rc.items(): proposed_allocs[rc_name] += amt if not curr_allocs == proposed_allocs: raise webob.HTTPBadRequest("Proposed reshaped allocations do not " "match existing allocations.") # The inventories and allocations match up, so now just switch the # connections among the entities. rp_obj.reshape(context, inventories, allocations) # ^^^ This needs to be written # We're going to create several lists of Inventory objects, keyed 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 for provider %(rp)s: ' 'actual: %(actual)s, given: %(given)s' % { 'rp': rp_uuid, '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_object = inventory.make_inventory_object( resource_provider, res_class, **inv_data) inv_list.append(inv_object) inventory_by_rp[resource_provider] = 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) reshaper.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 _set_allocations_for_consumer(req, schema): context = req.environ['placement.context'] context.can(policies.ALLOC_UPDATE) consumer_uuid = util.wsgi_path_item(req.environ, 'consumer_uuid') if not uuidutils.is_uuid_like(consumer_uuid): raise webob.exc.HTTPBadRequest( 'Malformed consumer_uuid: %(consumer_uuid)s' % {'consumer_uuid': consumer_uuid}) consumer_uuid = str(uuid.UUID(consumer_uuid)) data = util.extract_json(req.body, schema) allocation_data = data['allocations'] # Normalize allocation data to dict. want_version = req.environ[microversion.MICROVERSION_ENVIRON] if not want_version.matches((1, 12)): allocations_dict = {} # Allocation are list-ish, transform to dict-ish for allocation in allocation_data: resource_provider_uuid = allocation['resource_provider']['uuid'] allocations_dict[resource_provider_uuid] = { 'resources': allocation['resources'] } allocation_data = allocations_dict allocation_objects = [] # Consumer object saved in case we need to delete the auto-created consumer # record consumer = None # Whether we created a new consumer record created_new_consumer = False # Get or create the project, user, consumer, and consumer type. # This needs to be done in separate database transactions so that the # records can be read after a create collision due to a racing request. consumer, created_new_consumer, request_attr = (data_util.ensure_consumer( context, consumer_uuid, data.get('project_id'), data.get('user_id'), data.get('consumer_generation'), data.get('consumer_type'), want_version)) if not allocation_data: # The allocations are empty, which means wipe them out. Internal # to the allocation object this is signalled by a used value of 0. # We verified the consumer's generation in util.ensure_consumer() # NOTE(jaypipes): This will only occur 1.28+. The JSONSchema will # prevent an empty allocations object from being passed when there is # no consumer generation, so this is safe to do. allocations = alloc_obj.get_all_by_consumer_id(context, consumer_uuid) for allocation in allocations: allocation.used = 0 allocation_objects.append(allocation) else: # If the body includes an allocation for a resource provider # that does not exist, raise a 400. rp_objs = _resource_providers_by_uuid(context, allocation_data.keys()) for resource_provider_uuid, allocation in allocation_data.items(): resource_provider = rp_objs[resource_provider_uuid] new_allocations = _new_allocations(context, resource_provider, consumer, allocation['resources']) allocation_objects.extend(new_allocations) @db_api.placement_context_manager.writer def _update_consumers_and_create_allocations(ctx): # Update consumer attributes if requested attributes are different. # NOTE(melwitt): This will not raise ConcurrentUpdateDetected, that # happens later in AllocationList.replace_all() data_util.update_consumers([consumer], {consumer_uuid: request_attr}) alloc_obj.replace_all(ctx, allocation_objects) LOG.debug("Successfully wrote allocations %s", allocation_objects) def _create_allocations(): try: # NOTE(melwitt): Group the consumer and allocation database updates # in a single transaction so that updates get rolled back # automatically in the event of a consumer generation conflict. _update_consumers_and_create_allocations(context) except Exception: with excutils.save_and_reraise_exception(): if created_new_consumer: delete_consumers([consumer]) try: _create_allocations() # InvalidInventory is a parent for several exceptions that # indicate either that Inventory is not present, or that # capacity limits have been exceeded. except exception.NotFound as exc: raise webob.exc.HTTPBadRequest( "Unable to allocate inventory for consumer %(consumer_uuid)s: " "%(error)s" % { 'consumer_uuid': consumer_uuid, 'error': exc }) except exception.InvalidInventory as exc: raise webob.exc.HTTPConflict( 'Unable to allocate inventory: %(error)s' % {'error': exc}) except exception.ConcurrentUpdateDetected as exc: raise webob.exc.HTTPConflict( 'Inventory and/or allocations changed while attempting to ' 'allocate: %(error)s' % {'error': exc}, comment=errors.CONCURRENT_UPDATE) req.response.status = 204 req.response.content_type = None return req.response