Merge "[placement] Add cache headers to placement api requests"

This commit is contained in:
Zuul 2017-12-13 05:30:32 +00:00 committed by Gerrit Code Review
commit 2ca91b8ce5
26 changed files with 767 additions and 54 deletions

View File

@ -13,6 +13,7 @@
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import encodeutils from oslo_utils import encodeutils
from oslo_utils import timeutils
from nova.api.openstack.placement import microversion from nova.api.openstack.placement import microversion
from nova.api.openstack.placement import util from nova.api.openstack.placement import util
@ -30,11 +31,20 @@ PUT_AGGREGATES_SCHEMA = {
} }
def _send_aggregates(response, aggregate_uuids): def _send_aggregates(req, aggregate_uuids):
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
response = req.response
response.status = 200 response.status = 200
response.body = encodeutils.to_utf8( response.body = encodeutils.to_utf8(
jsonutils.dumps(_serialize_aggregates(aggregate_uuids))) jsonutils.dumps(_serialize_aggregates(aggregate_uuids)))
response.content_type = 'application/json' response.content_type = 'application/json'
if want_version.matches((1, 15)):
req.response.cache_control = 'no-cache'
# We never get an aggregate itself, we get the list of aggregates
# that are associated with a resource provider. We don't record the
# time when that association was made and the time when an aggregate
# uuid was created is not relevant, so here we punt and use utcnow.
req.response.last_modified = timeutils.utcnow(with_timezone=True)
return response return response
@ -59,7 +69,7 @@ def get_aggregates(req):
context, uuid) context, uuid)
aggregate_uuids = resource_provider.get_aggregates() aggregate_uuids = resource_provider.get_aggregates()
return _send_aggregates(req.response, aggregate_uuids) return _send_aggregates(req, aggregate_uuids)
@wsgi_wrapper.PlacementWsgify @wsgi_wrapper.PlacementWsgify
@ -73,4 +83,4 @@ def set_aggregates(req):
aggregate_uuids = util.extract_json(req.body, PUT_AGGREGATES_SCHEMA) aggregate_uuids = util.extract_json(req.body, PUT_AGGREGATES_SCHEMA)
resource_provider.set_aggregates(aggregate_uuids) resource_provider.set_aggregates(aggregate_uuids)
return _send_aggregates(req.response, aggregate_uuids) return _send_aggregates(req, aggregate_uuids)

View File

