diff --git a/nova/api/openstack/placement/handlers/aggregate.py b/nova/api/openstack/placement/handlers/aggregate.py index 3fec87ddbed0..382f190477f5 100644 --- a/nova/api/openstack/placement/handlers/aggregate.py +++ b/nova/api/openstack/placement/handlers/aggregate.py @@ -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) diff --git a/nova/api/openstack/placement/handlers/allocation.py b/nova/api/openstack/placement/handlers/allocation.py index c06d4db05dba..bafbbdf063cf 100644 --- a/nova/api/openstack/placement/handlers/allocation.py +++ b/nova/api/openstack/placement/handlers/allocation.py @@ -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, diff --git a/nova/api/openstack/placement/handlers/allocation_candidate.py b/nova/api/openstack/placement/handlers/allocation_candidate.py index 94b6b79e0bf4..56635f54cd84 100644 --- a/nova/api/openstack/placement/handlers/allocation_candidate.py +++ b/nova/api/openstack/placement/handlers/allocation_candidate.py @@ -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 diff --git a/nova/api/openstack/placement/handlers/inventory.py b/nova/api/openstack/placement/handlers/inventory.py index 3d9622053b7a..15d6b3c2097d 100644 --- a/nova/api/openstack/placement/handlers/inventory.py +++ b/nova/api/openstack/placement/handlers/inventory.py @@ -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) diff --git a/nova/api/openstack/placement/handlers/resource_class.py b/nova/api/openstack/placement/handlers/resource_class.py index 99a0dc0536c8..2cbeb7d91ac4 100644 --- a/nova/api/openstack/placement/handlers/resource_class.py +++ b/nova/api/openstack/placement/handlers/resource_class.py @@ -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 diff --git a/nova/api/openstack/placement/handlers/resource_provider.py b/nova/api/openstack/placement/handlers/resource_provider.py index 81ecce08638a..94eae5ca7ac0 100644 --- a/nova/api/openstack/placement/handlers/resource_provider.py +++ b/nova/api/openstack/placement/handlers/resource_provider.py @@ -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 diff --git a/nova/api/openstack/placement/handlers/root.py b/nova/api/openstack/placement/handlers/root.py index 9b5b1bb948dc..2f9c6861f2bc 100644 --- a/nova/api/openstack/placement/handlers/root.py +++ b/nova/api/openstack/placement/handlers/root.py @@ -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 diff --git a/nova/api/openstack/placement/handlers/trait.py b/nova/api/openstack/placement/handlers/trait.py index 50fc6b7dafd8..e5c6a38cbadc 100644 --- a/nova/api/openstack/placement/handlers/trait.py +++ b/nova/api/openstack/placement/handlers/trait.py @@ -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' diff --git a/nova/api/openstack/placement/handlers/usage.py b/nova/api/openstack/placement/handlers/usage.py index 6349c5352bcd..beec996ef18e 100644 --- a/nova/api/openstack/placement/handlers/usage.py +++ b/nova/api/openstack/placement/handlers/usage.py @@ -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 diff --git a/nova/api/openstack/placement/microversion.py b/nova/api/openstack/placement/microversion.py index d4a6a20cfbb4..030c7a61b8b3 100644 --- a/nova/api/openstack/placement/microversion.py +++ b/nova/api/openstack/placement/microversion.py @@ -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 ] diff --git a/nova/api/openstack/placement/rest_api_version_history.rst b/nova/api/openstack/placement/rest_api_version_history.rst index 5dc510a18193..380ad36541b5 100644 --- a/nova/api/openstack/placement/rest_api_version_history.rst +++ b/nova/api/openstack/placement/rest_api_version_history.rst @@ -197,3 +197,13 @@ A new ``in_tree=`` 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 ```` 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. diff --git a/nova/api/openstack/placement/util.py b/nova/api/openstack/placement/util.py index efb16dcbc71a..552ba2200607 100644 --- a/nova/api/openstack/placement/util.py +++ b/nova/api/openstack/placement/util.py @@ -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): diff --git a/nova/tests/functional/api/openstack/placement/gabbits/aggregate.yaml b/nova/tests/functional/api/openstack/placement/gabbits/aggregate.yaml index 377a3b62869f..c9c4de00b0dc 100644 --- a/nova/tests/functional/api/openstack/placement/gabbits/aggregate.yaml +++ b/nova/tests/functional/api/openstack/placement/gabbits/aggregate.yaml @@ -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 diff --git a/nova/tests/functional/api/openstack/placement/gabbits/allocation-candidates.yaml b/nova/tests/functional/api/openstack/placement/gabbits/allocation-candidates.yaml index 52738b69ccb5..a32115e6883c 100644 --- a/nova/tests/functional/api/openstack/placement/gabbits/allocation-candidates.yaml +++ b/nova/tests/functional/api/openstack/placement/gabbits/allocation-candidates.yaml @@ -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$/ diff --git a/nova/tests/functional/api/openstack/placement/gabbits/allocations.yaml b/nova/tests/functional/api/openstack/placement/gabbits/allocations.yaml index b732d6be3045..ee3408eea360 100644 --- a/nova/tests/functional/api/openstack/placement/gabbits/allocations.yaml +++ b/nova/tests/functional/api/openstack/placement/gabbits/allocations.yaml @@ -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 diff --git a/nova/tests/functional/api/openstack/placement/gabbits/basic-http.yaml b/nova/tests/functional/api/openstack/placement/gabbits/basic-http.yaml index 1c0dd6fcc5f1..244369bfdc3d 100644 --- a/nova/tests/functional/api/openstack/placement/gabbits/basic-http.yaml +++ b/nova/tests/functional/api/openstack/placement/gabbits/basic-http.yaml @@ -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 diff --git a/nova/tests/functional/api/openstack/placement/gabbits/inventory.yaml b/nova/tests/functional/api/openstack/placement/gabbits/inventory.yaml index 0e59eeddf85a..5ab3a8390113 100644 --- a/nova/tests/functional/api/openstack/placement/gabbits/inventory.yaml +++ b/nova/tests/functional/api/openstack/placement/gabbits/inventory.yaml @@ -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 diff --git a/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml b/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml index c2e8b6deb438..93110b615527 100644 --- a/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml +++ b/nova/tests/functional/api/openstack/placement/gabbits/microversion.yaml @@ -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: / diff --git a/nova/tests/functional/api/openstack/placement/gabbits/resource-classes-last-modified.yaml b/nova/tests/functional/api/openstack/placement/gabbits/resource-classes-last-modified.yaml new file mode 100644 index 000000000000..e60244777770 --- /dev/null +++ b/nova/tests/functional/api/openstack/placement/gabbits/resource-classes-last-modified.yaml @@ -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 diff --git a/nova/tests/functional/api/openstack/placement/gabbits/resource-provider.yaml b/nova/tests/functional/api/openstack/placement/gabbits/resource-provider.yaml index c0c7f53ce482..caf9c2f8763d 100644 --- a/nova/tests/functional/api/openstack/placement/gabbits/resource-provider.yaml +++ b/nova/tests/functional/api/openstack/placement/gabbits/resource-provider.yaml @@ -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 diff --git a/nova/tests/functional/api/openstack/placement/gabbits/traits.yaml b/nova/tests/functional/api/openstack/placement/gabbits/traits.yaml index 6300db6d3d19..0d5f911707a9 100644 --- a/nova/tests/functional/api/openstack/placement/gabbits/traits.yaml +++ b/nova/tests/functional/api/openstack/placement/gabbits/traits.yaml @@ -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$/ diff --git a/nova/tests/functional/api/openstack/placement/gabbits/usage.yaml b/nova/tests/functional/api/openstack/placement/gabbits/usage.yaml index b34c0255727e..8fa8995652d9 100644 --- a/nova/tests/functional/api/openstack/placement/gabbits/usage.yaml +++ b/nova/tests/functional/api/openstack/placement/gabbits/usage.yaml @@ -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: diff --git a/nova/tests/functional/api/openstack/placement/gabbits/with-allocations.yaml b/nova/tests/functional/api/openstack/placement/gabbits/with-allocations.yaml index eaa498ebe765..d19fb453e033 100644 --- a/nova/tests/functional/api/openstack/placement/gabbits/with-allocations.yaml +++ b/nova/tests/functional/api/openstack/placement/gabbits/with-allocations.yaml @@ -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$/ diff --git a/nova/tests/unit/api/openstack/placement/test_handler.py b/nova/tests/unit/api/openstack/placement/test_handler.py index 3e75ccc77287..a83f89149ded 100644 --- a/nova/tests/unit/api/openstack/placement/test_handler.py +++ b/nova/tests/unit/api/openstack/placement/test_handler.py @@ -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), } diff --git a/nova/tests/unit/api/openstack/placement/test_util.py b/nova/tests/unit/api/openstack/placement/test_util.py index aede250a7237..c7b7cb4e5948 100644 --- a/nova/tests/unit/api/openstack/placement/test_util.py +++ b/nova/tests/unit/api/openstack/placement/test_util.py @@ -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) diff --git a/releasenotes/notes/placement-last-modified-cf43aece4c54fc97.yaml b/releasenotes/notes/placement-last-modified-cf43aece4c54fc97.yaml new file mode 100644 index 000000000000..f97780a56498 --- /dev/null +++ b/releasenotes/notes/placement-last-modified-cf43aece4c54fc97.yaml @@ -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.