5fd2d18c30
Comparing benchmarks of creating (repeated) resource classes with traits, it became clear that creating resource classes [1] was about 1/3rd faster. Comparing the code, they were very similar, however the resource class side was ordered to check for existence first before attempting to do a create(). This meant that in the fairly common case where a resource class already existed we could finish early. This change updates the code so that the common idiom used in clients of "ensuring a trait" with PUT /traits/$name routine to be the same as the "ensuring a resource class" with PUT /resource_classes/$name routine: check that it is already there, first. Since it is best practice to always ensure trait or resource class before using a CUSTOM_ one, this is a good orientation. Note that with the recent addition of the AttributeCache, these queries for get_by_name [2] go to the ResourceClass or Trait cache. However, at this stage in processing, the cache is empty and calls to get_by_name will fill it. A unit test is added to confirm that the 'put_trait' handler will look at get_by_name and then go back to assuming the trait exists if the create() fails because it already exists. The general behavior of whether a trait is created or updated is confirmed by the gabbit tests in trait.yaml. That test also confirms that we have a legit last-modified time when we fall through both exceptions and make the assumption that a trait was created by some other thread. It is safe to use 'now' as the last-modified time because it was created in this same second, just not by us. [1]2f56f379a5/placement/handlers/resource_class.py (L209)
[2]2f56f379a5/placement/objects/resource_class.py (L72)
Change-Id: I88e42ef18bd5b2616a93730241d2419c6b431276
275 lines
9.9 KiB
Python
275 lines
9.9 KiB
Python
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
"""Traits handlers for Placement API."""
|
|
|
|
import jsonschema
|
|
from oslo_serialization import jsonutils
|
|
from oslo_utils import encodeutils
|
|
from oslo_utils import timeutils
|
|
import webob
|
|
|
|
from placement import errors
|
|
from placement import exception
|
|
from placement import microversion
|
|
from placement.objects import resource_provider as rp_obj
|
|
from placement.objects import trait as trait_obj
|
|
from placement.policies import trait as policies
|
|
from placement.schemas import trait as schema
|
|
from placement import util
|
|
from placement import wsgi_wrapper
|
|
|
|
|
|
def _normalize_traits_qs_param(qs):
|
|
try:
|
|
op, value = qs.split(':', 1)
|
|
except ValueError:
|
|
msg = ('Badly formatted name parameter. Expected name query string '
|
|
'parameter in form: '
|
|
'?name=[in|startswith]:[name1,name2|prefix]. Got: "%s"')
|
|
msg = msg % qs
|
|
raise webob.exc.HTTPBadRequest(msg)
|
|
|
|
filters = {}
|
|
if op == 'in':
|
|
filters['name_in'] = value.split(',')
|
|
elif op == 'startswith':
|
|
filters['prefix'] = value
|
|
|
|
return filters
|
|
|
|
|
|
def _serialize_traits(traits, want_version):
|
|
last_modified = None
|
|
get_last_modified = want_version.matches((1, 15))
|
|
trait_names = []
|
|
for trait in traits:
|
|
if get_last_modified:
|
|
last_modified = util.pick_last_modified(last_modified, trait)
|
|
trait_names.append(trait.name)
|
|
|
|
# If there were no traits, set last_modified to now
|
|
last_modified = last_modified or timeutils.utcnow(with_timezone=True)
|
|
|
|
return {'traits': trait_names}, last_modified
|
|
|
|
|
|
@wsgi_wrapper.PlacementWsgify
|
|
@microversion.version_handler('1.6')
|
|
def put_trait(req):
|
|
context = req.environ['placement.context']
|
|
context.can(policies.TRAITS_UPDATE)
|
|
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
|
name = util.wsgi_path_item(req.environ, 'name')
|
|
|
|
try:
|
|
jsonschema.validate(name, schema.CUSTOM_TRAIT)
|
|
except jsonschema.ValidationError:
|
|
raise webob.exc.HTTPBadRequest(
|
|
'The trait is invalid. A valid trait must be no longer than '
|
|
'255 characters, start with the prefix "CUSTOM_" and use '
|
|
'following characters: "A"-"Z", "0"-"9" and "_"')
|
|
|
|
status = 204
|
|
try:
|
|
trait = trait_obj.Trait.get_by_name(context, name)
|
|
except exception.TraitNotFound:
|
|
try:
|
|
trait = trait_obj.Trait(context, name=name)
|
|
trait.create()
|
|
status = 201
|
|
except exception.TraitExists:
|
|
# Something just created the trait
|
|
pass
|
|
|
|
req.response.status = status
|
|
req.response.content_type = None
|
|
req.response.location = util.trait_url(req.environ, trait)
|
|
if want_version.matches((1, 15)):
|
|
# If the TraitExists exception was hit above, created_at is None
|
|
# so fall back to now for the last modified header.
|
|
last_modified = (trait.created_at
|
|
or timeutils.utcnow(with_timezone=True))
|
|
req.response.last_modified = last_modified
|
|
req.response.cache_control = 'no-cache'
|
|
return req.response
|
|
|
|
|
|
@wsgi_wrapper.PlacementWsgify
|
|
@microversion.version_handler('1.6')
|
|
def get_trait(req):
|
|
context = req.environ['placement.context']
|
|
context.can(policies.TRAITS_SHOW)
|
|
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
|
name = util.wsgi_path_item(req.environ, 'name')
|
|
|
|
try:
|
|
trait = trait_obj.Trait.get_by_name(context, name)
|
|
except exception.TraitNotFound as ex:
|
|
raise webob.exc.HTTPNotFound(ex.format_message())
|
|
|
|
req.response.status = 204
|
|
req.response.content_type = None
|
|
if want_version.matches((1, 15)):
|
|
req.response.last_modified = trait.created_at
|
|
req.response.cache_control = 'no-cache'
|
|
return req.response
|
|
|
|
|
|
@wsgi_wrapper.PlacementWsgify
|
|
@microversion.version_handler('1.6')
|
|
def delete_trait(req):
|
|
context = req.environ['placement.context']
|
|
context.can(policies.TRAITS_DELETE)
|
|
name = util.wsgi_path_item(req.environ, 'name')
|
|
|
|
try:
|
|
trait = trait_obj.Trait.get_by_name(context, name)
|
|
trait.destroy()
|
|
except exception.TraitNotFound as ex:
|
|
raise webob.exc.HTTPNotFound(ex.format_message())
|
|
except exception.TraitCannotDeleteStandard as ex:
|
|
raise webob.exc.HTTPBadRequest(ex.format_message())
|
|
except exception.TraitInUse as ex:
|
|
raise webob.exc.HTTPConflict(ex.format_message())
|
|
|
|
req.response.status = 204
|
|
req.response.content_type = None
|
|
return req.response
|
|
|
|
|
|
@wsgi_wrapper.PlacementWsgify
|
|
@microversion.version_handler('1.6')
|
|
@util.check_accept('application/json')
|
|
def list_traits(req):
|
|
context = req.environ['placement.context']
|
|
context.can(policies.TRAITS_LIST)
|
|
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
|
filters = {}
|
|
|
|
util.validate_query_params(req, schema.LIST_TRAIT_SCHEMA)
|
|
|
|
if 'name' in req.GET:
|
|
filters = _normalize_traits_qs_param(req.GET['name'])
|
|
if 'associated' in req.GET:
|
|
if req.GET['associated'].lower() not in ['true', 'false']:
|
|
raise webob.exc.HTTPBadRequest(
|
|
'The query parameter "associated" only accepts '
|
|
'"true" or "false"')
|
|
filters['associated'] = (
|
|
True if req.GET['associated'].lower() == 'true' else False)
|
|
|
|
traits = trait_obj.get_all(context, filters)
|
|
req.response.status = 200
|
|
output, last_modified = _serialize_traits(traits, want_version)
|
|
if want_version.matches((1, 15)):
|
|
req.response.last_modified = last_modified
|
|
req.response.cache_control = 'no-cache'
|
|
req.response.body = encodeutils.to_utf8(jsonutils.dumps(output))
|
|
req.response.content_type = 'application/json'
|
|
return req.response
|
|
|
|
|
|
@wsgi_wrapper.PlacementWsgify
|
|
@microversion.version_handler('1.6')
|
|
@util.check_accept('application/json')
|
|
def list_traits_for_resource_provider(req):
|
|
context = req.environ['placement.context']
|
|
context.can(policies.RP_TRAIT_LIST)
|
|
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
|
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
|
|
|
# Resource provider object is needed for two things: If it is
|
|
# NotFound we'll get a 404 here, which needs to happen because
|
|
# get_all_by_resource_provider can return an empty list.
|
|
# It is also needed for the generation, used in the outgoing
|
|
# representation.
|
|
try:
|
|
rp = rp_obj.ResourceProvider.get_by_uuid(context, uuid)
|
|
except exception.NotFound as exc:
|
|
raise webob.exc.HTTPNotFound(
|
|
"No resource provider with uuid %(uuid)s found: %(error)s" %
|
|
{'uuid': uuid, 'error': exc})
|
|
|
|
traits = trait_obj.get_all_by_resource_provider(context, rp)
|
|
response_body, last_modified = _serialize_traits(traits, want_version)
|
|
response_body["resource_provider_generation"] = rp.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
|
|
|
|
|
|
@wsgi_wrapper.PlacementWsgify
|
|
@microversion.version_handler('1.6')
|
|
@util.require_content('application/json')
|
|
def update_traits_for_resource_provider(req):
|
|
context = req.environ['placement.context']
|
|
context.can(policies.RP_TRAIT_UPDATE)
|
|
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,
|
|
comment=errors.CONCURRENT_UPDATE)
|
|
|
|
trait_objs = trait_obj.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
|
|
|
|
|
|
@wsgi_wrapper.PlacementWsgify
|
|
@microversion.version_handler('1.6')
|
|
def delete_traits_for_resource_provider(req):
|
|
context = req.environ['placement.context']
|
|
context.can(policies.RP_TRAIT_DELETE)
|
|
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
|
|
|
resource_provider = rp_obj.ResourceProvider.get_by_uuid(context, uuid)
|
|
try:
|
|
resource_provider.set_traits([])
|
|
except exception.ConcurrentUpdateDetected as e:
|
|
raise webob.exc.HTTPConflict(e.format_message(),
|
|
comment=errors.CONCURRENT_UPDATE)
|
|
|
|
req.response.status = 204
|
|
req.response.content_type = None
|
|
return req.response
|