@ -17,6 +17,7 @@ import copy
from oslo_log import log as logging from oslo_log import log as logging
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import encodeutils from oslo_utils import encodeutils
from oslo_utils import timeutils
import webob import webob
from nova.api.openstack.placement import microversion from nova.api.openstack.placement import microversion
@ -161,7 +162,16 @@ def _allocations_dict(allocations, key_fetcher, resource_provider=None,
"""Turn allocations into a dict of resources keyed by key_fetcher.""" """Turn allocations into a dict of resources keyed by key_fetcher."""
allocation_data = collections.defaultdict(dict) allocation_data = collections.defaultdict(dict)
# NOTE(cdent): The last_modified for an allocation will always be
# based off the created_at column because allocations are only
# ever inserted, never updated.
last_modified = None
# Only calculate last-modified if we are using a microversion that
# supports it.
get_last_modified = want_version and want_version.matches((1, 15))
for allocation in allocations: for allocation in allocations:
if get_last_modified:
last_modified = util.pick_last_modified(last_modified, allocation)
key = key_fetcher(allocation) key = key_fetcher(allocation)
if 'resources' not in allocation_data[key]: if 'resources' not in allocation_data[key]:
allocation_data[key]['resources'] = {} allocation_data[key]['resources'] = {}
@ -183,7 +193,8 @@ def _allocations_dict(allocations, key_fetcher, resource_provider=None,
result['project_id'] = allocations[0].project_id result['project_id'] = allocations[0].project_id
result['user_id'] = allocations[0].user_id result['user_id'] = allocations[0].user_id
return result last_modified = last_modified or timeutils.utcnow(with_timezone=True)
return result, last_modified
def _serialize_allocations_for_consumer(allocations, want_version=None): def _serialize_allocations_for_consumer(allocations, want_version=None):
@ -245,6 +256,7 @@ def _serialize_allocations_for_resource_provider(allocations,
def list_for_consumer(req): def list_for_consumer(req):
"""List allocations associated with a consumer.""" """List allocations associated with a consumer."""
context = req.environ['placement.context'] context = req.environ['placement.context']
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
consumer_id = util.wsgi_path_item(req.environ, 'consumer_uuid') consumer_id = util.wsgi_path_item(req.environ, 'consumer_uuid')
want_version = req.environ[microversion.MICROVERSION_ENVIRON] want_version = req.environ[microversion.MICROVERSION_ENVIRON]
@ -254,13 +266,18 @@ def list_for_consumer(req):
allocations = rp_obj.AllocationList.get_all_by_consumer_id( allocations = rp_obj.AllocationList.get_all_by_consumer_id(
context, consumer_id) context, consumer_id)
allocations_json = jsonutils.dumps( output, last_modified = _serialize_allocations_for_consumer(
_serialize_allocations_for_consumer(allocations, want_version)) allocations, want_version)
allocations_json = jsonutils.dumps(output)
req.response.status = 200 response = req.response
req.response.body = encodeutils.to_utf8(allocations_json) response.status = 200
req.response.content_type = 'application/json' response.body = encodeutils.to_utf8(allocations_json)
return req.response response.content_type = 'application/json'
if want_version.matches((1, 15)):
response.last_modified = last_modified
response.cache_control = 'no-cache'
return response
@wsgi_wrapper.PlacementWsgify @wsgi_wrapper.PlacementWsgify
@ -273,6 +290,7 @@ def list_for_resource_provider(req):
# using a dict of dicts for the output we are potentially limiting # using a dict of dicts for the output we are potentially limiting
# ourselves in terms of sorting and filtering. # ourselves in terms of sorting and filtering.
context = req.environ['placement.context'] context = req.environ['placement.context']
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
uuid = util.wsgi_path_item(req.environ, 'uuid') uuid = util.wsgi_path_item(req.environ, 'uuid')
# confirm existence of resource provider so we get a reasonable # confirm existence of resource provider so we get a reasonable
@ -286,13 +304,18 @@ def list_for_resource_provider(req):
allocs = rp_obj.AllocationList.get_all_by_resource_provider(context, rp) allocs = rp_obj.AllocationList.get_all_by_resource_provider(context, rp)
allocations_json = jsonutils.dumps( output, last_modified = _serialize_allocations_for_resource_provider(
_serialize_allocations_for_resource_provider(allocs, rp)) allocs, rp)
allocations_json = jsonutils.dumps(output)
req.response.status = 200 response = req.response
req.response.body = encodeutils.to_utf8(allocations_json) response.status = 200
req.response.content_type = 'application/json' response.body = encodeutils.to_utf8(allocations_json)
return req.response response.content_type = 'application/json'
if want_version.matches((1, 15)):
response.last_modified = last_modified
response.cache_control = 'no-cache'
return response
def _new_allocations(context, resource_provider_uuid, consumer_uuid, def _new_allocations(context, resource_provider_uuid, consumer_uuid,

View File

@ -17,6 +17,7 @@ import collections
from oslo_log import log as logging from oslo_log import log as logging
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import encodeutils from oslo_utils import encodeutils
from oslo_utils import timeutils
import webob import webob
from nova.api.openstack.placement import microversion from nova.api.openstack.placement import microversion
@ -224,4 +225,7 @@ def list_allocation_candidates(req):
json_data = jsonutils.dumps(trx_cands) json_data = jsonutils.dumps(trx_cands)
response.body = encodeutils.to_utf8(json_data) response.body = encodeutils.to_utf8(json_data)
response.content_type = 'application/json' response.content_type = 'application/json'
if want_version.matches((1, 15)):
response.cache_control = 'no-cache'
response.last_modified = timeutils.utcnow(with_timezone=True)
return response return response

View File

@ -161,21 +161,33 @@ def _make_inventory_object(resource_provider, resource_class, **data):
return inventory return inventory
def _send_inventories(response, resource_provider, inventories): def _send_inventories(req, resource_provider, inventories):
"""Send a JSON representation of a list of inventories.""" """Send a JSON representation of a list of inventories."""
response = req.response
response.status = 200 response.status = 200
response.body = encodeutils.to_utf8(jsonutils.dumps( output, last_modified = _serialize_inventories(
_serialize_inventories(inventories, resource_provider.generation))) inventories, resource_provider.generation)
response.body = encodeutils.to_utf8(jsonutils.dumps(output))
response.content_type = 'application/json' response.content_type = 'application/json'
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
if want_version.matches((1, 15)):
response.last_modified = last_modified
response.cache_control = 'no-cache'
return response return response
def _send_inventory(response, resource_provider, inventory, status=200): def _send_inventory(req, resource_provider, inventory, status=200):
"""Send a JSON representation of one single inventory.""" """Send a JSON representation of one single inventory."""
response = req.response
response.status = status response.status = status
response.body = encodeutils.to_utf8(jsonutils.dumps(_serialize_inventory( response.body = encodeutils.to_utf8(jsonutils.dumps(_serialize_inventory(
inventory, generation=resource_provider.generation))) inventory, generation=resource_provider.generation)))
response.content_type = 'application/json' response.content_type = 'application/json'
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
if want_version.matches((1, 15)):
modified = util.pick_last_modified(None, inventory)
response.last_modified = modified
response.cache_control = 'no-cache'
return response return response
@ -195,11 +207,13 @@ def _serialize_inventories(inventories, generation):
inventories_by_class = {inventory.resource_class: inventory inventories_by_class = {inventory.resource_class: inventory
for inventory in inventories} for inventory in inventories}
inventories_dict = {} inventories_dict = {}
last_modified = None
for resource_class, inventory in inventories_by_class.items(): for resource_class, inventory in inventories_by_class.items():
last_modified = util.pick_last_modified(last_modified, inventory)
inventories_dict[resource_class] = _serialize_inventory( inventories_dict[resource_class] = _serialize_inventory(
inventory, generation=None) inventory, generation=None)
return {'resource_provider_generation': generation, return ({'resource_provider_generation': generation,
'inventories': inventories_dict} 'inventories': inventories_dict}, last_modified)
@wsgi_wrapper.PlacementWsgify @wsgi_wrapper.PlacementWsgify
@ -238,7 +252,7 @@ def create_inventory(req):
response = req.response response = req.response
response.location = util.inventory_url( response.location = util.inventory_url(
req.environ, resource_provider, resource_class) req.environ, resource_provider, resource_class)
return _send_inventory(response, resource_provider, inventory, return _send_inventory(req, resource_provider, inventory,
status=201) status=201)
@ -294,7 +308,7 @@ def get_inventories(req):
inv_list = rp_obj.InventoryList.get_all_by_resource_provider(context, rp) inv_list = rp_obj.InventoryList.get_all_by_resource_provider(context, rp)
return _send_inventories(req.response, rp, inv_list) return _send_inventories(req, rp, inv_list)
@wsgi_wrapper.PlacementWsgify @wsgi_wrapper.PlacementWsgify
@ -323,7 +337,7 @@ def get_inventory(req):
_('No inventory of class %(class)s for %(rp_uuid)s') % _('No inventory of class %(class)s for %(rp_uuid)s') %
{'class': resource_class, 'rp_uuid': uuid}) {'class': resource_class, 'rp_uuid': uuid})
return _send_inventory(req.response, rp, inventory) return _send_inventory(req, rp, inventory)
@wsgi_wrapper.PlacementWsgify @wsgi_wrapper.PlacementWsgify
@ -383,7 +397,7 @@ def set_inventories(req):
'%(rp_uuid)s: %(error)s') % {'rp_uuid': resource_provider.uuid, '%(rp_uuid)s: %(error)s') % {'rp_uuid': resource_provider.uuid,
'error': exc}) 'error': exc})
return _send_inventories(req.response, resource_provider, inventories) return _send_inventories(req, resource_provider, inventories)
@wsgi_wrapper.PlacementWsgify @wsgi_wrapper.PlacementWsgify
@ -468,4 +482,4 @@ def update_inventory(req):
'%(rp_uuid)s: %(error)s') % {'rp_uuid': resource_provider.uuid, '%(rp_uuid)s: %(error)s') % {'rp_uuid': resource_provider.uuid,
'error': exc}) 'error': exc})
return _send_inventory(req.response, resource_provider, inventory) return _send_inventory(req, resource_provider, inventory)

View File

@ -15,6 +15,7 @@ import copy
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import encodeutils from oslo_utils import encodeutils
from oslo_utils import timeutils
import webob import webob
from nova.api.openstack.placement import microversion from nova.api.openstack.placement import microversion
@ -56,12 +57,17 @@ def _serialize_resource_class(environ, rc):
return data return data
def _serialize_resource_classes(environ, rcs): def _serialize_resource_classes(environ, rcs, want_version):
output = [] output = []
last_modified = None
get_last_modified = want_version.matches((1, 15))
for rc in rcs: for rc in rcs:
if get_last_modified:
last_modified = util.pick_last_modified(last_modified, rc)
data = _serialize_resource_class(environ, rc) data = _serialize_resource_class(environ, rc)
output.append(data) output.append(data)
return {"resource_classes": output} last_modified = last_modified or timeutils.utcnow(with_timezone=True)
return ({"resource_classes": output}, last_modified)
@wsgi_wrapper.PlacementWsgify @wsgi_wrapper.PlacementWsgify
@ -131,6 +137,7 @@ def get_resource_class(req):
""" """
name = util.wsgi_path_item(req.environ, 'name') name = util.wsgi_path_item(req.environ, 'name')
context = req.environ['placement.context'] context = req.environ['placement.context']
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
# The containing application will catch a not found here. # The containing application will catch a not found here.
rc = rp_obj.ResourceClass.get_by_name(context, name) rc = rp_obj.ResourceClass.get_by_name(context, name)
@ -138,6 +145,13 @@ def get_resource_class(req):
_serialize_resource_class(req.environ, rc)) _serialize_resource_class(req.environ, rc))
) )
req.response.content_type = 'application/json' req.response.content_type = 'application/json'
if want_version.matches((1, 15)):
req.response.cache_control = 'no-cache'
# Non-custom resource classes will return None from pick_last_modified,
# so the 'or' causes utcnow to be used.
last_modified = util.pick_last_modified(None, rc) or timeutils.utcnow(
with_timezone=True)
req.response.last_modified = last_modified
return req.response return req.response
@ -151,13 +165,17 @@ def list_resource_classes(req):
a collection of resource classes. a collection of resource classes.
""" """
context = req.environ['placement.context'] context = req.environ['placement.context']
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
rcs = rp_obj.ResourceClassList.get_all(context) rcs = rp_obj.ResourceClassList.get_all(context)
response = req.response response = req.response
response.body = encodeutils.to_utf8(jsonutils.dumps( output, last_modified = _serialize_resource_classes(
_serialize_resource_classes(req.environ, rcs)) req.environ, rcs, want_version)
) response.body = encodeutils.to_utf8(jsonutils.dumps(output))
response.content_type = 'application/json' response.content_type = 'application/json'
if want_version.matches((1, 15)):
response.last_modified = last_modified
response.cache_control = 'no-cache'
return response return response

