Expose node's network_interface field in API
This patch exposes the node's network_interface field in the REST API. It also adds restrictions on the node states in which network interface change is possible and whether the requested network interface is enabled. As a temporary solution until the driver composition work is completed, we have taken an approach that requires all API and Conductor nodes to have the same setting for enabled_network_interfaces. There are inline notes in the code indicating where we will address this in the future. Partial-bug: #1526403 Co-Authored-By: Om Kumar <om.kumar@hp.com> Co-Authored-By: Vasyl Saienko <vsaienko@mirantis.com> Co-Authored-By: Sivaramakrishna Garimella <sivaramakrishna.garimella@hp.com> Co-Authored-By: Vladyslav Drok <vdrok@mirantis.com> Co-Authored-By: Zhenguo Niu <Niu.ZGlinux@gmail.com> Change-Id: I67495196c3334f51ed034f4ca6e32a3e01a58f15
This commit is contained in:
parent
6d846590bc
commit
c62e1bee29
@ -32,6 +32,10 @@ always requests the newest supported API version.
|
|||||||
API Versions History
|
API Versions History
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
**1.20**
|
||||||
|
|
||||||
|
Add node ``network_interface`` field.
|
||||||
|
|
||||||
**1.19**
|
**1.19**
|
||||||
|
|
||||||
Add ``local_link_connection`` and ``pxe_enabled`` fields to the port object.
|
Add ``local_link_connection`` and ``pxe_enabled`` fields to the port object.
|
||||||
|
@ -37,8 +37,11 @@
|
|||||||
# recommended set of production-oriented network interfaces. A
|
# recommended set of production-oriented network interfaces. A
|
||||||
# complete list of network interfaces present on your system
|
# complete list of network interfaces present on your system
|
||||||
# may be found by enumerating the
|
# may be found by enumerating the
|
||||||
# "ironic.hardware.interfaces.network" entrypoint. (list
|
# "ironic.hardware.interfaces.network" entrypoint.This value
|
||||||
# value)
|
# must be the same on all ironic-conductor and ironic-api
|
||||||
|
# services, because it is used by ironic-api service to
|
||||||
|
# validate a new or updated node's network_interface value.
|
||||||
|
# (list value)
|
||||||
#enabled_network_interfaces = flat,noop
|
#enabled_network_interfaces = flat,noop
|
||||||
|
|
||||||
# Default network interface to be used for nodes that do not
|
# Default network interface to be used for nodes that do not
|
||||||
@ -920,17 +923,18 @@
|
|||||||
|
|
||||||
# Size of EFI system partition in MiB when configuring UEFI
|
# Size of EFI system partition in MiB when configuring UEFI
|
||||||
# systems for local boot. (integer value)
|
# systems for local boot. (integer value)
|
||||||
# Deprecated group/name - [deploy]/efi_system_partition_size
|
|
||||||
#efi_system_partition_size = 200
|
#efi_system_partition_size = 200
|
||||||
|
|
||||||
|
# Size of BIOS Boot partition in MiB when configuring GPT
|
||||||
|
# partitioned systems for local boot in BIOS. (integer value)
|
||||||
|
#bios_boot_partition_size = 1
|
||||||
|
|
||||||
# Block size to use when writing to the nodes disk. (string
|
# Block size to use when writing to the nodes disk. (string
|
||||||
# value)
|
# value)
|
||||||
# Deprecated group/name - [deploy]/dd_block_size
|
|
||||||
#dd_block_size = 1M
|
#dd_block_size = 1M
|
||||||
|
|
||||||
# Maximum attempts to verify an iSCSI connection is active,
|
# Maximum attempts to verify an iSCSI connection is active,
|
||||||
# sleeping 1 second between attempts. (integer value)
|
# sleeping 1 second between attempts. (integer value)
|
||||||
# Deprecated group/name - [deploy]/iscsi_verify_attempts
|
|
||||||
#iscsi_verify_attempts = 3
|
#iscsi_verify_attempts = 3
|
||||||
|
|
||||||
|
|
||||||
@ -1289,7 +1293,16 @@
|
|||||||
# From keystonemiddleware.auth_token
|
# From keystonemiddleware.auth_token
|
||||||
#
|
#
|
||||||
|
|
||||||
# Complete public Identity API endpoint. (string value)
|
# Complete "public" Identity API endpoint. This endpoint
|
||||||
|
# should not be an "admin" endpoint, as it should be
|
||||||
|
# accessible by all end users. Unauthenticated clients are
|
||||||
|
# redirected to this endpoint to authenticate. Although this
|
||||||
|
# endpoint should ideally be unversioned, client support in
|
||||||
|
# the wild varies. If you're using a versioned v2 endpoint
|
||||||
|
# here, then this should *not* be the same endpoint the
|
||||||
|
# service user utilizes for validating tokens, because normal
|
||||||
|
# end users may not be able to reach that endpoint. (string
|
||||||
|
# value)
|
||||||
#auth_uri = <None>
|
#auth_uri = <None>
|
||||||
|
|
||||||
# API version of the admin Identity API endpoint. (string
|
# API version of the admin Identity API endpoint. (string
|
||||||
@ -1428,12 +1441,12 @@
|
|||||||
# (list value)
|
# (list value)
|
||||||
#hash_algorithms = md5
|
#hash_algorithms = md5
|
||||||
|
|
||||||
# Authentication type to load (unknown value)
|
# Authentication type to load (string value)
|
||||||
# Deprecated group/name - [keystone_authtoken]/auth_plugin
|
# Deprecated group/name - [keystone_authtoken]/auth_plugin
|
||||||
#auth_type = <None>
|
#auth_type = <None>
|
||||||
|
|
||||||
# Config Section from which to load plugin specific options
|
# Config Section from which to load plugin specific options
|
||||||
# (unknown value)
|
# (string value)
|
||||||
#auth_section = <None>
|
#auth_section = <None>
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,6 +45,7 @@ from ironic import objects
|
|||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
CONF.import_opt('heartbeat_timeout', 'ironic.conductor.manager',
|
CONF.import_opt('heartbeat_timeout', 'ironic.conductor.manager',
|
||||||
group='conductor')
|
group='conductor')
|
||||||
|
CONF.import_opt('enabled_network_interfaces', 'ironic.common.driver_factory')
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
_CLEAN_STEPS_SCHEMA = {
|
_CLEAN_STEPS_SCHEMA = {
|
||||||
@ -109,7 +110,12 @@ def get_nodes_controller_reserved_names():
|
|||||||
|
|
||||||
|
|
||||||
def hide_fields_in_newer_versions(obj):
|
def hide_fields_in_newer_versions(obj):
|
||||||
# if requested version is < 1.3, hide driver_internal_info
|
"""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.
|
||||||
|
"""
|
||||||
if pecan.request.version.minor < versions.MINOR_3_DRIVER_INTERNAL_INFO:
|
if pecan.request.version.minor < versions.MINOR_3_DRIVER_INTERNAL_INFO:
|
||||||
obj.driver_internal_info = wsme.Unset
|
obj.driver_internal_info = wsme.Unset
|
||||||
|
|
||||||
@ -128,6 +134,9 @@ def hide_fields_in_newer_versions(obj):
|
|||||||
obj.raid_config = wsme.Unset
|
obj.raid_config = wsme.Unset
|
||||||
obj.target_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
|
||||||
|
|
||||||
|
|
||||||
def update_state_in_older_versions(obj):
|
def update_state_in_older_versions(obj):
|
||||||
"""Change provision state names for API backwards compatability.
|
"""Change provision state names for API backwards compatability.
|
||||||
@ -696,6 +705,9 @@ class Node(base.APIBase):
|
|||||||
states = wsme.wsattr([link.Link], readonly=True)
|
states = wsme.wsattr([link.Link], readonly=True)
|
||||||
"""Links to endpoint for retrieving and setting node states"""
|
"""Links to endpoint for retrieving and setting node states"""
|
||||||
|
|
||||||
|
network_interface = wsme.wsattr(wtypes.text)
|
||||||
|
"""The network interface to be used for this node"""
|
||||||
|
|
||||||
# NOTE(deva): "conductor_affinity" shouldn't be presented on the
|
# NOTE(deva): "conductor_affinity" shouldn't be presented on the
|
||||||
# API because it's an internal value. Don't add it here.
|
# API because it's an internal value. Don't add it here.
|
||||||
|
|
||||||
@ -794,7 +806,8 @@ class Node(base.APIBase):
|
|||||||
maintenance=False, maintenance_reason=None,
|
maintenance=False, maintenance_reason=None,
|
||||||
inspection_finished_at=None, inspection_started_at=time,
|
inspection_finished_at=None, inspection_started_at=time,
|
||||||
console_enabled=False, clean_step={},
|
console_enabled=False, clean_step={},
|
||||||
raid_config=None, target_raid_config=None)
|
raid_config=None, target_raid_config=None,
|
||||||
|
network_interface='flat')
|
||||||
# NOTE(matty_dubs): The chassis_uuid getter() is based on the
|
# NOTE(matty_dubs): The chassis_uuid getter() is based on the
|
||||||
# _chassis_uuid variable:
|
# _chassis_uuid variable:
|
||||||
sample._chassis_uuid = 'edcad704-b2da-41d5-96d9-afd580ecfa12'
|
sample._chassis_uuid = 'edcad704-b2da-41d5-96d9-afd580ecfa12'
|
||||||
@ -1129,6 +1142,7 @@ class NodesController(rest.RestController):
|
|||||||
api_utils.check_allow_specify_fields(fields)
|
api_utils.check_allow_specify_fields(fields)
|
||||||
api_utils.check_for_invalid_state_and_allow_filter(provision_state)
|
api_utils.check_for_invalid_state_and_allow_filter(provision_state)
|
||||||
api_utils.check_allow_specify_driver(driver)
|
api_utils.check_allow_specify_driver(driver)
|
||||||
|
api_utils.check_allow_specify_network_interface_in_fields(fields)
|
||||||
if fields is None:
|
if fields is None:
|
||||||
fields = _DEFAULT_RETURN_FIELDS
|
fields = _DEFAULT_RETURN_FIELDS
|
||||||
return self._get_nodes_collection(chassis_uuid, instance_uuid,
|
return self._get_nodes_collection(chassis_uuid, instance_uuid,
|
||||||
@ -1213,6 +1227,7 @@ class NodesController(rest.RestController):
|
|||||||
raise exception.OperationNotPermitted()
|
raise exception.OperationNotPermitted()
|
||||||
|
|
||||||
api_utils.check_allow_specify_fields(fields)
|
api_utils.check_allow_specify_fields(fields)
|
||||||
|
api_utils.check_allow_specify_network_interface_in_fields(fields)
|
||||||
|
|
||||||
rpc_node = api_utils.get_rpc_node(node_ident)
|
rpc_node = api_utils.get_rpc_node(node_ident)
|
||||||
return Node.convert_with_links(rpc_node, fields=fields)
|
return Node.convert_with_links(rpc_node, fields=fields)
|
||||||
@ -1226,6 +1241,26 @@ class NodesController(rest.RestController):
|
|||||||
if self.from_chassis:
|
if self.from_chassis:
|
||||||
raise exception.OperationNotPermitted()
|
raise exception.OperationNotPermitted()
|
||||||
|
|
||||||
|
n_interface = node.network_interface
|
||||||
|
if (not api_utils.allow_network_interface() and
|
||||||
|
n_interface is not wtypes.Unset):
|
||||||
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
|
# NOTE(vsaienko) The validation is performed on API side,
|
||||||
|
# all conductors and api should have the same list of
|
||||||
|
# enabled_network_interfaces.
|
||||||
|
# TODO(vsaienko) remove it once driver-composition-reform
|
||||||
|
# is implemented.
|
||||||
|
if (n_interface is not wtypes.Unset and
|
||||||
|
not api_utils.is_valid_network_interface(n_interface)):
|
||||||
|
error_msg = _("Cannot create node with the invalid network "
|
||||||
|
"interface '%(n_interface)s'. Enabled network "
|
||||||
|
"interfaces are: %(enabled_int)s")
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
error_msg % {'n_interface': n_interface,
|
||||||
|
'enabled_int': CONF.enabled_network_interfaces},
|
||||||
|
status_code=http_client.BAD_REQUEST)
|
||||||
|
|
||||||
# NOTE(deva): get_topic_for checks if node.driver is in the hash ring
|
# NOTE(deva): get_topic_for checks if node.driver is in the hash ring
|
||||||
# and raises NoValidHost if it is not.
|
# and raises NoValidHost if it is not.
|
||||||
# We need to ensure that node has a UUID before it can
|
# We need to ensure that node has a UUID before it can
|
||||||
@ -1265,6 +1300,21 @@ class NodesController(rest.RestController):
|
|||||||
if self.from_chassis:
|
if self.from_chassis:
|
||||||
raise exception.OperationNotPermitted()
|
raise exception.OperationNotPermitted()
|
||||||
|
|
||||||
|
n_interfaces = api_utils.get_patch_values(patch, '/network_interface')
|
||||||
|
if n_interfaces and not api_utils.allow_network_interface():
|
||||||
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
|
for n_interface in n_interfaces:
|
||||||
|
if (n_interface is not None and
|
||||||
|
not api_utils.is_valid_network_interface(n_interface)):
|
||||||
|
error_msg = _("Node %(node)s: Cannot change "
|
||||||
|
"network_interface to invalid value: "
|
||||||
|
"%(n_interface)s")
|
||||||
|
raise wsme.exc.ClientSideError(
|
||||||
|
error_msg % {'node': node_ident,
|
||||||
|
'n_interface': n_interface},
|
||||||
|
status_code=http_client.BAD_REQUEST)
|
||||||
|
|
||||||
rpc_node = api_utils.get_rpc_node(node_ident)
|
rpc_node = api_utils.get_rpc_node(node_ident)
|
||||||
|
|
||||||
remove_inst_uuid_patch = [{'op': 'remove', 'path': '/instance_uuid'}]
|
remove_inst_uuid_patch = [{'op': 'remove', 'path': '/instance_uuid'}]
|
||||||
|
@ -240,6 +240,34 @@ def check_allow_specify_fields(fields):
|
|||||||
raise exception.NotAcceptable()
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
|
|
||||||
|
def check_allow_specify_network_interface_in_fields(fields):
|
||||||
|
"""Check if fetching a network_interface attribute is allowed.
|
||||||
|
|
||||||
|
Version 1.20 of the API allows to fetching a network_interface
|
||||||
|
attribute. This method check if the required version is being
|
||||||
|
requested.
|
||||||
|
"""
|
||||||
|
if (fields is not None
|
||||||
|
and 'network_interface' in fields
|
||||||
|
and not allow_network_interface()):
|
||||||
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE(vsaienko) The validation is performed on API side, all conductors
|
||||||
|
# and api should have the same list of enabled_network_interfaces.
|
||||||
|
# TODO(vsaienko) remove it once driver-composition-reform is implemented.
|
||||||
|
def is_valid_network_interface(network_interface):
|
||||||
|
"""Determine if the provided network_interface is valid.
|
||||||
|
|
||||||
|
Check to see that the provided network_interface is in the enabled
|
||||||
|
network interfaces list.
|
||||||
|
|
||||||
|
:param: network_interface: the node network interface to check.
|
||||||
|
:returns: True if the network_interface is valid, False otherwise.
|
||||||
|
"""
|
||||||
|
return network_interface in CONF.enabled_network_interfaces
|
||||||
|
|
||||||
|
|
||||||
def check_allow_management_verbs(verb):
|
def check_allow_management_verbs(verb):
|
||||||
min_version = MIN_VERB_VERSIONS.get(verb)
|
min_version = MIN_VERB_VERSIONS.get(verb)
|
||||||
if min_version is not None and pecan.request.version.minor < min_version:
|
if min_version is not None and pecan.request.version.minor < min_version:
|
||||||
@ -322,6 +350,15 @@ def allow_port_advanced_net_fields():
|
|||||||
versions.MINOR_19_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 get_controller_reserved_names(cls):
|
def get_controller_reserved_names(cls):
|
||||||
"""Get reserved names for a given controller.
|
"""Get reserved names for a given controller.
|
||||||
|
|
||||||
|
@ -49,6 +49,7 @@ BASE_VERSION = 1
|
|||||||
# v1.17: Add 'adopt' verb for ADOPTING active nodes.
|
# v1.17: Add 'adopt' verb for ADOPTING active nodes.
|
||||||
# v1.18: Add port.internal_info.
|
# v1.18: Add port.internal_info.
|
||||||
# v1.19: Add port.local_link_connection and port.pxe_enabled.
|
# v1.19: Add port.local_link_connection and port.pxe_enabled.
|
||||||
|
# v1.20: Add node.network_interface
|
||||||
|
|
||||||
MINOR_0_JUNO = 0
|
MINOR_0_JUNO = 0
|
||||||
MINOR_1_INITIAL_VERSION = 1
|
MINOR_1_INITIAL_VERSION = 1
|
||||||
@ -70,11 +71,12 @@ MINOR_16_DRIVER_FILTER = 16
|
|||||||
MINOR_17_ADOPT_VERB = 17
|
MINOR_17_ADOPT_VERB = 17
|
||||||
MINOR_18_PORT_INTERNAL_INFO = 18
|
MINOR_18_PORT_INTERNAL_INFO = 18
|
||||||
MINOR_19_PORT_ADVANCED_NET_FIELDS = 19
|
MINOR_19_PORT_ADVANCED_NET_FIELDS = 19
|
||||||
|
MINOR_20_NETWORK_INTERFACE = 20
|
||||||
|
|
||||||
# When adding another version, update MINOR_MAX_VERSION and also update
|
# When adding another version, update MINOR_MAX_VERSION and also update
|
||||||
# doc/source/webapi/v1.rst with a detailed explanation of what the version has
|
# doc/source/webapi/v1.rst with a detailed explanation of what the version has
|
||||||
# changed.
|
# changed.
|
||||||
MINOR_MAX_VERSION = MINOR_19_PORT_ADVANCED_NET_FIELDS
|
MINOR_MAX_VERSION = MINOR_20_NETWORK_INTERFACE
|
||||||
|
|
||||||
# String representations of the minor and maximum versions
|
# String representations of the minor and maximum versions
|
||||||
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||||
|
@ -51,7 +51,11 @@ driver_opts = [
|
|||||||
'production-oriented network interfaces. A complete '
|
'production-oriented network interfaces. A complete '
|
||||||
'list of network interfaces present on your system may '
|
'list of network interfaces present on your system may '
|
||||||
'be found by enumerating the '
|
'be found by enumerating the '
|
||||||
'"ironic.hardware.interfaces.network" entrypoint.')),
|
'"ironic.hardware.interfaces.network" entrypoint.'
|
||||||
|
'This value must be the same on all ironic-conductor '
|
||||||
|
'and ironic-api services, because it is used by '
|
||||||
|
'ironic-api service to validate a new or updated '
|
||||||
|
'node\'s network_interface value.')),
|
||||||
cfg.StrOpt('default_network_interface',
|
cfg.StrOpt('default_network_interface',
|
||||||
help=_('Default network interface to be used for nodes that '
|
help=_('Default network interface to be used for nodes that '
|
||||||
'do not have network_interface field set. A complete '
|
'do not have network_interface field set. A complete '
|
||||||
|
@ -91,7 +91,8 @@ class ConductorManager(base_manager.BaseConductorManager):
|
|||||||
|
|
||||||
@messaging.expected_exceptions(exception.InvalidParameterValue,
|
@messaging.expected_exceptions(exception.InvalidParameterValue,
|
||||||
exception.MissingParameterValue,
|
exception.MissingParameterValue,
|
||||||
exception.NodeLocked)
|
exception.NodeLocked,
|
||||||
|
exception.InvalidState)
|
||||||
def update_node(self, context, node_obj):
|
def update_node(self, context, node_obj):
|
||||||
"""Update a node with the supplied data.
|
"""Update a node with the supplied data.
|
||||||
|
|
||||||
@ -113,6 +114,27 @@ class ConductorManager(base_manager.BaseConductorManager):
|
|||||||
if 'maintenance' in delta and not node_obj.maintenance:
|
if 'maintenance' in delta and not node_obj.maintenance:
|
||||||
node_obj.maintenance_reason = None
|
node_obj.maintenance_reason = None
|
||||||
|
|
||||||
|
if 'network_interface' in delta:
|
||||||
|
allowed_update_states = [states.ENROLL, states.INSPECTING,
|
||||||
|
states.MANAGEABLE]
|
||||||
|
if not (node_obj.provision_state in allowed_update_states or
|
||||||
|
node_obj.maintenance):
|
||||||
|
action = _("Node %(node)s can not have network_interface "
|
||||||
|
"updated unless it is in one of allowed "
|
||||||
|
"(%(allowed)s) states or in maintenance mode.")
|
||||||
|
raise exception.InvalidState(
|
||||||
|
action % {'node': node_obj.uuid,
|
||||||
|
'allowed': ', '.join(allowed_update_states)})
|
||||||
|
net_iface = node_obj.network_interface
|
||||||
|
if net_iface not in CONF.enabled_network_interfaces:
|
||||||
|
raise exception.InvalidParameterValue(
|
||||||
|
_("Cannot change network_interface to invalid value "
|
||||||
|
"%(n_interface)s for node %(node)s, valid interfaces "
|
||||||
|
"are: %(valid_choices)s.") % {
|
||||||
|
'n_interface': net_iface, 'node': node_obj.uuid,
|
||||||
|
'valid_choices': CONF.enabled_network_interfaces,
|
||||||
|
})
|
||||||
|
|
||||||
driver_name = node_obj.driver if 'driver' in delta else None
|
driver_name = node_obj.driver if 'driver' in delta else None
|
||||||
with task_manager.acquire(context, node_id, shared=False,
|
with task_manager.acquire(context, node_id, shared=False,
|
||||||
driver_name=driver_name,
|
driver_name=driver_name,
|
||||||
|
@ -110,6 +110,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertNotIn('clean_step', data['nodes'][0])
|
self.assertNotIn('clean_step', data['nodes'][0])
|
||||||
self.assertNotIn('raid_config', data['nodes'][0])
|
self.assertNotIn('raid_config', data['nodes'][0])
|
||||||
self.assertNotIn('target_raid_config', data['nodes'][0])
|
self.assertNotIn('target_raid_config', data['nodes'][0])
|
||||||
|
self.assertNotIn('network_interface', data['nodes'][0])
|
||||||
# never expose the chassis_id
|
# never expose the chassis_id
|
||||||
self.assertNotIn('chassis_id', data['nodes'][0])
|
self.assertNotIn('chassis_id', data['nodes'][0])
|
||||||
|
|
||||||
@ -135,6 +136,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertIn('inspection_started_at', data)
|
self.assertIn('inspection_started_at', data)
|
||||||
self.assertIn('clean_step', data)
|
self.assertIn('clean_step', data)
|
||||||
self.assertIn('states', data)
|
self.assertIn('states', data)
|
||||||
|
self.assertIn('network_interface', data)
|
||||||
# never expose the chassis_id
|
# never expose the chassis_id
|
||||||
self.assertNotIn('chassis_id', data)
|
self.assertNotIn('chassis_id', data)
|
||||||
|
|
||||||
@ -206,6 +208,25 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertItemsEqual(['driver_info', 'links'], data)
|
self.assertItemsEqual(['driver_info', 'links'], data)
|
||||||
self.assertEqual('******', data['driver_info']['fake_password'])
|
self.assertEqual('******', data['driver_info']['fake_password'])
|
||||||
|
|
||||||
|
def test_get_network_interface_fields_invalid_api_version(self):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
chassis_id=self.chassis.id)
|
||||||
|
fields = 'network_interface'
|
||||||
|
response = self.get_json(
|
||||||
|
'/nodes/%s?fields=%s' % (node.uuid, fields),
|
||||||
|
headers={api_base.Version.string: str(api_v1.MIN_VER)},
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
||||||
|
|
||||||
|
def test_get_network_interface_fields(self):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
chassis_id=self.chassis.id)
|
||||||
|
fields = 'network_interface'
|
||||||
|
response = self.get_json(
|
||||||
|
'/nodes/%s?fields=%s' % (node.uuid, fields),
|
||||||
|
headers={api_base.Version.string: str(api_v1.MAX_VER)})
|
||||||
|
self.assertIn('network_interface', response)
|
||||||
|
|
||||||
def test_detail(self):
|
def test_detail(self):
|
||||||
node = obj_utils.create_test_node(self.context,
|
node = obj_utils.create_test_node(self.context,
|
||||||
chassis_id=self.chassis.id)
|
chassis_id=self.chassis.id)
|
||||||
@ -229,6 +250,7 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
self.assertIn('inspection_started_at', data['nodes'][0])
|
self.assertIn('inspection_started_at', data['nodes'][0])
|
||||||
self.assertIn('raid_config', data['nodes'][0])
|
self.assertIn('raid_config', data['nodes'][0])
|
||||||
self.assertIn('target_raid_config', data['nodes'][0])
|
self.assertIn('target_raid_config', data['nodes'][0])
|
||||||
|
self.assertIn('network_interface', data['nodes'][0])
|
||||||
# never expose the chassis_id
|
# never expose the chassis_id
|
||||||
self.assertNotIn('chassis_id', data['nodes'][0])
|
self.assertNotIn('chassis_id', data['nodes'][0])
|
||||||
|
|
||||||
@ -303,6 +325,17 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
headers={api_base.Version.string: "1.7"})
|
headers={api_base.Version.string: "1.7"})
|
||||||
self.assertEqual({"foo": "bar"}, data['clean_step'])
|
self.assertEqual({"foo": "bar"}, data['clean_step'])
|
||||||
|
|
||||||
|
def test_hide_fields_in_newer_versions_network_interface(self):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
network_interface='flat')
|
||||||
|
data = self.get_json(
|
||||||
|
'/nodes/detail', headers={api_base.Version.string: '1.19'})
|
||||||
|
self.assertNotIn('network_interface', data['nodes'][0])
|
||||||
|
new_data = self.get_json(
|
||||||
|
'/nodes/detail', headers={api_base.Version.string: '1.20'})
|
||||||
|
self.assertEqual(node.network_interface,
|
||||||
|
new_data['nodes'][0]["network_interface"])
|
||||||
|
|
||||||
def test_many(self):
|
def test_many(self):
|
||||||
nodes = []
|
nodes = []
|
||||||
for id in range(5):
|
for id in range(5):
|
||||||
@ -1390,6 +1423,35 @@ class TestPatch(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
self.assertEqual(http_client.OK, response.status_code)
|
self.assertEqual(http_client.OK, response.status_code)
|
||||||
|
|
||||||
|
def test_update_network_interface(self):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
uuid=uuidutils.generate_uuid())
|
||||||
|
self.mock_update_node.return_value = node
|
||||||
|
network_interface = 'flat'
|
||||||
|
headers = {api_base.Version.string: str(api_v1.MAX_VER)}
|
||||||
|
response = self.patch_json('/nodes/%s' % node.uuid,
|
||||||
|
[{'path': '/network_interface',
|
||||||
|
'value': network_interface,
|
||||||
|
'op': 'add'}],
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.OK, response.status_code)
|
||||||
|
|
||||||
|
def test_update_network_interface_old_api(self):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
uuid=uuidutils.generate_uuid())
|
||||||
|
self.mock_update_node.return_value = node
|
||||||
|
network_interface = 'flat'
|
||||||
|
headers = {api_base.Version.string: '1.15'}
|
||||||
|
response = self.patch_json('/nodes/%s' % node.uuid,
|
||||||
|
[{'path': '/network_interface',
|
||||||
|
'value': network_interface,
|
||||||
|
'op': 'add'}],
|
||||||
|
headers=headers,
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
||||||
|
|
||||||
|
|
||||||
class TestPost(test_api_base.BaseApiTest):
|
class TestPost(test_api_base.BaseApiTest):
|
||||||
|
|
||||||
@ -1703,6 +1765,34 @@ class TestPost(test_api_base.BaseApiTest):
|
|||||||
# Assert RPC method wasn't called this time
|
# Assert RPC method wasn't called this time
|
||||||
self.assertFalse(get_methods_mock.called)
|
self.assertFalse(get_methods_mock.called)
|
||||||
|
|
||||||
|
def test_create_node_network_interface(self):
|
||||||
|
ndict = test_api_utils.post_get_test_node(
|
||||||
|
network_interface='flat')
|
||||||
|
response = self.post_json('/nodes', ndict,
|
||||||
|
headers={api_base.Version.string:
|
||||||
|
str(api_v1.MAX_VER)})
|
||||||
|
self.assertEqual(http_client.CREATED, response.status_int)
|
||||||
|
result = self.get_json('/nodes/%s' % ndict['uuid'],
|
||||||
|
headers={api_base.Version.string:
|
||||||
|
str(api_v1.MAX_VER)})
|
||||||
|
self.assertEqual('flat', result['network_interface'])
|
||||||
|
|
||||||
|
def test_create_node_network_interface_old_api_version(self):
|
||||||
|
ndict = test_api_utils.post_get_test_node(
|
||||||
|
network_interface='flat')
|
||||||
|
response = self.post_json('/nodes', ndict, expect_errors=True)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
|
||||||
|
|
||||||
|
def test_create_node_invalid_network_interface(self):
|
||||||
|
ndict = test_api_utils.post_get_test_node(
|
||||||
|
network_interface='foo')
|
||||||
|
response = self.post_json('/nodes', ndict, expect_errors=True,
|
||||||
|
headers={api_base.Version.string:
|
||||||
|
str(api_v1.MAX_VER)})
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
|
||||||
|
|
||||||
|
|
||||||
class TestDelete(test_api_base.BaseApiTest):
|
class TestDelete(test_api_base.BaseApiTest):
|
||||||
|
|
||||||
|
@ -130,6 +130,22 @@ class TestApiUtils(base.TestCase):
|
|||||||
self.assertRaises(exception.NotAcceptable,
|
self.assertRaises(exception.NotAcceptable,
|
||||||
utils.check_allow_specify_fields, ['foo'])
|
utils.check_allow_specify_fields, ['foo'])
|
||||||
|
|
||||||
|
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||||
|
def test_check_allow_specify_network_interface(self, mock_request):
|
||||||
|
mock_request.version.minor = 20
|
||||||
|
self.assertIsNone(
|
||||||
|
utils.check_allow_specify_network_interface_in_fields(
|
||||||
|
['network_interface']))
|
||||||
|
|
||||||
|
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||||
|
def test_check_allow_specify_network_interface_in_fields_fail(
|
||||||
|
self, mock_request):
|
||||||
|
mock_request.version.minor = 19
|
||||||
|
self.assertRaises(
|
||||||
|
exception.NotAcceptable,
|
||||||
|
utils.check_allow_specify_network_interface_in_fields,
|
||||||
|
['network_interface'])
|
||||||
|
|
||||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||||
def test_check_allow_specify_driver(self, mock_request):
|
def test_check_allow_specify_driver(self, mock_request):
|
||||||
mock_request.version.minor = 16
|
mock_request.version.minor = 16
|
||||||
@ -232,6 +248,13 @@ class TestApiUtils(base.TestCase):
|
|||||||
mock_request.version.minor = 18
|
mock_request.version.minor = 18
|
||||||
self.assertFalse(utils.allow_port_advanced_net_fields())
|
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())
|
||||||
|
|
||||||
|
|
||||||
class TestNodeIdent(base.TestCase):
|
class TestNodeIdent(base.TestCase):
|
||||||
|
|
||||||
|
@ -285,6 +285,51 @@ class UpdateNodeTestCase(mgr_utils.ServiceSetUpMixin,
|
|||||||
node.refresh()
|
node.refresh()
|
||||||
self.assertEqual(existing_driver, node.driver)
|
self.assertEqual(existing_driver, node.driver)
|
||||||
|
|
||||||
|
def test_update_network_node_deleting_state(self):
|
||||||
|
node = obj_utils.create_test_node(self.context, driver='fake',
|
||||||
|
provision_state=states.DELETING,
|
||||||
|
network_interface='flat')
|
||||||
|
old_iface = node.network_interface
|
||||||
|
node.network_interface = 'noop'
|
||||||
|
exc = self.assertRaises(messaging.rpc.ExpectedException,
|
||||||
|
self.service.update_node,
|
||||||
|
self.context, node)
|
||||||
|
self.assertEqual(exception.InvalidState, exc.exc_info[0])
|
||||||
|
node.refresh()
|
||||||
|
self.assertEqual(old_iface, node.network_interface)
|
||||||
|
|
||||||
|
def test_update_network_node_manageable_state(self):
|
||||||
|
node = obj_utils.create_test_node(self.context, driver='fake',
|
||||||
|
provision_state=states.MANAGEABLE,
|
||||||
|
network_interface='flat')
|
||||||
|
node.network_interface = 'noop'
|
||||||
|
self.service.update_node(self.context, node)
|
||||||
|
node.refresh()
|
||||||
|
self.assertEqual('noop', node.network_interface)
|
||||||
|
|
||||||
|
def test_update_network_node_active_state_and_maintenance(self):
|
||||||
|
node = obj_utils.create_test_node(self.context, driver='fake',
|
||||||
|
provision_state=states.ACTIVE,
|
||||||
|
network_interface='flat',
|
||||||
|
maintenance=True)
|
||||||
|
node.network_interface = 'noop'
|
||||||
|
self.service.update_node(self.context, node)
|
||||||
|
node.refresh()
|
||||||
|
self.assertEqual('noop', node.network_interface)
|
||||||
|
|
||||||
|
def test_update_node_invalid_network_interface(self):
|
||||||
|
node = obj_utils.create_test_node(self.context, driver='fake',
|
||||||
|
provision_state=states.MANAGEABLE,
|
||||||
|
network_interface='flat')
|
||||||
|
old_iface = node.network_interface
|
||||||
|
node.network_interface = 'cosci'
|
||||||
|
exc = self.assertRaises(messaging.rpc.ExpectedException,
|
||||||
|
self.service.update_node,
|
||||||
|
self.context, node)
|
||||||
|
self.assertEqual(exception.InvalidParameterValue, exc.exc_info[0])
|
||||||
|
node.refresh()
|
||||||
|
self.assertEqual(old_iface, node.network_interface)
|
||||||
|
|
||||||
|
|
||||||
@mgr_utils.mock_record_keepalive
|
@mgr_utils.mock_record_keepalive
|
||||||
class VendorPassthruTestCase(mgr_utils.ServiceSetUpMixin,
|
class VendorPassthruTestCase(mgr_utils.ServiceSetUpMixin,
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Bumped API version to 1.20. It adds API methods to work with
|
||||||
|
``network_interface`` node object field, that specifies the network
|
||||||
|
interface to use for that node. Its value must be identical and
|
||||||
|
present in the ``[DEFAULT]enabled_network_interfaces`` list option
|
||||||
|
on conductor and api nodes.
|
Loading…
x
Reference in New Issue
Block a user