[placement] Add cache headers to placement api requests
In relevant requests to the placement API add last-modified and cache-control headers. According the HTTP 1.1 RFC headers last-modified headers SHOULD always be sent and should have a tie to the real last modified time. If we do send them, we need Cache-Control headers to prevent inadvertent caching of resources. This change adds a microversion 1.15 which adds the headers to GET requests and some PUT or POST requests. Despite what it says 'no-cache' means "check to see if the version you have is still valid as far as the server is concerned". Since our server doesn't currently validate conditional requests and will always return an entity, it ends up meaning "don't cache" (which is what we want). The main steps in the patch are: * To both the get single entity and get collection handlers add response.cache_control = 'no-cache' * For single entity add response.last_modified = obj.updated_at or obj.created_at * For collections, discover the max modified time when traversing the list of objects to create the serialized JSON output. In most of those loops an optimization is done where we only check for last-modified information if we have a high enough microversion such that the information will be used. This is not done when listing inventories because the expectation is that no single resource provider will ever have a huge number of inventory records. * Both of the prior steps are assisted by a new util method: pick_last_modfied. Where a time cannot be determined the current time is used. In typical placement framework fashion this has been done in a very explicit way, as it makes what the handler is doing very visible, even though it results in a bit of boilerplate. For those requests that are created from multiple objects or by doing calculations, such as usages and aggregate associations, the current time is used. The handler for PUT /traits is modified a bit more extensively than some of the others: This is because the method can either create or validate the existence of the trait. In the case where the trait already exists, we need to get it from the DB to get its created_at time. We only do this if the microversion is high enough (at least 1.15) to warrant needing the info. Because these changes add new headers (even though they don't do anything) a new microversion, 1.15, is added. Partial-Bug: #1632852 Partially-Implements: bp placement-cache-headers Change-Id: I727d4c77aaa31f0ef31c8af22c2d46cad8ab8b8e
This commit is contained in:
parent
3cec0cb584
commit
83030804cc
@ -13,6 +13,7 @@
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import timeutils
|
||||
|
||||
from nova.api.openstack.placement import microversion
|
||||
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.body = encodeutils.to_utf8(
|
||||
jsonutils.dumps(_serialize_aggregates(aggregate_uuids)))
|
||||
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
|
||||
|
||||
|
||||
@ -59,7 +69,7 @@ def get_aggregates(req):
|
||||
context, uuid)
|
||||
aggregate_uuids = resource_provider.get_aggregates()
|
||||
|
||||
return _send_aggregates(req.response, aggregate_uuids)
|
||||
return _send_aggregates(req, aggregate_uuids)
|
||||
|
||||
|
||||
@wsgi_wrapper.PlacementWsgify
|
||||
@ -73,4 +83,4 @@ def set_aggregates(req):
|
||||
aggregate_uuids = util.extract_json(req.body, PUT_AGGREGATES_SCHEMA)
|
||||
resource_provider.set_aggregates(aggregate_uuids)
|
||||
|
||||
return _send_aggregates(req.response, aggregate_uuids)
|
||||
return _send_aggregates(req, aggregate_uuids)
|
||||
|
@ -17,6 +17,7 @@ import copy
|
||||
from oslo_log import log as logging
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import timeutils
|
||||
import webob
|
||||
|
||||
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."""
|
||||
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:
|
||||
if get_last_modified:
|
||||
last_modified = util.pick_last_modified(last_modified, allocation)
|
||||
key = key_fetcher(allocation)
|
||||
if 'resources' not in allocation_data[key]:
|
||||
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['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):
|
||||
@ -245,6 +256,7 @@ def _serialize_allocations_for_resource_provider(allocations,
|
||||
def list_for_consumer(req):
|
||||
"""List allocations associated with a consumer."""
|
||||
context = req.environ['placement.context']
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
consumer_id = util.wsgi_path_item(req.environ, 'consumer_uuid')
|
||||
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(
|
||||
context, consumer_id)
|
||||
|
||||
allocations_json = jsonutils.dumps(
|
||||
_serialize_allocations_for_consumer(allocations, want_version))
|
||||
output, last_modified = _serialize_allocations_for_consumer(
|
||||
allocations, want_version)
|
||||
allocations_json = jsonutils.dumps(output)
|
||||
|
||||
req.response.status = 200
|
||||
req.response.body = encodeutils.to_utf8(allocations_json)
|
||||
req.response.content_type = 'application/json'
|
||||
return req.response
|
||||
response = req.response
|
||||
response.status = 200
|
||||
response.body = encodeutils.to_utf8(allocations_json)
|
||||
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
|
||||
@ -273,6 +290,7 @@ def list_for_resource_provider(req):
|
||||
# using a dict of dicts for the output we are potentially limiting
|
||||
# ourselves in terms of sorting and filtering.
|
||||
context = req.environ['placement.context']
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
||||
|
||||
# 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)
|
||||
|
||||
allocations_json = jsonutils.dumps(
|
||||
_serialize_allocations_for_resource_provider(allocs, rp))
|
||||
output, last_modified = _serialize_allocations_for_resource_provider(
|
||||
allocs, rp)
|
||||
allocations_json = jsonutils.dumps(output)
|
||||
|
||||
req.response.status = 200
|
||||
req.response.body = encodeutils.to_utf8(allocations_json)
|
||||
req.response.content_type = 'application/json'
|
||||
return req.response
|
||||
response = req.response
|
||||
response.status = 200
|
||||
response.body = encodeutils.to_utf8(allocations_json)
|
||||
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,
|
||||
|
@ -17,6 +17,7 @@ import collections
|
||||
from oslo_log import log as logging
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import timeutils
|
||||
import webob
|
||||
|
||||
from nova.api.openstack.placement import microversion
|
||||
@ -224,4 +225,7 @@ def list_allocation_candidates(req):
|
||||
json_data = jsonutils.dumps(trx_cands)
|
||||
response.body = encodeutils.to_utf8(json_data)
|
||||
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
|
||||
|
@ -161,21 +161,33 @@ def _make_inventory_object(resource_provider, resource_class, **data):
|
||||
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."""
|
||||
response = req.response
|
||||
response.status = 200
|
||||
response.body = encodeutils.to_utf8(jsonutils.dumps(
|
||||
_serialize_inventories(inventories, resource_provider.generation)))
|
||||
output, last_modified = _serialize_inventories(
|
||||
inventories, resource_provider.generation)
|
||||
response.body = encodeutils.to_utf8(jsonutils.dumps(output))
|
||||
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
|
||||
|
||||
|
||||
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."""
|
||||
response = req.response
|
||||
response.status = status
|
||||
response.body = encodeutils.to_utf8(jsonutils.dumps(_serialize_inventory(
|
||||
inventory, generation=resource_provider.generation)))
|
||||
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
|
||||
|
||||
|
||||
@ -195,11 +207,13 @@ def _serialize_inventories(inventories, generation):
|
||||
inventories_by_class = {inventory.resource_class: inventory
|
||||
for inventory in inventories}
|
||||
inventories_dict = {}
|
||||
last_modified = None
|
||||
for resource_class, inventory in inventories_by_class.items():
|
||||
last_modified = util.pick_last_modified(last_modified, inventory)
|
||||
inventories_dict[resource_class] = _serialize_inventory(
|
||||
inventory, generation=None)
|
||||
return {'resource_provider_generation': generation,
|
||||
'inventories': inventories_dict}
|
||||
return ({'resource_provider_generation': generation,
|
||||
'inventories': inventories_dict}, last_modified)
|
||||
|
||||
|
||||
@wsgi_wrapper.PlacementWsgify
|
||||
@ -238,7 +252,7 @@ def create_inventory(req):
|
||||
response = req.response
|
||||
response.location = util.inventory_url(
|
||||
req.environ, resource_provider, resource_class)
|
||||
return _send_inventory(response, resource_provider, inventory,
|
||||
return _send_inventory(req, resource_provider, inventory,
|
||||
status=201)
|
||||
|
||||
|
||||
@ -294,7 +308,7 @@ def get_inventories(req):
|
||||
|
||||
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
|
||||
@ -323,7 +337,7 @@ def get_inventory(req):
|
||||
_('No inventory of class %(class)s for %(rp_uuid)s') %
|
||||
{'class': resource_class, 'rp_uuid': uuid})
|
||||
|
||||
return _send_inventory(req.response, rp, inventory)
|
||||
return _send_inventory(req, rp, inventory)
|
||||
|
||||
|
||||
@wsgi_wrapper.PlacementWsgify
|
||||
@ -383,7 +397,7 @@ def set_inventories(req):
|
||||
'%(rp_uuid)s: %(error)s') % {'rp_uuid': resource_provider.uuid,
|
||||
'error': exc})
|
||||
|
||||
return _send_inventories(req.response, resource_provider, inventories)
|
||||
return _send_inventories(req, resource_provider, inventories)
|
||||
|
||||
|
||||
@wsgi_wrapper.PlacementWsgify
|
||||
@ -468,4 +482,4 @@ def update_inventory(req):
|
||||
'%(rp_uuid)s: %(error)s') % {'rp_uuid': resource_provider.uuid,
|
||||
'error': exc})
|
||||
|
||||
return _send_inventory(req.response, resource_provider, inventory)
|
||||
return _send_inventory(req, resource_provider, inventory)
|
||||
|
@ -15,6 +15,7 @@ import copy
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import timeutils
|
||||
import webob
|
||||
|
||||
from nova.api.openstack.placement import microversion
|
||||
@ -56,12 +57,17 @@ def _serialize_resource_class(environ, rc):
|
||||
return data
|
||||
|
||||
|
||||
def _serialize_resource_classes(environ, rcs):
|
||||
def _serialize_resource_classes(environ, rcs, want_version):
|
||||
output = []
|
||||
last_modified = None
|
||||
get_last_modified = want_version.matches((1, 15))
|
||||
for rc in rcs:
|
||||
if get_last_modified:
|
||||
last_modified = util.pick_last_modified(last_modified, rc)
|
||||
data = _serialize_resource_class(environ, rc)
|
||||
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
|
||||
@ -131,6 +137,7 @@ def get_resource_class(req):
|
||||
"""
|
||||
name = util.wsgi_path_item(req.environ, 'name')
|
||||
context = req.environ['placement.context']
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
# The containing application will catch a not found here.
|
||||
rc = rp_obj.ResourceClass.get_by_name(context, name)
|
||||
|
||||
@ -138,6 +145,13 @@ def get_resource_class(req):
|
||||
_serialize_resource_class(req.environ, rc))
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@ -151,13 +165,17 @@ def list_resource_classes(req):
|
||||
a collection of resource classes.
|
||||
"""
|
||||
context = req.environ['placement.context']
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
rcs = rp_obj.ResourceClassList.get_all(context)
|
||||
|
||||
response = req.response
|
||||
response.body = encodeutils.to_utf8(jsonutils.dumps(
|
||||
_serialize_resource_classes(req.environ, rcs))
|
||||
)
|
||||
output, last_modified = _serialize_resource_classes(
|
||||
req.environ, rcs, want_version)
|
||||
response.body = encodeutils.to_utf8(jsonutils.dumps(output))
|
||||
response.content_type = 'application/json'
|
||||
if want_version.matches((1, 15)):
|
||||
response.last_modified = last_modified
|
||||
response.cache_control = 'no-cache'
|
||||
return response
|
||||
|
||||
|
||||
|
@ -16,6 +16,7 @@ import copy
|
||||
from oslo_db import exception as db_exc
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import timeutils
|
||||
from oslo_utils import uuidutils
|
||||
import webob
|
||||
|
||||
@ -140,10 +141,15 @@ def _serialize_provider(environ, resource_provider, want_version):
|
||||
|
||||
def _serialize_providers(environ, resource_providers, want_version):
|
||||
output = []
|
||||
last_modified = None
|
||||
get_last_modified = want_version.matches((1, 15))
|
||||
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)
|
||||
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
|
||||
@ -219,6 +225,7 @@ def get_resource_provider(req):
|
||||
On success return a 200 with an application/json body representing
|
||||
the resource provider.
|
||||
"""
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
||||
# The containing application will catch a not found here.
|
||||
context = req.environ['placement.context']
|
||||
@ -226,11 +233,15 @@ def get_resource_provider(req):
|
||||
resource_provider = rp_obj.ResourceProvider.get_by_uuid(
|
||||
context, uuid)
|
||||
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
req.response.body = encodeutils.to_utf8(jsonutils.dumps(
|
||||
response = req.response
|
||||
response.body = encodeutils.to_utf8(jsonutils.dumps(
|
||||
_serialize_provider(req.environ, resource_provider, want_version)))
|
||||
req.response.content_type = 'application/json'
|
||||
return req.response
|
||||
response.content_type = 'application/json'
|
||||
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
|
||||
@ -287,9 +298,13 @@ def list_resource_providers(req):
|
||||
{'error': exc})
|
||||
|
||||
response = req.response
|
||||
response.body = encodeutils.to_utf8(jsonutils.dumps(
|
||||
_serialize_providers(req.environ, resource_providers, want_version)))
|
||||
output, last_modified = _serialize_providers(
|
||||
req.environ, resource_providers, want_version)
|
||||
response.body = encodeutils.to_utf8(jsonutils.dumps(output))
|
||||
response.content_type = 'application/json'
|
||||
if want_version.matches((1, 15)):
|
||||
response.last_modified = last_modified
|
||||
response.cache_control = 'no-cache'
|
||||
return response
|
||||
|
||||
|
||||
@ -303,6 +318,7 @@ def update_resource_provider(req):
|
||||
"""
|
||||
uuid = util.wsgi_path_item(req.environ, 'uuid')
|
||||
context = req.environ['placement.context']
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
|
||||
# The containing application will catch a not found here.
|
||||
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') %
|
||||
{'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)))
|
||||
req.response.status = 200
|
||||
req.response.content_type = 'application/json'
|
||||
return req.response
|
||||
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
|
||||
|
@ -13,6 +13,7 @@
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import timeutils
|
||||
|
||||
|
||||
from nova.api.openstack.placement import microversion
|
||||
@ -21,6 +22,7 @@ from nova.api.openstack.placement import wsgi_wrapper
|
||||
|
||||
@wsgi_wrapper.PlacementWsgify
|
||||
def home(req):
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
min_version = microversion.min_version_string()
|
||||
max_version = microversion.max_version_string()
|
||||
# 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]})
|
||||
req.response.body = encodeutils.to_utf8(version_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
|
||||
|
@ -16,6 +16,7 @@ import copy
|
||||
import jsonschema
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import timeutils
|
||||
import webob
|
||||
|
||||
from nova.api.openstack.placement import microversion
|
||||
@ -85,14 +86,26 @@ def _normalize_traits_qs_param(qs):
|
||||
return filters
|
||||
|
||||
|
||||
def _serialize_traits(traits):
|
||||
return {'traits': [trait.name for trait in traits]}
|
||||
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']
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
name = util.wsgi_path_item(req.environ, 'name')
|
||||
|
||||
try:
|
||||
@ -110,10 +123,16 @@ def put_trait(req):
|
||||
trait.create()
|
||||
req.response.status = 201
|
||||
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.content_type = None
|
||||
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
|
||||
|
||||
|
||||
@ -121,16 +140,20 @@ def put_trait(req):
|
||||
@microversion.version_handler('1.6')
|
||||
def get_trait(req):
|
||||
context = req.environ['placement.context']
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
name = util.wsgi_path_item(req.environ, 'name')
|
||||
|
||||
try:
|
||||
rp_obj.Trait.get_by_name(context, name)
|
||||
trait = rp_obj.Trait.get_by_name(context, name)
|
||||
except exception.TraitNotFound as ex:
|
||||
raise webob.exc.HTTPNotFound(
|
||||
explanation=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
|
||||
|
||||
|
||||
@ -163,6 +186,7 @@ def delete_trait(req):
|
||||
@util.check_accept('application/json')
|
||||
def list_traits(req):
|
||||
context = req.environ['placement.context']
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
filters = {}
|
||||
|
||||
try:
|
||||
@ -185,8 +209,11 @@ def list_traits(req):
|
||||
|
||||
traits = rp_obj.TraitList.get_all(context, filters)
|
||||
req.response.status = 200
|
||||
req.response.body = encodeutils.to_utf8(
|
||||
jsonutils.dumps(_serialize_traits(traits)))
|
||||
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
|
||||
|
||||
@ -196,6 +223,7 @@ def list_traits(req):
|
||||
@util.check_accept('application/json')
|
||||
def list_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')
|
||||
|
||||
# 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})
|
||||
|
||||
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
|
||||
|
||||
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'
|
||||
@ -225,6 +257,7 @@ def list_traits_for_resource_provider(req):
|
||||
@util.require_content('application/json')
|
||||
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, SET_TRAITS_FOR_RP_SCHEMA)
|
||||
rp_gen = data['resource_provider_generation']
|
||||
@ -248,9 +281,12 @@ def update_traits_for_resource_provider(req):
|
||||
|
||||
resource_provider.set_traits(trait_objs)
|
||||
|
||||
response_body = _serialize_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'
|
||||
|
@ -13,6 +13,7 @@
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import timeutils
|
||||
import webob
|
||||
|
||||
from nova.api.openstack.placement import microversion
|
||||
@ -64,6 +65,7 @@ def list_usages(req):
|
||||
"""
|
||||
context = req.environ['placement.context']
|
||||
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
|
||||
# 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(
|
||||
_serialize_usages(resource_provider, usage)))
|
||||
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
|
||||
|
||||
|
||||
@ -99,6 +109,7 @@ def get_total_usages(req):
|
||||
Return 404 Not Found if the wanted microversion does not match.
|
||||
"""
|
||||
context = req.environ['placement.context']
|
||||
want_version = req.environ[microversion.MICROVERSION_ENVIRON]
|
||||
|
||||
schema = GET_USAGES_SCHEMA_1_9
|
||||
|
||||
@ -115,4 +126,12 @@ def get_total_usages(req):
|
||||
for resource in usages}}
|
||||
response.body = encodeutils.to_utf8(jsonutils.dumps(usages_dict))
|
||||
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
|
||||
|
@ -54,9 +54,9 @@ VERSIONS = [
|
||||
# as GET. The 'allocation_requests' format in GET
|
||||
# /allocation_candidates is updated to be the same as well.
|
||||
'1.13', # Adds POST /allocations to set allocations for multiple consumers
|
||||
# as GET
|
||||
'1.14', # Adds parent and root provider UUID on resource provider
|
||||
# representation and 'in_tree' filter on GET /resource_providers
|
||||
'1.15', # Include last-modified and cache-control headers
|
||||
]
|
||||
|
||||
|
||||
|
@ -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``
|
||||
parameter will cause all resource providers within the "provider tree" of the
|
||||
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.
|
||||
|
@ -17,6 +17,7 @@ import re
|
||||
import jsonschema
|
||||
from oslo_middleware import request_id
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import timeutils
|
||||
from oslo_utils import uuidutils
|
||||
import webob
|
||||
|
||||
@ -124,6 +125,25 @@ def json_error_formatter(body, status, title, environ):
|
||||
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):
|
||||
"""Decorator to require a content type in a handler."""
|
||||
def decorator(f):
|
||||
|
@ -65,12 +65,19 @@ tests:
|
||||
status: 200
|
||||
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:
|
||||
$.aggregates[0]: *agg_1
|
||||
$.aggregates[1]: *agg_2
|
||||
|
||||
- name: get those aggregates
|
||||
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:
|
||||
$.aggregates.`len`: 2
|
||||
|
||||
@ -117,3 +124,26 @@ tests:
|
||||
- JSON does not validate
|
||||
response_json_paths:
|
||||
$.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
|
||||
|
@ -83,6 +83,11 @@ tests:
|
||||
# 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].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
|
||||
# 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
|
||||
# resource requests, but is listed twice
|
||||
$.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$/
|
||||
|
@ -373,6 +373,11 @@ tests:
|
||||
DISK_GB: 2
|
||||
VCPU: 8
|
||||
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
|
||||
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
|
||||
response_json_paths:
|
||||
$.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
|
||||
|
@ -159,3 +159,20 @@ tests:
|
||||
$.errors[0].title: Not Found
|
||||
response_strings:
|
||||
- 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
|
||||
|
@ -166,6 +166,13 @@ tests:
|
||||
- name: get that inventory
|
||||
GET: $LOCATION
|
||||
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:
|
||||
$.resource_provider_generation: 1
|
||||
$.total: 2048
|
||||
@ -175,6 +182,15 @@ tests:
|
||||
$.step_size: 10
|
||||
$.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
|
||||
PUT: $LAST_URL
|
||||
request_headers:
|
||||
@ -310,6 +326,13 @@ tests:
|
||||
|
||||
- name: get list of 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:
|
||||
$.resource_provider_generation: 2
|
||||
$.inventories.DISK_GB.total: 2048
|
||||
@ -407,6 +430,8 @@ tests:
|
||||
PUT: $LOCATION/inventories
|
||||
request_headers:
|
||||
content-type: application/json
|
||||
# set microversion to 1.15 to get timestamp headers
|
||||
openstack-api-version: placement 1.15
|
||||
data:
|
||||
resource_provider_generation: 0
|
||||
inventories:
|
||||
@ -415,6 +440,10 @@ tests:
|
||||
DISK_GB:
|
||||
total: 1024
|
||||
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:
|
||||
$.resource_provider_generation: 1
|
||||
$.inventories.IPV4_ADDRESS.total: 253
|
||||
|
@ -39,13 +39,13 @@ tests:
|
||||
response_json_paths:
|
||||
$.errors[0].title: Not Acceptable
|
||||
|
||||
- name: latest microversion is 1.14
|
||||
- name: latest microversion is 1.15
|
||||
GET: /
|
||||
request_headers:
|
||||
openstack-api-version: placement latest
|
||||
response_headers:
|
||||
vary: /OpenStack-API-Version/
|
||||
openstack-api-version: placement 1.14
|
||||
openstack-api-version: placement 1.15
|
||||
|
||||
- name: other accept header bad version
|
||||
GET: /
|
||||
|
@ -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
|
@ -12,8 +12,15 @@ tests:
|
||||
|
||||
- name: what is at resource providers
|
||||
GET: /resource_providers
|
||||
request_headers:
|
||||
# microversion 1.15 for cache headers
|
||||
openstack-api-version: placement 1.15
|
||||
response_json_paths:
|
||||
$.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
|
||||
GET: /resource_providers
|
||||
@ -78,6 +85,12 @@ tests:
|
||||
GET: /resource_providers/$ENVIRON['RP_UUID']
|
||||
request_headers:
|
||||
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:
|
||||
$.uuid: $ENVIRON['RP_UUID']
|
||||
$.name: $ENVIRON['RP_NAME']
|
||||
@ -104,6 +117,8 @@ tests:
|
||||
|
||||
- name: list one resource providers
|
||||
GET: /resource_providers
|
||||
request_headers:
|
||||
openstack-api-version: placement 1.15
|
||||
response_json_paths:
|
||||
$.resource_providers.`len`: 1
|
||||
$.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 = "inventories"].href: /resource_providers/$ENVIRON['RP_UUID']/inventories
|
||||
$.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
|
||||
GET: /resource_providers?name=flubblebubble
|
||||
@ -181,11 +200,15 @@ tests:
|
||||
PUT: /resource_providers/$RESPONSE['$.resource_providers[0].uuid']
|
||||
request_headers:
|
||||
content-type: application/json
|
||||
openstack-api-version: placement 1.15
|
||||
data:
|
||||
name: new name
|
||||
status: 200
|
||||
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_forbidden_headers:
|
||||
- location
|
||||
response_json_paths:
|
||||
@ -491,3 +514,11 @@ tests:
|
||||
- "Failed validating 'maxLength'"
|
||||
response_json_paths:
|
||||
$.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
|
||||
|
@ -5,7 +5,8 @@ fixtures:
|
||||
defaults:
|
||||
request_headers:
|
||||
x-auth-token: admin
|
||||
openstack-api-version: placement latest
|
||||
# traits introduced in 1.6
|
||||
openstack-api-version: placement 1.6
|
||||
|
||||
tests:
|
||||
|
||||
@ -34,6 +35,9 @@ tests:
|
||||
location: //traits/CUSTOM_TRAIT_1/
|
||||
response_forbidden_headers:
|
||||
- content-type
|
||||
# PUT in 1.6 version should not have cache headers
|
||||
- cache-control
|
||||
- last-modified
|
||||
|
||||
- name: create a trait which existed
|
||||
PUT: /traits/CUSTOM_TRAIT_1
|
||||
@ -48,6 +52,9 @@ tests:
|
||||
status: 204
|
||||
response_forbidden_headers:
|
||||
- content-type
|
||||
# In early versions cache headers should not be present
|
||||
- cache-control
|
||||
- last-modified
|
||||
|
||||
- name: get a non-existed trait
|
||||
GET: /traits/NON_EXISTED
|
||||
@ -56,6 +63,11 @@ tests:
|
||||
- name: delete a trait
|
||||
DELETE: /traits/CUSTOM_TRAIT_1
|
||||
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
|
||||
DELETE: /traits/CUSTOM_NON_EXSITED
|
||||
@ -133,6 +145,61 @@ tests:
|
||||
response_strings:
|
||||
- "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
|
||||
POST: /resource_providers
|
||||
request_headers:
|
||||
@ -152,6 +219,10 @@ tests:
|
||||
response_json_paths:
|
||||
$.resource_provider_generation: 0
|
||||
$.traits.`len`: 0
|
||||
response_forbidden_headers:
|
||||
# In 1.6 no cache headers
|
||||
- cache-control
|
||||
- last-modified
|
||||
|
||||
- name: set traits for resource provider
|
||||
PUT: /resource_providers/$ENVIRON['RP_UUID']/traits
|
||||
@ -169,6 +240,10 @@ tests:
|
||||
response_strings:
|
||||
- CUSTOM_TRAIT_1
|
||||
- CUSTOM_TRAIT_2
|
||||
response_forbidden_headers:
|
||||
# In 1.6 no cache headers
|
||||
- cache-control
|
||||
- last-modified
|
||||
|
||||
- name: get associated traits
|
||||
GET: /traits?associated=true
|
||||
@ -272,3 +347,58 @@ tests:
|
||||
status: 404
|
||||
response_strings:
|
||||
- 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$/
|
||||
|
@ -38,6 +38,21 @@ tests:
|
||||
response_json_paths:
|
||||
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
|
||||
GET: /usages?project_id=$ENVIRON['PROJECT_ID']
|
||||
request_headers:
|
||||
|
@ -65,6 +65,10 @@ tests:
|
||||
request_headers:
|
||||
openstack-api-version: placement 1.9
|
||||
status: 200
|
||||
# In pre 1.15 microversions cache headers not present
|
||||
response_forbidden_headers:
|
||||
- last-modified
|
||||
- cache-control
|
||||
response_json_paths:
|
||||
$.usages.DISK_GB: 20
|
||||
$.usages.VCPU: 1
|
||||
@ -87,3 +91,12 @@ tests:
|
||||
$.project_id: $ENVIRON['PROJECT_ID']
|
||||
$.user_id: $ENVIRON['USER_ID']
|
||||
$.`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$/
|
||||
|
@ -19,6 +19,7 @@ import webob
|
||||
|
||||
from nova.api.openstack.placement import handler
|
||||
from nova.api.openstack.placement.handlers import root
|
||||
from nova.api.openstack.placement import microversion
|
||||
from nova import test
|
||||
from nova.tests import uuidsentinel
|
||||
|
||||
@ -35,6 +36,9 @@ def _environ(path='/moo', method='GET'):
|
||||
'SERVER_NAME': 'example.com',
|
||||
'SERVER_PORT': '80',
|
||||
'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),
|
||||
}
|
||||
|
||||
|
||||
|
@ -13,8 +13,12 @@
|
||||
"""Unit tests for the utility functions used by the placement API."""
|
||||
|
||||
|
||||
import datetime
|
||||
|
||||
import fixtures
|
||||
import mock
|
||||
from oslo_middleware import request_id
|
||||
from oslo_utils import timeutils
|
||||
import webob
|
||||
|
||||
import six.moves.urllib.parse as urlparse
|
||||
@ -594,3 +598,83 @@ class TestParseQsResourcesAndTraits(test.NoDBTestCase):
|
||||
'&required2=CUSTOM_SWITCH_BIG,CUSTOM_PHYSNET_PROD'
|
||||
'&resources3=CUSTOM_MAGIC:123')
|
||||
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)
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user