View File

@ -16,6 +16,7 @@ import copy
from oslo_db import exception as db_exc from oslo_db import exception as db_exc
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import encodeutils from oslo_utils import encodeutils
from oslo_utils import timeutils
from oslo_utils import uuidutils from oslo_utils import uuidutils
import webob import webob
@ -140,10 +141,15 @@ def _serialize_provider(environ, resource_provider, want_version):
def _serialize_providers(environ, resource_providers, want_version): def _serialize_providers(environ, resource_providers, want_version):
output = [] output = []
last_modified = None
get_last_modified = want_version.matches((1, 15))
for provider in resource_providers: for provider in resource_providers:
if get_last_modified:
last_modified = util.pick_last_modified(last_modified, provider)
provider_data = _serialize_provider(environ, provider, want_version) provider_data = _serialize_provider(environ, provider, want_version)
output.append(provider_data) output.append(provider_data)
return {"resource_providers": output} last_modified = last_modified or timeutils.utcnow(with_timezone=True)
return ({"resource_providers": output}, last_modified)
@wsgi_wrapper.PlacementWsgify @wsgi_wrapper.PlacementWsgify
@ -219,6 +225,7 @@ def get_resource_provider(req):
On success return a 200 with an application/json body representing On success return a 200 with an application/json body representing
the resource provider. the resource provider.
""" """
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
uuid = util.wsgi_path_item(req.environ, 'uuid') uuid = util.wsgi_path_item(req.environ, 'uuid')
# The containing application will catch a not found here. # The containing application will catch a not found here.
context = req.environ['placement.context'] context = req.environ['placement.context']
@ -226,11 +233,15 @@ def get_resource_provider(req):
resource_provider = rp_obj.ResourceProvider.get_by_uuid( resource_provider = rp_obj.ResourceProvider.get_by_uuid(
context, uuid) context, uuid)
want_version = req.environ[microversion.MICROVERSION_ENVIRON] response = req.response
req.response.body = encodeutils.to_utf8(jsonutils.dumps( response.body = encodeutils.to_utf8(jsonutils.dumps(
_serialize_provider(req.environ, resource_provider, want_version))) _serialize_provider(req.environ, resource_provider, want_version)))
req.response.content_type = 'application/json' response.content_type = 'application/json'
return req.response if want_version.matches((1, 15)):
modified = util.pick_last_modified(None, resource_provider)
response.last_modified = modified
response.cache_control = 'no-cache'
return response
@wsgi_wrapper.PlacementWsgify @wsgi_wrapper.PlacementWsgify
@ -287,9 +298,13 @@ def list_resource_providers(req):
{'error': exc}) {'error': exc})
response = req.response response = req.response
response.body = encodeutils.to_utf8(jsonutils.dumps( output, last_modified = _serialize_providers(
_serialize_providers(req.environ, resource_providers, want_version))) req.environ, resource_providers, want_version)
response.body = encodeutils.to_utf8(jsonutils.dumps(output))
response.content_type = 'application/json' response.content_type = 'application/json'
if want_version.matches((1, 15)):
response.last_modified = last_modified
response.cache_control = 'no-cache'
return response return response
@ -303,6 +318,7 @@ def update_resource_provider(req):
""" """
uuid = util.wsgi_path_item(req.environ, 'uuid') uuid = util.wsgi_path_item(req.environ, 'uuid')
context = req.environ['placement.context'] context = req.environ['placement.context']
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
# The containing application will catch a not found here. # The containing application will catch a not found here.
resource_provider = rp_obj.ResourceProvider.get_by_uuid( resource_provider = rp_obj.ResourceProvider.get_by_uuid(
@ -330,8 +346,12 @@ def update_resource_provider(req):
_('Unable to save resource provider %(rp_uuid)s: %(error)s') % _('Unable to save resource provider %(rp_uuid)s: %(error)s') %
{'rp_uuid': uuid, 'error': exc}) {'rp_uuid': uuid, 'error': exc})
req.response.body = encodeutils.to_utf8(jsonutils.dumps( response = req.response
response.status = 200
response.body = encodeutils.to_utf8(jsonutils.dumps(
_serialize_provider(req.environ, resource_provider, want_version))) _serialize_provider(req.environ, resource_provider, want_version)))
req.response.status = 200 response.content_type = 'application/json'
req.response.content_type = 'application/json' if want_version.matches((1, 15)):
return req.response response.last_modified = resource_provider.updated_at
response.cache_control = 'no-cache'
return response

View File

@ -13,6 +13,7 @@
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import encodeutils from oslo_utils import encodeutils
from oslo_utils import timeutils
from nova.api.openstack.placement import microversion from nova.api.openstack.placement import microversion
@ -21,6 +22,7 @@ from nova.api.openstack.placement import wsgi_wrapper
@wsgi_wrapper.PlacementWsgify @wsgi_wrapper.PlacementWsgify
def home(req): def home(req):
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
min_version = microversion.min_version_string() min_version = microversion.min_version_string()
max_version = microversion.max_version_string() max_version = microversion.max_version_string()
# NOTE(cdent): As sections of the api are added, links can be # NOTE(cdent): As sections of the api are added, links can be
@ -34,4 +36,7 @@ def home(req):
version_json = jsonutils.dumps({'versions': [version_data]}) version_json = jsonutils.dumps({'versions': [version_data]})
req.response.body = encodeutils.to_utf8(version_json) req.response.body = encodeutils.to_utf8(version_json)
req.response.content_type = 'application/json' req.response.content_type = 'application/json'
if want_version.matches((1, 15)):
req.response.cache_control = 'no-cache'
req.response.last_modified = timeutils.utcnow(with_timezone=True)
return req.response return req.response

View File

