Merge "Refactor API code for checking microversions"
This commit is contained in:
commit
79f59de3f2
@ -119,73 +119,44 @@ def get_nodes_controller_reserved_names():
|
||||
return _NODES_CONTROLLER_RESERVED_WORDS
|
||||
|
||||
|
||||
def _hide_fields_in_newer_versions_part_one(obj):
|
||||
if pecan.request.version.minor < versions.MINOR_3_DRIVER_INTERNAL_INFO:
|
||||
obj.driver_internal_info = wsme.Unset
|
||||
|
||||
if not api_utils.allow_node_logical_names():
|
||||
obj.name = wsme.Unset
|
||||
|
||||
# if requested version is < 1.6, hide inspection_*_at fields
|
||||
if pecan.request.version.minor < versions.MINOR_6_INSPECT_STATE:
|
||||
obj.inspection_finished_at = wsme.Unset
|
||||
obj.inspection_started_at = wsme.Unset
|
||||
|
||||
if pecan.request.version.minor < versions.MINOR_7_NODE_CLEAN:
|
||||
obj.clean_step = wsme.Unset
|
||||
|
||||
if pecan.request.version.minor < versions.MINOR_12_RAID_CONFIG:
|
||||
obj.raid_config = wsme.Unset
|
||||
obj.target_raid_config = wsme.Unset
|
||||
|
||||
if pecan.request.version.minor < versions.MINOR_20_NETWORK_INTERFACE:
|
||||
obj.network_interface = wsme.Unset
|
||||
|
||||
if pecan.request.version.minor < versions.MINOR_42_FAULT:
|
||||
obj.fault = wsme.Unset
|
||||
|
||||
if pecan.request.version.minor < versions.MINOR_44_NODE_DEPLOY_STEP:
|
||||
obj.deploy_step = wsme.Unset
|
||||
|
||||
|
||||
def _hide_fields_in_newer_versions_part_two(obj):
|
||||
if not api_utils.allow_resource_class():
|
||||
obj.resource_class = wsme.Unset
|
||||
|
||||
if not api_utils.allow_dynamic_interfaces():
|
||||
for field in api_utils.V31_FIELDS:
|
||||
setattr(obj, field, wsme.Unset)
|
||||
|
||||
if not api_utils.allow_storage_interface():
|
||||
obj.storage_interface = wsme.Unset
|
||||
|
||||
if not api_utils.allow_traits():
|
||||
obj.traits = wsme.Unset
|
||||
|
||||
if not api_utils.allow_rescue_interface():
|
||||
obj.rescue_interface = wsme.Unset
|
||||
|
||||
if not api_utils.allow_bios_interface():
|
||||
obj.bios_interface = wsme.Unset
|
||||
|
||||
if not api_utils.allow_conductor_group():
|
||||
obj.conductor_group = wsme.Unset
|
||||
|
||||
if not api_utils.allow_automated_clean():
|
||||
obj.automated_clean = wsme.Unset
|
||||
|
||||
|
||||
def hide_fields_in_newer_versions(obj):
|
||||
"""This method hides fields that were added in newer API versions.
|
||||
|
||||
Certain node fields were introduced at certain API versions.
|
||||
These fields are only made available when the request's API version
|
||||
matches or exceeds the versions when these fields were introduced.
|
||||
|
||||
This is broken into two methods for cyclomatic complexity's sake.
|
||||
"""
|
||||
_hide_fields_in_newer_versions_part_one(obj)
|
||||
_hide_fields_in_newer_versions_part_two(obj)
|
||||
for field in api_utils.disallowed_fields():
|
||||
setattr(obj, field, wsme.Unset)
|
||||
|
||||
|
||||
def reject_fields_in_newer_versions(obj):
|
||||
"""When creating an object, reject fields that appear in newer versions."""
|
||||
for field in api_utils.disallowed_fields():
|
||||
if field == 'conductor_group':
|
||||
# NOTE(jroll) this is special-cased to "" and not Unset,
|
||||
# because it is used in hash ring calculations
|
||||
empty_value = ''
|
||||
elif field == 'name' and obj.name is None:
|
||||
# NOTE(dtantsur): for some reason we allow specifying name=None
|
||||
# explicitly even in old API versions..
|
||||
continue
|
||||
else:
|
||||
empty_value = wtypes.Unset
|
||||
|
||||
if getattr(obj, field, empty_value) != empty_value:
|
||||
LOG.debug('Field %(field)s is not acceptable in version %(ver)s',
|
||||
{'field': field, 'ver': pecan.request.version})
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
|
||||
def reject_patch_in_newer_versions(patch):
|
||||
for field in api_utils.disallowed_fields():
|
||||
value = api_utils.get_patch_values(patch, '/%s' % field)
|
||||
if value:
|
||||
LOG.debug('Field %(field)s is not acceptable in version %(ver)s',
|
||||
{'field': field, 'ver': pecan.request.version})
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
|
||||
def update_state_in_older_versions(obj):
|
||||
@ -1904,47 +1875,13 @@ class NodesController(rest.RestController):
|
||||
if self.from_chassis:
|
||||
raise exception.OperationNotPermitted()
|
||||
|
||||
if (not api_utils.allow_resource_class()
|
||||
and node.resource_class is not wtypes.Unset):
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
n_interface = node.network_interface
|
||||
if (not api_utils.allow_network_interface()
|
||||
and n_interface is not wtypes.Unset):
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
if not api_utils.allow_dynamic_interfaces():
|
||||
for field in api_utils.V31_FIELDS:
|
||||
if getattr(node, field) is not wsme.Unset:
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
if (not api_utils.allow_storage_interface()
|
||||
and node.storage_interface is not wtypes.Unset):
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
if (not api_utils.allow_bios_interface() and
|
||||
node.bios_interface is not wtypes.Unset):
|
||||
raise exception.NotAcceptable()
|
||||
reject_fields_in_newer_versions(node)
|
||||
|
||||
if node.traits is not wtypes.Unset:
|
||||
msg = _("Cannot specify node traits on node creation. Traits must "
|
||||
"be set via the node traits API.")
|
||||
raise exception.Invalid(msg)
|
||||
|
||||
if (not api_utils.allow_rescue_interface()
|
||||
and node.rescue_interface is not wtypes.Unset):
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
# NOTE(jroll) this is special-cased to "" and not Unset,
|
||||
# because it is used in hash ring calculations
|
||||
if (not api_utils.allow_conductor_group()
|
||||
and node.conductor_group != ""):
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
if (not api_utils.allow_automated_clean()
|
||||
and node.automated_clean is not wtypes.Unset):
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
# NOTE(deva): get_topic_for checks if node.driver is in the hash ring
|
||||
# and raises NoValidHost if it is not.
|
||||
# We need to ensure that node has a UUID before it can
|
||||
@ -1983,27 +1920,11 @@ class NodesController(rest.RestController):
|
||||
chassis_uuid=api_node.chassis_uuid)
|
||||
return api_node
|
||||
|
||||
# NOTE (yolanda): isolate validation to avoid patch too complex pep error
|
||||
def _validate_patch(self, patch, reset_interfaces):
|
||||
if self.from_chassis:
|
||||
raise exception.OperationNotPermitted()
|
||||
|
||||
resource_class = api_utils.get_patch_values(patch, '/resource_class')
|
||||
if resource_class and not api_utils.allow_resource_class():
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
n_interfaces = api_utils.get_patch_values(patch, '/network_interface')
|
||||
if n_interfaces and not api_utils.allow_network_interface():
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
if not api_utils.allow_dynamic_interfaces():
|
||||
for field in api_utils.V31_FIELDS:
|
||||
if api_utils.get_patch_values(patch, '/%s' % field):
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
s_interface = api_utils.get_patch_values(patch, '/storage_interface')
|
||||
if s_interface and not api_utils.allow_storage_interface():
|
||||
raise exception.NotAcceptable()
|
||||
reject_patch_in_newer_versions(patch)
|
||||
|
||||
traits = api_utils.get_patch_values(patch, '/traits')
|
||||
if traits:
|
||||
@ -2011,28 +1932,12 @@ class NodesController(rest.RestController):
|
||||
"should be updated via the node traits API.")
|
||||
raise exception.Invalid(msg)
|
||||
|
||||
r_interface = api_utils.get_patch_values(patch, '/rescue_interface')
|
||||
if r_interface and not api_utils.allow_rescue_interface():
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
b_interface = api_utils.get_patch_values(patch, '/bios_interface')
|
||||
if b_interface and not api_utils.allow_bios_interface():
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
driver = api_utils.get_patch_values(patch, '/driver')
|
||||
if reset_interfaces and not driver:
|
||||
msg = _("The reset_interfaces parameter can only be used when "
|
||||
"changing the node's driver.")
|
||||
raise exception.Invalid(msg)
|
||||
|
||||
conductor_group = api_utils.get_patch_values(patch, '/conductor_group')
|
||||
if conductor_group and not api_utils.allow_conductor_group():
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
automated_clean = api_utils.get_patch_values(patch, '/automated_clean')
|
||||
if automated_clean and not api_utils.allow_automated_clean():
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
@METRICS.timer('NodesController.patch')
|
||||
@wsme.validate(types.uuid, types.boolean, [NodePatchType])
|
||||
@expose.expose(Node, types.uuid_or_name, types.boolean,
|
||||
|
@ -358,6 +358,42 @@ def check_allow_specify_fields(fields):
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
|
||||
VERSIONED_FIELDS = {
|
||||
'driver_internal_info': versions.MINOR_3_DRIVER_INTERNAL_INFO,
|
||||
'name': versions.MINOR_5_NODE_NAME,
|
||||
'inspection_finished_at': versions.MINOR_6_INSPECT_STATE,
|
||||
'inspection_started_at': versions.MINOR_6_INSPECT_STATE,
|
||||
'clean_step': versions.MINOR_7_NODE_CLEAN,
|
||||
'raid_config': versions.MINOR_12_RAID_CONFIG,
|
||||
'target_raid_config': versions.MINOR_12_RAID_CONFIG,
|
||||
'network_interface': versions.MINOR_20_NETWORK_INTERFACE,
|
||||
'resource_class': versions.MINOR_21_RESOURCE_CLASS,
|
||||
'storage_interface': versions.MINOR_33_STORAGE_INTERFACE,
|
||||
'traits': versions.MINOR_37_NODE_TRAITS,
|
||||
'rescue_interface': versions.MINOR_38_RESCUE_INTERFACE,
|
||||
'bios_interface': versions.MINOR_40_BIOS_INTERFACE,
|
||||
'fault': versions.MINOR_42_FAULT,
|
||||
'deploy_step': versions.MINOR_44_NODE_DEPLOY_STEP,
|
||||
'conductor_group': versions.MINOR_46_NODE_CONDUCTOR_GROUP,
|
||||
'automated_clean': versions.MINOR_47_NODE_AUTOMATED_CLEAN,
|
||||
}
|
||||
|
||||
for field in V31_FIELDS:
|
||||
VERSIONED_FIELDS[field] = versions.MINOR_31_DYNAMIC_INTERFACES
|
||||
|
||||
|
||||
def allow_field(field):
|
||||
"""Check if a field is allowed in the current version."""
|
||||
return pecan.request.version.minor >= VERSIONED_FIELDS[field]
|
||||
|
||||
|
||||
def disallowed_fields():
|
||||
"""Generator of fields not allowed in the current request."""
|
||||
for field in VERSIONED_FIELDS:
|
||||
if not allow_field(field):
|
||||
yield field
|
||||
|
||||
|
||||
def check_allowed_fields(fields):
|
||||
"""Check if fetching a particular field is allowed.
|
||||
|
||||
@ -366,25 +402,9 @@ def check_allowed_fields(fields):
|
||||
"""
|
||||
if fields is None:
|
||||
return
|
||||
if 'bios_interface' in fields and not allow_bios_interface():
|
||||
raise exception.NotAcceptable()
|
||||
if 'network_interface' in fields and not allow_network_interface():
|
||||
raise exception.NotAcceptable()
|
||||
if 'resource_class' in fields and not allow_resource_class():
|
||||
raise exception.NotAcceptable()
|
||||
if not allow_dynamic_interfaces():
|
||||
if set(V31_FIELDS).intersection(set(fields)):
|
||||
for field in disallowed_fields():
|
||||
if field in fields:
|
||||
raise exception.NotAcceptable()
|
||||
if 'storage_interface' in fields and not allow_storage_interface():
|
||||
raise exception.NotAcceptable()
|
||||
if 'traits' in fields and not allow_traits():
|
||||
raise exception.NotAcceptable()
|
||||
if 'rescue_interface' in fields and not allow_rescue_interface():
|
||||
raise exception.NotAcceptable()
|
||||
if 'conductor_group' in fields and not allow_conductor_group():
|
||||
raise exception.NotAcceptable()
|
||||
if 'automated_clean' in fields and not allow_automated_clean():
|
||||
raise exception.NotAcceptable()
|
||||
|
||||
|
||||
def check_allowed_portgroup_fields(fields):
|
||||
@ -585,24 +605,6 @@ def allow_port_advanced_net_fields():
|
||||
>= versions.MINOR_19_PORT_ADVANCED_NET_FIELDS)
|
||||
|
||||
|
||||
def allow_network_interface():
|
||||
"""Check if we should support network_interface node field.
|
||||
|
||||
Version 1.20 of the API added support for network interfaces.
|
||||
"""
|
||||
return (pecan.request.version.minor
|
||||
>= versions.MINOR_20_NETWORK_INTERFACE)
|
||||
|
||||
|
||||
def allow_resource_class():
|
||||
"""Check if we should support resource_class node field.
|
||||
|
||||
Version 1.21 of the API added support for resource_class.
|
||||
"""
|
||||
return (pecan.request.version.minor
|
||||
>= versions.MINOR_21_RESOURCE_CLASS)
|
||||
|
||||
|
||||
def allow_ramdisk_endpoints():
|
||||
"""Check if heartbeat and lookup endpoints are allowed.
|
||||
|
||||
@ -689,7 +691,7 @@ def allow_volume():
|
||||
|
||||
|
||||
def allow_storage_interface():
|
||||
"""Check if we should support storage_interface node field.
|
||||
"""Check if we should support storage_interface node and driver fields.
|
||||
|
||||
Version 1.33 of the API added support for storage interfaces.
|
||||
"""
|
||||
@ -738,7 +740,7 @@ def allow_rescue_interface():
|
||||
|
||||
|
||||
def allow_bios_interface():
|
||||
"""Check if we should support bios interface.
|
||||
"""Check if we should support bios interface and endpoints.
|
||||
|
||||
Version 1.40 of the API added support for bios interface.
|
||||
"""
|
||||
@ -889,24 +891,6 @@ def allow_reset_interfaces():
|
||||
versions.MINOR_45_RESET_INTERFACES)
|
||||
|
||||
|
||||
def allow_conductor_group():
|
||||
"""Check if passing a conductor_group for a node is allowed.
|
||||
|
||||
Version 1.46 exposes this field.
|
||||
"""
|
||||
return (pecan.request.version.minor >=
|
||||
versions.MINOR_46_NODE_CONDUCTOR_GROUP)
|
||||
|
||||
|
||||
def allow_automated_clean():
|
||||
"""Check if passing automated_clean for a node is allowed.
|
||||
|
||||
Version 1.47 exposes this field.
|
||||
"""
|
||||
return (pecan.request.version.minor >=
|
||||
versions.MINOR_47_NODE_AUTOMATED_CLEAN)
|
||||
|
||||
|
||||
def get_request_return_fields(fields, detail, default_fields):
|
||||
"""Calculate fields to return from an API request
|
||||
|
||||
|
@ -404,20 +404,6 @@ class TestApiUtils(base.TestCase):
|
||||
mock_request.version.minor = 18
|
||||
self.assertFalse(utils.allow_port_advanced_net_fields())
|
||||
|
||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||
def test_allow_network_interface(self, mock_request):
|
||||
mock_request.version.minor = 20
|
||||
self.assertTrue(utils.allow_network_interface())
|
||||
mock_request.version.minor = 19
|
||||
self.assertFalse(utils.allow_network_interface())
|
||||
|
||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||
def test_allow_resource_class(self, mock_request):
|
||||
mock_request.version.minor = 21
|
||||
self.assertTrue(utils.allow_resource_class())
|
||||
mock_request.version.minor = 20
|
||||
self.assertFalse(utils.allow_resource_class())
|
||||
|
||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||
def test_allow_ramdisk_endpoints(self, mock_request):
|
||||
mock_request.version.minor = 22
|
||||
@ -537,13 +523,6 @@ class TestApiUtils(base.TestCase):
|
||||
mock_request.version.minor = 40
|
||||
self.assertFalse(utils.allow_inspect_abort())
|
||||
|
||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||
def test_allow_conductor_group(self, mock_request):
|
||||
mock_request.version.minor = 46
|
||||
self.assertTrue(utils.allow_conductor_group())
|
||||
mock_request.version.minor = 45
|
||||
self.assertFalse(utils.allow_conductor_group())
|
||||
|
||||
|
||||
class TestNodeIdent(base.TestCase):
|
||||
|
||||
|
@ -23,9 +23,9 @@ from ironic.api.controllers.v1 import chassis as chassis_controller
|
||||
from ironic.api.controllers.v1 import node as node_controller
|
||||
from ironic.api.controllers.v1 import port as port_controller
|
||||
from ironic.api.controllers.v1 import portgroup as portgroup_controller
|
||||
from ironic.api.controllers.v1 import utils as api_utils
|
||||
from ironic.api.controllers.v1 import volume_connector as vc_controller
|
||||
from ironic.api.controllers.v1 import volume_target as vt_controller
|
||||
from ironic.drivers import base as drivers_base
|
||||
from ironic.tests.unit.db import utils as db_utils
|
||||
|
||||
ADMIN_TOKEN = '4562138218392831'
|
||||
@ -104,16 +104,9 @@ def node_post_data(**kw):
|
||||
# NOTE(jroll): pop out fields that were introduced in later API versions,
|
||||
# unless explicitly requested. Otherwise, these will cause tests using
|
||||
# older API versions to fail.
|
||||
for iface in drivers_base.ALL_INTERFACES:
|
||||
name = '%s_interface' % iface
|
||||
if name not in kw:
|
||||
node.pop(name)
|
||||
if 'resource_class' not in kw:
|
||||
node.pop('resource_class')
|
||||
if 'fault' not in kw:
|
||||
node.pop('fault')
|
||||
if 'automated_clean' not in kw:
|
||||
node.pop('automated_clean')
|
||||
for field in api_utils.VERSIONED_FIELDS:
|
||||
if field not in kw:
|
||||
node.pop(field, None)
|
||||
|
||||
internal = node_controller.NodePatchType.internal_attrs()
|
||||
return remove_internal(node, internal)
|
||||
|
Loading…
x
Reference in New Issue
Block a user