[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:
Chris Dent 2017-11-20 18:08:06 +00:00
parent 3cec0cb584
commit 83030804cc
26 changed files with 767 additions and 54 deletions

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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
]

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``
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.

View File

@ -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):

View File

@ -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

View File

@ -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$/

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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: /

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
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

View File

@ -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$/

View File

@ -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:

View File

@ -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$/

View File

@ -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),
}

View File

@ -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)

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.