@ -16,6 +16,7 @@ import copy
import jsonschema import jsonschema
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import encodeutils from oslo_utils import encodeutils
from oslo_utils import timeutils
import webob import webob
from nova.api.openstack.placement import microversion from nova.api.openstack.placement import microversion
@ -85,14 +86,26 @@ def _normalize_traits_qs_param(qs):
return filters return filters
def _serialize_traits(traits): def _serialize_traits(traits, want_version):
return {'traits': [trait.name for trait in traits]} 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 @wsgi_wrapper.PlacementWsgify
@microversion.version_handler('1.6') @microversion.version_handler('1.6')
def put_trait(req): def put_trait(req):
context = req.environ['placement.context'] context = req.environ['placement.context']
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
name = util.wsgi_path_item(req.environ, 'name') name = util.wsgi_path_item(req.environ, 'name')
try: try:
@ -110,10 +123,16 @@ def put_trait(req):
trait.create() trait.create()
req.response.status = 201 req.response.status = 201
except exception.TraitExists: except exception.TraitExists:
# Get the trait that already exists to get last-modified time.
if want_version.matches((1, 15)):
trait = rp_obj.Trait.get_by_name(context, name)
req.response.status = 204 req.response.status = 204
req.response.content_type = None req.response.content_type = None
req.response.location = util.trait_url(req.environ, trait) req.response.location = util.trait_url(req.environ, trait)
if want_version.matches((1, 15)):
req.response.last_modified = trait.created_at
req.response.cache_control = 'no-cache'
return req.response return req.response
@ -121,16 +140,20 @@ def put_trait(req):
@microversion.version_handler('1.6') @microversion.version_handler('1.6')
def get_trait(req): def get_trait(req):
context = req.environ['placement.context'] context = req.environ['placement.context']
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
name = util.wsgi_path_item(req.environ, 'name') name = util.wsgi_path_item(req.environ, 'name')
try: try:
rp_obj.Trait.get_by_name(context, name) trait = rp_obj.Trait.get_by_name(context, name)
except exception.TraitNotFound as ex: except exception.TraitNotFound as ex:
raise webob.exc.HTTPNotFound( raise webob.exc.HTTPNotFound(
explanation=ex.format_message()) explanation=ex.format_message())
req.response.status = 204 req.response.status = 204
req.response.content_type = None 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 return req.response
@ -163,6 +186,7 @@ def delete_trait(req):
@util.check_accept('application/json') @util.check_accept('application/json')
def list_traits(req): def list_traits(req):
context = req.environ['placement.context'] context = req.environ['placement.context']
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
filters = {} filters = {}
try: try:
@ -185,8 +209,11 @@ def list_traits(req):
traits = rp_obj.TraitList.get_all(context, filters) traits = rp_obj.TraitList.get_all(context, filters)
req.response.status = 200 req.response.status = 200
req.response.body = encodeutils.to_utf8( output, last_modified = _serialize_traits(traits, want_version)
jsonutils.dumps(_serialize_traits(traits))) 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' req.response.content_type = 'application/json'
return req.response return req.response
@ -196,6 +223,7 @@ def list_traits(req):
@util.check_accept('application/json') @util.check_accept('application/json')
def list_traits_for_resource_provider(req): def list_traits_for_resource_provider(req):
context = req.environ['placement.context'] context = req.environ['placement.context']
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
uuid = util.wsgi_path_item(req.environ, 'uuid') uuid = util.wsgi_path_item(req.environ, 'uuid')
# Resource provider object is needed for two things: If it is # Resource provider object is needed for two things: If it is
@ -211,9 +239,13 @@ def list_traits_for_resource_provider(req):
{'uuid': uuid, 'error': exc}) {'uuid': uuid, 'error': exc})
traits = rp_obj.TraitList.get_all_by_resource_provider(context, rp) traits = rp_obj.TraitList.get_all_by_resource_provider(context, rp)
response_body = _serialize_traits(traits) response_body, last_modified = _serialize_traits(traits, want_version)
response_body["resource_provider_generation"] = rp.generation 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.status = 200
req.response.body = encodeutils.to_utf8(jsonutils.dumps(response_body)) req.response.body = encodeutils.to_utf8(jsonutils.dumps(response_body))
req.response.content_type = 'application/json' req.response.content_type = 'application/json'
@ -225,6 +257,7 @@ def list_traits_for_resource_provider(req):
@util.require_content('application/json') @util.require_content('application/json')
def update_traits_for_resource_provider(req): def update_traits_for_resource_provider(req):
context = req.environ['placement.context'] context = req.environ['placement.context']
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
uuid = util.wsgi_path_item(req.environ, 'uuid') uuid = util.wsgi_path_item(req.environ, 'uuid')
data = util.extract_json(req.body, SET_TRAITS_FOR_RP_SCHEMA) data = util.extract_json(req.body, SET_TRAITS_FOR_RP_SCHEMA)
rp_gen = data['resource_provider_generation'] rp_gen = data['resource_provider_generation']
@ -248,9 +281,12 @@ def update_traits_for_resource_provider(req):
resource_provider.set_traits(trait_objs) resource_provider.set_traits(trait_objs)
response_body = _serialize_traits(trait_objs) response_body, last_modified = _serialize_traits(trait_objs, want_version)
response_body[ response_body[
'resource_provider_generation'] = resource_provider.generation '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.status = 200
req.response.body = encodeutils.to_utf8(jsonutils.dumps(response_body)) req.response.body = encodeutils.to_utf8(jsonutils.dumps(response_body))
req.response.content_type = 'application/json' req.response.content_type = 'application/json'

View File

@ -13,6 +13,7 @@
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import encodeutils from oslo_utils import encodeutils
from oslo_utils import timeutils
import webob import webob
from nova.api.openstack.placement import microversion from nova.api.openstack.placement import microversion
@ -64,6 +65,7 @@ def list_usages(req):
""" """
context = req.environ['placement.context'] context = req.environ['placement.context']
uuid = util.wsgi_path_item(req.environ, 'uuid') uuid = util.wsgi_path_item(req.environ, 'uuid')
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
# Resource provider object needed for two things: If it is # Resource provider object needed for two things: If it is
# NotFound we'll get a 404 here, which needs to happen because # NotFound we'll get a 404 here, which needs to happen because
@ -85,6 +87,14 @@ def list_usages(req):
response.body = encodeutils.to_utf8(jsonutils.dumps( response.body = encodeutils.to_utf8(jsonutils.dumps(
_serialize_usages(resource_provider, usage))) _serialize_usages(resource_provider, usage)))
req.response.content_type = 'application/json' req.response.content_type = 'application/json'
if want_version.matches((1, 15)):
req.response.cache_control = 'no-cache'
# While it would be possible to generate a last-modified time
# based on the collection of allocations that result in a usage
# value (with some spelunking in the SQL) that doesn't align with
# the question that is being asked in a request for usages: What
# is the usage, now? So the last-modified time is set to utcnow.
req.response.last_modified = timeutils.utcnow(with_timezone=True)
return req.response return req.response
@ -99,6 +109,7 @@ def get_total_usages(req):
Return 404 Not Found if the wanted microversion does not match. Return 404 Not Found if the wanted microversion does not match.
""" """
context = req.environ['placement.context'] context = req.environ['placement.context']
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
schema = GET_USAGES_SCHEMA_1_9 schema = GET_USAGES_SCHEMA_1_9
@ -115,4 +126,12 @@ def get_total_usages(req):
for resource in usages}} for resource in usages}}
response.body = encodeutils.to_utf8(jsonutils.dumps(usages_dict)) response.body = encodeutils.to_utf8(jsonutils.dumps(usages_dict))
req.response.content_type = 'application/json' req.response.content_type = 'application/json'
if want_version.matches((1, 15)):
req.response.cache_control = 'no-cache'
# While it would be possible to generate a last-modified time
# based on the collection of allocations that result in a usage
# value (with some spelunking in the SQL) that doesn't align with
# the question that is being asked in a request for usages: What
# is the usage, now? So the last-modified time is set to utcnow.
req.response.last_modified = timeutils.utcnow(with_timezone=True)
return req.response return req.response

View File

@ -54,9 +54,9 @@ VERSIONS = [
# as GET. The 'allocation_requests' format in GET # as GET. The 'allocation_requests' format in GET
# /allocation_candidates is updated to be the same as well. # /allocation_candidates is updated to be the same as well.
'1.13', # Adds POST /allocations to set allocations for multiple consumers '1.13', # Adds POST /allocations to set allocations for multiple consumers
# as GET
'1.14', # Adds parent and root provider UUID on resource provider '1.14', # Adds parent and root provider UUID on resource provider
# representation and 'in_tree' filter on GET /resource_providers # representation and 'in_tree' filter on GET /resource_providers
'1.15', # Include last-modified and cache-control headers
] ]

View File

@ -197,3 +197,13 @@ A new ``in_tree=<UUID>`` parameter is now available in the ``GET
/resource-providers`` API call. Supplying a UUID value for the ``in_tree`` /resource-providers`` API call. Supplying a UUID value for the ``in_tree``
parameter will cause all resource providers within the "provider tree" of the parameter will cause all resource providers within the "provider tree" of the
provider matching ``<UUID>`` to be returned. provider matching ``<UUID>`` to be returned.
1.15 Add 'last-modified' and 'cache-control' headers
----------------------------------------------------
Throughout the API, 'last-modified' headers have been added to GET responses
and those PUT and POST responses that have bodies. The value is either the
actual last modified time of the most recently modified associated database
entity or the current time if there is no direct mapping to the database. In
addition, 'cache-control: no-cache' headers are added where the 'last-modified'
header has been added to prevent inadvertent caching of resources.

View File

@ -17,6 +17,7 @@ import re
import jsonschema import jsonschema
from oslo_middleware import request_id from oslo_middleware import request_id
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_utils import timeutils
from oslo_utils import uuidutils from oslo_utils import uuidutils
import webob import webob
@ -124,6 +125,25 @@ def json_error_formatter(body, status, title, environ):
return {'errors': [error_dict]} return {'errors': [error_dict]}
def pick_last_modified(last_modified, obj):
"""Choose max of last_modified and obj.updated_at or obj.created_at.
If updated_at is not implemented in `obj` use the current time in UTC.
"""
try:
current_modified = (obj.updated_at or obj.created_at)
except NotImplementedError:
# If updated_at is not implemented, we are looking at objects that
# have not come from the database, so "now" is the right modified
# time.
current_modified = timeutils.utcnow(with_timezone=True)
if last_modified:
last_modified = max(last_modified, current_modified)
else:
last_modified = current_modified
return last_modified
def require_content(content_type): def require_content(content_type):
"""Decorator to require a content type in a handler.""" """Decorator to require a content type in a handler."""
def decorator(f): def decorator(f):

View File

@ -65,12 +65,19 @@ tests:
status: 200 status: 200
response_headers: response_headers:
content-type: /application/json/ content-type: /application/json/
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
response_json_paths: response_json_paths:
$.aggregates[0]: *agg_1 $.aggregates[0]: *agg_1
$.aggregates[1]: *agg_2 $.aggregates[1]: *agg_2
- name: get those aggregates - name: get those aggregates
GET: $LAST_URL GET: $LAST_URL
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
response_json_paths: response_json_paths:
$.aggregates.`len`: 2 $.aggregates.`len`: 2
@ -117,3 +124,26 @@ tests:
- JSON does not validate - JSON does not validate
response_json_paths: response_json_paths:
$.errors[0].title: Bad Request $.errors[0].title: Bad Request
# The next two tests confirm that prior to version 1.15 we do
# not set the cache-control or last-modified headers on either
# PUT or GET.
- name: put some aggregates v1.14
PUT: $LAST_URL
request_headers:
openstack-api-version: placement 1.14
data:
- *agg_1
- *agg_2
response_forbidden_headers:
- last-modified
- cache-control
- name: get those aggregates v1.14
GET: $LAST_URL
request_headers:
openstack-api-version: placement 1.14
response_forbidden_headers:
- last-modified
- cache-control

View File

@ -83,6 +83,11 @@ tests:
# storage show correct capacity and usage # storage show correct capacity and usage
$.provider_summaries["$ENVIRON['SS_UUID']"].resources[DISK_GB].capacity: 1900 # 1.0 * 2000 - 100G $.provider_summaries["$ENVIRON['SS_UUID']"].resources[DISK_GB].capacity: 1900 # 1.0 * 2000 - 100G
$.provider_summaries["$ENVIRON['SS_UUID']"].resources[DISK_GB].used: 0 $.provider_summaries["$ENVIRON['SS_UUID']"].resources[DISK_GB].used: 0
response_forbidden_headers:
# In the default microversion in this file (1.10) the cache headers
# are not preset.
- cache-control
- last-modified
# Verify the 1.12 format of the allocation_requests sub object which # Verify the 1.12 format of the allocation_requests sub object which
# changes from a list-list to dict-ish format. # changes from a list-list to dict-ish format.
@ -111,3 +116,13 @@ tests:
# Verify that shared storage provider only has DISK_GB listed in the # Verify that shared storage provider only has DISK_GB listed in the
# resource requests, but is listed twice # resource requests, but is listed twice
$.allocation_requests..allocations["$ENVIRON['SS_UUID']"].resources[DISK_GB]: [100, 100] $.allocation_requests..allocations["$ENVIRON['SS_UUID']"].resources[DISK_GB]: [100, 100]
- name: get allocation candidates cache headers
GET: /allocation_candidates?resources=VCPU:1,MEMORY_MB:1024,DISK_GB:100
request_headers:
# microversion 1.15 to cause cache headers
openstack-api-version: placement 1.15
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/

View File

@ -373,6 +373,11 @@ tests:
DISK_GB: 2 DISK_GB: 2
VCPU: 8 VCPU: 8
status: 204 status: 204
# These headers should not be present in any microversion on PUT
# because there is no response body.
response_forbidden_headers:
- cache-control
- last-modified
- name: get those allocations for consumer - name: get those allocations for consumer
GET: /allocations/1835b1c9-1c61-45af-9eb3-3e0e9f29487b GET: /allocations/1835b1c9-1c61-45af-9eb3-3e0e9f29487b
@ -409,3 +414,37 @@ tests:
- Allocation for resource provider 'be8b9cba-e7db-4a12-a386-99b4242167fe' that does not exist - Allocation for resource provider 'be8b9cba-e7db-4a12-a386-99b4242167fe' that does not exist
response_json_paths: response_json_paths:
$.errors[0].title: Bad Request $.errors[0].title: Bad Request
- name: get allocations for resource provider with cache headers 1.15
GET: /resource_providers/fcfa516a-abbe-45d1-8152-d5225d82e596/allocations
request_headers:
openstack-api-version: placement 1.15
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
- name: get allocations for resource provider without cache headers 1.14
GET: /resource_providers/fcfa516a-abbe-45d1-8152-d5225d82e596/allocations
request_headers:
openstack-api-version: placement 1.14
response_forbidden_headers:
- cache-control
- last-modified
- name: get allocations for consumer with cache headers 1.15
GET: /allocations/1835b1c9-1c61-45af-9eb3-3e0e9f29487b
request_headers:
openstack-api-version: placement 1.15
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
- name: get allocations for consumer without cache headers 1.14
GET: /allocations/1835b1c9-1c61-45af-9eb3-3e0e9f29487b
request_headers:
openstack-api-version: placement 1.14
response_forbidden_headers:
- cache-control
- last-modified

View File

@ -159,3 +159,20 @@ tests:
$.errors[0].title: Not Found $.errors[0].title: Not Found
response_strings: response_strings:
- The resource could not be found. - The resource could not be found.
- name: root at 1.15 has cache headers
GET: /
request_headers:
openstack-api-version: placement 1.15
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
- name: root at 1.14 no cache headers
GET: /
request_headers:
openstack-api-version: placement 1.14
response_forbidden_headers:
- last-modified
- cache-control

View File

@ -166,6 +166,13 @@ tests:
- name: get that inventory - name: get that inventory
GET: $LOCATION GET: $LOCATION
status: 200 status: 200
request_headers:
# set microversion to 1.15 to get timestamp headers
openstack-api-version: placement 1.15
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
response_json_paths: response_json_paths:
$.resource_provider_generation: 1 $.resource_provider_generation: 1
$.total: 2048 $.total: 2048
@ -175,6 +182,15 @@ tests:
$.step_size: 10 $.step_size: 10
$.allocation_ratio: 1.0 $.allocation_ratio: 1.0
- name: get inventory v1.14 no cache headers
GET: $LAST_URL
status: 200
request_headers:
openstack-api-version: placement 1.14
response_forbidden_headers:
- cache-control
- last-modified
- name: modify the inventory - name: modify the inventory
PUT: $LAST_URL PUT: $LAST_URL
request_headers: request_headers:
@ -310,6 +326,13 @@ tests:
- name: get list of inventories - name: get list of inventories
GET: /resource_providers/$ENVIRON['RP_UUID']/inventories GET: /resource_providers/$ENVIRON['RP_UUID']/inventories
request_headers:
# set microversion to 1.15 to get timestamp headers
openstack-api-version: placement 1.15
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
response_json_paths: response_json_paths:
$.resource_provider_generation: 2 $.resource_provider_generation: 2
$.inventories.DISK_GB.total: 2048 $.inventories.DISK_GB.total: 2048
@ -407,6 +430,8 @@ tests:
PUT: $LOCATION/inventories PUT: $LOCATION/inventories
request_headers: request_headers:
content-type: application/json content-type: application/json
# set microversion to 1.15 to get timestamp headers
openstack-api-version: placement 1.15
data: data:
resource_provider_generation: 0 resource_provider_generation: 0
inventories: inventories:
@ -415,6 +440,10 @@ tests:
DISK_GB: DISK_GB:
total: 1024 total: 1024
status: 200 status: 200
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
response_json_paths: response_json_paths:
$.resource_provider_generation: 1 $.resource_provider_generation: 1
$.inventories.IPV4_ADDRESS.total: 253 $.inventories.IPV4_ADDRESS.total: 253

View File

@ -39,13 +39,13 @@ tests:
response_json_paths: response_json_paths:
$.errors[0].title: Not Acceptable $.errors[0].title: Not Acceptable
- name: latest microversion is 1.14 - name: latest microversion is 1.15
GET: / GET: /
request_headers: request_headers:
openstack-api-version: placement latest openstack-api-version: placement latest
response_headers: response_headers:
vary: /OpenStack-API-Version/ vary: /OpenStack-API-Version/
openstack-api-version: placement 1.14 openstack-api-version: placement 1.15
- name: other accept header bad version - name: other accept header bad version
GET: / GET: /

View File

@ -0,0 +1,117 @@
# Confirm the behavior and presence of last-modified headers for resource
# classes across multiple microversions.
#
# We have the following routes, with associated microversion, and bodies.
#
# '/resource_classes': {
# 'GET': resource_class.list_resource_classes,
# v1.2, body
# 'POST': resource_class.create_resource_class
# v1.2, no body
# },
# '/resource_classes/{name}': {
# 'GET': resource_class.get_resource_class,
# v1.2, body
# 'PUT': resource_class.update_resource_class,
# v1.2, body, but time's arrow
# v1.7, no body
# 'DELETE': resource_class.delete_resource_class,
# v1.2, no body
# },
#
# This means that in 1.15 we only expect last-modified headers for
# the two GET requests, for the other requests we should confirm it
# is not there.
fixtures:
- APIFixture
defaults:
request_headers:
x-auth-token: admin
accept: application/json
content-type: application/json
openstack-api-version: placement 1.15
tests:
- name: get resource classes
desc: last modified is now with standards only
GET: /resource_classes
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
- name: create a custom class
PUT: /resource_classes/CUSTOM_MOO_MACHINE
status: 201
response_forbidden_headers:
- last-modified
- cache-control
- name: get custom class
GET: $LAST_URL
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
- name: get standard class
GET: /resource_classes/VCPU
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
- name: post a resource class
POST: /resource_classes
data:
name: CUSTOM_ALPHA
status: 201
response_forbidden_headers:
- last-modified
- cache-control
- name: get resource classes including custom
desc: last modified will still be now with customs because of standards
GET: /resource_classes
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
- name: put a resource class 1.6 microversion
PUT: /resource_classes/CUSTOM_MOO_MACHINE
request_headers:
openstack-api-version: placement 1.6
data:
name: CUSTOM_BETA
status: 200
response_forbidden_headers:
- last-modified
- cache-control
- name: get resource classes 1.14 microversion
GET: /resource_classes
request_headers:
openstack-api-version: placement 1.14
response_forbidden_headers:
- last-modified
- cache-control
- name: get standard class 1.14 microversion
GET: /resource_classes/VCPU
request_headers:
openstack-api-version: placement 1.14
response_forbidden_headers:
- last-modified
- cache-control
- name: get custom class 1.14 microversion
GET: $LAST_URL
request_headers:
openstack-api-version: placement 1.14
response_forbidden_headers:
- last-modified
- cache-control

View File

@ -12,8 +12,15 @@ tests:
- name: what is at resource providers - name: what is at resource providers
GET: /resource_providers GET: /resource_providers
request_headers:
# microversion 1.15 for cache headers
openstack-api-version: placement 1.15
response_json_paths: response_json_paths:
$.resource_providers: [] $.resource_providers: []
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
- name: non admin forbidden - name: non admin forbidden
GET: /resource_providers GET: /resource_providers
@ -78,6 +85,12 @@ tests:
GET: /resource_providers/$ENVIRON['RP_UUID'] GET: /resource_providers/$ENVIRON['RP_UUID']
request_headers: request_headers:
content-type: application/json content-type: application/json
openstack-api-version: placement 1.15
response_headers:
content-type: application/json
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
response_json_paths: response_json_paths:
$.uuid: $ENVIRON['RP_UUID'] $.uuid: $ENVIRON['RP_UUID']
$.name: $ENVIRON['RP_NAME'] $.name: $ENVIRON['RP_NAME']
@ -104,6 +117,8 @@ tests:
- name: list one resource providers - name: list one resource providers
GET: /resource_providers GET: /resource_providers
request_headers:
openstack-api-version: placement 1.15
response_json_paths: response_json_paths:
$.resource_providers.`len`: 1 $.resource_providers.`len`: 1
$.resource_providers[0].uuid: $ENVIRON['RP_UUID'] $.resource_providers[0].uuid: $ENVIRON['RP_UUID']
@ -113,6 +128,10 @@ tests:
$.resource_providers[0].links[?rel = "self"].href: /resource_providers/$ENVIRON['RP_UUID'] $.resource_providers[0].links[?rel = "self"].href: /resource_providers/$ENVIRON['RP_UUID']
$.resource_providers[0].links[?rel = "inventories"].href: /resource_providers/$ENVIRON['RP_UUID']/inventories $.resource_providers[0].links[?rel = "inventories"].href: /resource_providers/$ENVIRON['RP_UUID']/inventories
$.resource_providers[0].links[?rel = "usages"].href: /resource_providers/$ENVIRON['RP_UUID']/usages $.resource_providers[0].links[?rel = "usages"].href: /resource_providers/$ENVIRON['RP_UUID']/usages
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
- name: filter out all resource providers by name - name: filter out all resource providers by name
GET: /resource_providers?name=flubblebubble GET: /resource_providers?name=flubblebubble
@ -181,11 +200,15 @@ tests:
PUT: /resource_providers/$RESPONSE['$.resource_providers[0].uuid'] PUT: /resource_providers/$RESPONSE['$.resource_providers[0].uuid']
request_headers: request_headers:
content-type: application/json content-type: application/json
openstack-api-version: placement 1.15
data: data:
name: new name name: new name
status: 200 status: 200
response_headers: response_headers:
content-type: /application/json/ content-type: /application/json/
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
response_forbidden_headers: response_forbidden_headers:
- location - location
response_json_paths: response_json_paths:
@ -491,3 +514,11 @@ tests:
- "Failed validating 'maxLength'" - "Failed validating 'maxLength'"
response_json_paths: response_json_paths:
$.errors[0].title: Bad Request $.errors[0].title: Bad Request
- name: confirm no cache-control headers before 1.15
GET: /resource_providers
request_headers:
openstack-api-version: placement 1.14
response_forbidden_headers:
- cache-control
- last-modified

View File

@ -5,7 +5,8 @@ fixtures:
defaults: defaults:
request_headers: request_headers:
x-auth-token: admin x-auth-token: admin
openstack-api-version: placement latest # traits introduced in 1.6
openstack-api-version: placement 1.6
tests: tests:
@ -34,6 +35,9 @@ tests:
location: //traits/CUSTOM_TRAIT_1/ location: //traits/CUSTOM_TRAIT_1/
response_forbidden_headers: response_forbidden_headers:
- content-type - content-type
# PUT in 1.6 version should not have cache headers
- cache-control
- last-modified
- name: create a trait which existed - name: create a trait which existed
PUT: /traits/CUSTOM_TRAIT_1 PUT: /traits/CUSTOM_TRAIT_1
@ -48,6 +52,9 @@ tests:
status: 204 status: 204
response_forbidden_headers: response_forbidden_headers:
- content-type - content-type
# In early versions cache headers should not be present
- cache-control
- last-modified
- name: get a non-existed trait - name: get a non-existed trait
GET: /traits/NON_EXISTED GET: /traits/NON_EXISTED
@ -56,6 +63,11 @@ tests:
- name: delete a trait - name: delete a trait
DELETE: /traits/CUSTOM_TRAIT_1 DELETE: /traits/CUSTOM_TRAIT_1
status: 204 status: 204
response_forbidden_headers:
- content-type
# DELETE in any version should not have cache headers
- cache-control
- last-modified
- name: delete a non-existed trait - name: delete a non-existed trait
DELETE: /traits/CUSTOM_NON_EXSITED DELETE: /traits/CUSTOM_NON_EXSITED
@ -133,6 +145,61 @@ tests:
response_strings: response_strings:
- "Invalid query string parameters: Additional properties are not allowed" - "Invalid query string parameters: Additional properties are not allowed"
- name: list traits 1.14 no cache headers
GET: /traits
request_headers:
openstack-api-version: placement 1.14
response_forbidden_headers:
- cache-control
- last-modified
- name: list traits 1.15 has cache headers
GET: /traits
request_headers:
openstack-api-version: placement 1.15
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
- name: get trait 1.14 no cache headers
GET: /traits/CUSTOM_TRAIT_1
request_headers:
openstack-api-version: placement 1.14
status: 204
response_forbidden_headers:
- cache-control
- last-modified
- name: get trait 1.15 has cache headers
GET: /traits/CUSTOM_TRAIT_1
request_headers:
openstack-api-version: placement 1.15
status: 204
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
- name: put trait 1.14 no cache headers
PUT: /traits/CUSTOM_TRAIT_1
request_headers:
openstack-api-version: placement 1.14
status: 204
response_forbidden_headers:
- cache-control
- last-modified
- name: put trait 1.15 has cache headers
PUT: /traits/CUSTOM_TRAIT_1
request_headers:
openstack-api-version: placement 1.15
status: 204
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
- name: post new resource provider - name: post new resource provider
POST: /resource_providers POST: /resource_providers
request_headers: request_headers:
@ -152,6 +219,10 @@ tests:
response_json_paths: response_json_paths:
$.resource_provider_generation: 0 $.resource_provider_generation: 0
$.traits.`len`: 0 $.traits.`len`: 0
response_forbidden_headers:
# In 1.6 no cache headers
- cache-control
- last-modified
- name: set traits for resource provider - name: set traits for resource provider
PUT: /resource_providers/$ENVIRON['RP_UUID']/traits PUT: /resource_providers/$ENVIRON['RP_UUID']/traits
@ -169,6 +240,10 @@ tests:
response_strings: response_strings:
- CUSTOM_TRAIT_1 - CUSTOM_TRAIT_1
- CUSTOM_TRAIT_2 - CUSTOM_TRAIT_2
response_forbidden_headers:
# In 1.6 no cache headers
- cache-control
- last-modified
- name: get associated traits - name: get associated traits
GET: /traits?associated=true GET: /traits?associated=true
@ -272,3 +347,58 @@ tests:
status: 404 status: 404
response_strings: response_strings:
- No resource provider with uuid non_existed found - No resource provider with uuid non_existed found
- name: empty traits for resource provider 1.15 has cache headers
GET: /resource_providers/$ENVIRON['RP_UUID']/traits
request_headers:
openstack-api-version: placement 1.15
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
- name: update rp trait 1.14 no cache headers
PUT: /resource_providers/$ENVIRON['RP_UUID']/traits
data:
traits:
- CUSTOM_TRAIT_1
- CUSTOM_TRAIT_2
resource_provider_generation: 2
request_headers:
openstack-api-version: placement 1.14
content-type: application/json
response_forbidden_headers:
- cache-control
- last-modified
- name: update rp trait 1.15 has cache headers
PUT: /resource_providers/$ENVIRON['RP_UUID']/traits
data:
traits:
- CUSTOM_TRAIT_1
- CUSTOM_TRAIT_2
resource_provider_generation: 3
request_headers:
openstack-api-version: placement 1.15
content-type: application/json
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
- name: list traits for resource provider 1.14 no cache headers
GET: /resource_providers/$ENVIRON['RP_UUID']/traits
request_headers:
openstack-api-version: placement 1.14
response_forbidden_headers:
- cache-control
- last-modified
- name: list traits for resource provider 1.15 has cache headers
GET: /resource_providers/$ENVIRON['RP_UUID']/traits
request_headers:
openstack-api-version: placement 1.15
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/

View File

@ -38,6 +38,21 @@ tests:
response_json_paths: response_json_paths:
usages: {} usages: {}
- name: get usages no cache headers base microversion
GET: $LAST_URL
response_forbidden_headers:
- last-modified
- cache-control
- name: get usages cache headers 1.15
GET: $LAST_URL
request_headers:
openstack-api-version: placement 1.15
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/
- name: get total usages earlier version - name: get total usages earlier version
GET: /usages?project_id=$ENVIRON['PROJECT_ID'] GET: /usages?project_id=$ENVIRON['PROJECT_ID']
request_headers: request_headers:

View File

@ -65,6 +65,10 @@ tests:
request_headers: request_headers:
openstack-api-version: placement 1.9 openstack-api-version: placement 1.9
status: 200 status: 200
# In pre 1.15 microversions cache headers not present
response_forbidden_headers:
- last-modified
- cache-control
response_json_paths: response_json_paths:
$.usages.DISK_GB: 20 $.usages.DISK_GB: 20
$.usages.VCPU: 1 $.usages.VCPU: 1
@ -87,3 +91,12 @@ tests:
$.project_id: $ENVIRON['PROJECT_ID'] $.project_id: $ENVIRON['PROJECT_ID']
$.user_id: $ENVIRON['USER_ID'] $.user_id: $ENVIRON['USER_ID']
$.`len`: 3 $.`len`: 3
- name: get total usages with cache headers
GET: /usages?project_id=$ENVIRON['PROJECT_ID']&user_id=$ENVIRON['ALT_USER_ID']
request_headers:
openstack-api-version: placement 1.15
response_headers:
cache-control: no-cache
# Does last-modified look like a legit timestamp?
last-modified: /^\w+, \d+ \w+ \d{4} [\d:]+ GMT$/

View File

@ -19,6 +19,7 @@ import webob
from nova.api.openstack.placement import handler from nova.api.openstack.placement import handler
from nova.api.openstack.placement.handlers import root from nova.api.openstack.placement.handlers import root
from nova.api.openstack.placement import microversion
from nova import test from nova import test
from nova.tests import uuidsentinel from nova.tests import uuidsentinel
@ -35,6 +36,9 @@ def _environ(path='/moo', method='GET'):
'SERVER_NAME': 'example.com', 'SERVER_NAME': 'example.com',
'SERVER_PORT': '80', 'SERVER_PORT': '80',
'wsgi.url_scheme': 'http', 'wsgi.url_scheme': 'http',
# The microversion version value is not used, but it
# needs to be set to avoid a KeyError.
microversion.MICROVERSION_ENVIRON: microversion.Version(1, 12),
} }

View File

@ -13,8 +13,12 @@
"""Unit tests for the utility functions used by the placement API.""" """Unit tests for the utility functions used by the placement API."""
import datetime
import fixtures import fixtures
import mock
from oslo_middleware import request_id from oslo_middleware import request_id
from oslo_utils import timeutils
import webob import webob
import six.moves.urllib.parse as urlparse import six.moves.urllib.parse as urlparse
@ -594,3 +598,83 @@ class TestParseQsResourcesAndTraits(test.NoDBTestCase):
'&required2=CUSTOM_SWITCH_BIG,CUSTOM_PHYSNET_PROD' '&required2=CUSTOM_SWITCH_BIG,CUSTOM_PHYSNET_PROD'
'&resources3=CUSTOM_MAGIC:123') '&resources3=CUSTOM_MAGIC:123')
self.assertRaises(webob.exc.HTTPBadRequest, self.do_parse, qs) self.assertRaises(webob.exc.HTTPBadRequest, self.do_parse, qs)
class TestPickLastModified(test.NoDBTestCase):
def setUp(self):
super(TestPickLastModified, self).setUp()
self.resource_provider = rp_obj.ResourceProvider(
name=uuidsentinel.rp_name, uuid=uuidsentinel.rp_uuid)
def test_updated_versus_none(self):
now = timeutils.utcnow(with_timezone=True)
self.resource_provider.updated_at = now
self.resource_provider.created_at = now
chosen_time = util.pick_last_modified(None, self.resource_provider)
self.assertEqual(now, chosen_time)
def test_created_versus_none(self):
now = timeutils.utcnow(with_timezone=True)
self.resource_provider.created_at = now
self.resource_provider.updated_at = None
chosen_time = util.pick_last_modified(None, self.resource_provider)
self.assertEqual(now, chosen_time)
def test_last_modified_less(self):
now = timeutils.utcnow(with_timezone=True)
less = now - datetime.timedelta(seconds=300)
self.resource_provider.updated_at = now
self.resource_provider.created_at = now
chosen_time = util.pick_last_modified(less, self.resource_provider)
self.assertEqual(now, chosen_time)
def test_last_modified_more(self):
now = timeutils.utcnow(with_timezone=True)
more = now + datetime.timedelta(seconds=300)
self.resource_provider.updated_at = now
self.resource_provider.created_at = now
chosen_time = util.pick_last_modified(more, self.resource_provider)
self.assertEqual(more, chosen_time)
def test_last_modified_same(self):
now = timeutils.utcnow(with_timezone=True)
self.resource_provider.updated_at = now
self.resource_provider.created_at = now
chosen_time = util.pick_last_modified(now, self.resource_provider)
self.assertEqual(now, chosen_time)
def test_no_object_time_fields_less(self):
# An unsaved ovo will not have the created_at or updated_at fields
# present on the object at all.
now = timeutils.utcnow(with_timezone=True)
less = now - datetime.timedelta(seconds=300)
with mock.patch('oslo_utils.timeutils.utcnow') as mock_utc:
mock_utc.return_value = now
chosen_time = util.pick_last_modified(
less, self.resource_provider)
self.assertEqual(now, chosen_time)
mock_utc.assert_called_once_with(with_timezone=True)
def test_no_object_time_fields_more(self):
# An unsaved ovo will not have the created_at or updated_at fields
# present on the object at all.
now = timeutils.utcnow(with_timezone=True)
more = now + datetime.timedelta(seconds=300)
with mock.patch('oslo_utils.timeutils.utcnow') as mock_utc:
mock_utc.return_value = now
chosen_time = util.pick_last_modified(
more, self.resource_provider)
self.assertEqual(more, chosen_time)
mock_utc.assert_called_once_with(with_timezone=True)
def test_no_object_time_fields_none(self):
# An unsaved ovo will not have the created_at or updated_at fields
# present on the object at all.
now = timeutils.utcnow(with_timezone=True)
with mock.patch('oslo_utils.timeutils.utcnow') as mock_utc:
mock_utc.return_value = now
chosen_time = util.pick_last_modified(
None, self.resource_provider)
self.assertEqual(now, chosen_time)
mock_utc.assert_called_once_with(with_timezone=True)

View File

@ -0,0 +1,10 @@
---
features:
- |
Throughout the API, in microversion 1.15, 'last-modified' headers have been
added to GET responses and those PUT and POST responses that have bodies.
The value is either the actual last modified time of the most recently
modified associated database entity or the current time if there is no
direct mapping to the database. In addition, 'cache-control: no-cache'
headers are added where the 'last-modified' header has been added to
prevent inadvertent caching of resources.