From c62e1bee29e795ece05b6be8c613910d75a7543e Mon Sep 17 00:00:00 2001 From: Vasyl Saienko Date: Tue, 17 May 2016 13:59:50 +0300 Subject: [PATCH] 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 Co-Authored-By: Vasyl Saienko Co-Authored-By: Sivaramakrishna Garimella Co-Authored-By: Vladyslav Drok Co-Authored-By: Zhenguo Niu Change-Id: I67495196c3334f51ed034f4ca6e32a3e01a58f15 --- doc/source/webapi/v1.rst | 4 + etc/ironic/ironic.conf.sample | 29 ++++-- ironic/api/controllers/v1/node.py | 54 ++++++++++- ironic/api/controllers/v1/utils.py | 37 ++++++++ ironic/api/controllers/v1/versions.py | 4 +- ironic/common/driver_factory.py | 6 +- ironic/conductor/manager.py | 24 ++++- ironic/tests/unit/api/v1/test_nodes.py | 90 +++++++++++++++++++ ironic/tests/unit/api/v1/test_utils.py | 23 +++++ ironic/tests/unit/conductor/test_manager.py | 45 ++++++++++ ...etwork-interface-api-a3a56b8d0c796d88.yaml | 7 ++ 11 files changed, 310 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/add-network-interface-api-a3a56b8d0c796d88.yaml diff --git a/doc/source/webapi/v1.rst b/doc/source/webapi/v1.rst index d1c6b536e4..0b4f929a9d 100644 --- a/doc/source/webapi/v1.rst +++ b/doc/source/webapi/v1.rst @@ -32,6 +32,10 @@ always requests the newest supported API version. API Versions History -------------------- +**1.20** + + Add node ``network_interface`` field. + **1.19** Add ``local_link_connection`` and ``pxe_enabled`` fields to the port object. diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample index 796358e9fd..cde6ae6815 100644 --- a/etc/ironic/ironic.conf.sample +++ b/etc/ironic/ironic.conf.sample @@ -37,8 +37,11 @@ # recommended set of production-oriented network interfaces. A # complete list of network interfaces present on your system # may be found by enumerating the -# "ironic.hardware.interfaces.network" entrypoint. (list -# value) +# "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. +# (list value) #enabled_network_interfaces = flat,noop # 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 # systems for local boot. (integer value) -# Deprecated group/name - [deploy]/efi_system_partition_size #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 # value) -# Deprecated group/name - [deploy]/dd_block_size #dd_block_size = 1M # Maximum attempts to verify an iSCSI connection is active, # sleeping 1 second between attempts. (integer value) -# Deprecated group/name - [deploy]/iscsi_verify_attempts #iscsi_verify_attempts = 3 @@ -1289,7 +1293,16 @@ # 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 = # API version of the admin Identity API endpoint. (string @@ -1428,12 +1441,12 @@ # (list value) #hash_algorithms = md5 -# Authentication type to load (unknown value) +# Authentication type to load (string value) # Deprecated group/name - [keystone_authtoken]/auth_plugin #auth_type = # Config Section from which to load plugin specific options -# (unknown value) +# (string value) #auth_section = diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py index 303bbd3ca0..021aa4de85 100644 --- a/ironic/api/controllers/v1/node.py +++ b/ironic/api/controllers/v1/node.py @@ -45,6 +45,7 @@ from ironic import objects CONF = cfg.CONF CONF.import_opt('heartbeat_timeout', 'ironic.conductor.manager', group='conductor') +CONF.import_opt('enabled_network_interfaces', 'ironic.common.driver_factory') LOG = log.getLogger(__name__) _CLEAN_STEPS_SCHEMA = { @@ -109,7 +110,12 @@ def get_nodes_controller_reserved_names(): 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: obj.driver_internal_info = wsme.Unset @@ -128,6 +134,9 @@ def hide_fields_in_newer_versions(obj): 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 + def update_state_in_older_versions(obj): """Change provision state names for API backwards compatability. @@ -696,6 +705,9 @@ class Node(base.APIBase): states = wsme.wsattr([link.Link], readonly=True) """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 # 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, inspection_finished_at=None, inspection_started_at=time, 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 # _chassis_uuid variable: 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_for_invalid_state_and_allow_filter(provision_state) api_utils.check_allow_specify_driver(driver) + api_utils.check_allow_specify_network_interface_in_fields(fields) if fields is None: fields = _DEFAULT_RETURN_FIELDS return self._get_nodes_collection(chassis_uuid, instance_uuid, @@ -1213,6 +1227,7 @@ class NodesController(rest.RestController): raise exception.OperationNotPermitted() 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) return Node.convert_with_links(rpc_node, fields=fields) @@ -1226,6 +1241,26 @@ class NodesController(rest.RestController): if self.from_chassis: 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 # and raises NoValidHost if it is not. # We need to ensure that node has a UUID before it can @@ -1265,6 +1300,21 @@ class NodesController(rest.RestController): if self.from_chassis: 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) remove_inst_uuid_patch = [{'op': 'remove', 'path': '/instance_uuid'}] diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py index 70098f4881..51ccef062f 100644 --- a/ironic/api/controllers/v1/utils.py +++ b/ironic/api/controllers/v1/utils.py @@ -240,6 +240,34 @@ def check_allow_specify_fields(fields): 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): min_version = MIN_VERB_VERSIONS.get(verb) 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) +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): """Get reserved names for a given controller. diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py index 9bbfd32c75..d2e75862b1 100644 --- a/ironic/api/controllers/v1/versions.py +++ b/ironic/api/controllers/v1/versions.py @@ -49,6 +49,7 @@ BASE_VERSION = 1 # v1.17: Add 'adopt' verb for ADOPTING active nodes. # v1.18: Add port.internal_info. # v1.19: Add port.local_link_connection and port.pxe_enabled. +# v1.20: Add node.network_interface MINOR_0_JUNO = 0 MINOR_1_INITIAL_VERSION = 1 @@ -70,11 +71,12 @@ MINOR_16_DRIVER_FILTER = 16 MINOR_17_ADOPT_VERB = 17 MINOR_18_PORT_INTERNAL_INFO = 18 MINOR_19_PORT_ADVANCED_NET_FIELDS = 19 +MINOR_20_NETWORK_INTERFACE = 20 # 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 # 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 MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION) diff --git a/ironic/common/driver_factory.py b/ironic/common/driver_factory.py index bd4439f9e4..967ac9252e 100644 --- a/ironic/common/driver_factory.py +++ b/ironic/common/driver_factory.py @@ -51,7 +51,11 @@ driver_opts = [ 'production-oriented network interfaces. A complete ' 'list of network interfaces present on your system may ' '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', help=_('Default network interface to be used for nodes that ' 'do not have network_interface field set. A complete ' diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py index 5b277b8870..28e3487511 100644 --- a/ironic/conductor/manager.py +++ b/ironic/conductor/manager.py @@ -91,7 +91,8 @@ class ConductorManager(base_manager.BaseConductorManager): @messaging.expected_exceptions(exception.InvalidParameterValue, exception.MissingParameterValue, - exception.NodeLocked) + exception.NodeLocked, + exception.InvalidState) def update_node(self, context, node_obj): """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: 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 with task_manager.acquire(context, node_id, shared=False, driver_name=driver_name, diff --git a/ironic/tests/unit/api/v1/test_nodes.py b/ironic/tests/unit/api/v1/test_nodes.py index 483cd069a1..5fa6cf2e6d 100644 --- a/ironic/tests/unit/api/v1/test_nodes.py +++ b/ironic/tests/unit/api/v1/test_nodes.py @@ -110,6 +110,7 @@ class TestListNodes(test_api_base.BaseApiTest): self.assertNotIn('clean_step', data['nodes'][0]) self.assertNotIn('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 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('clean_step', data) self.assertIn('states', data) + self.assertIn('network_interface', data) # never expose the chassis_id self.assertNotIn('chassis_id', data) @@ -206,6 +208,25 @@ class TestListNodes(test_api_base.BaseApiTest): self.assertItemsEqual(['driver_info', 'links'], data) 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): node = obj_utils.create_test_node(self.context, 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('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 self.assertNotIn('chassis_id', data['nodes'][0]) @@ -303,6 +325,17 @@ class TestListNodes(test_api_base.BaseApiTest): headers={api_base.Version.string: "1.7"}) 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): nodes = [] for id in range(5): @@ -1390,6 +1423,35 @@ class TestPatch(test_api_base.BaseApiTest): self.assertEqual('application/json', response.content_type) 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): @@ -1703,6 +1765,34 @@ class TestPost(test_api_base.BaseApiTest): # Assert RPC method wasn't called this time 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): diff --git a/ironic/tests/unit/api/v1/test_utils.py b/ironic/tests/unit/api/v1/test_utils.py index 0e6998bd32..68d896d086 100644 --- a/ironic/tests/unit/api/v1/test_utils.py +++ b/ironic/tests/unit/api/v1/test_utils.py @@ -130,6 +130,22 @@ class TestApiUtils(base.TestCase): self.assertRaises(exception.NotAcceptable, 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']) def test_check_allow_specify_driver(self, mock_request): mock_request.version.minor = 16 @@ -232,6 +248,13 @@ 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()) + class TestNodeIdent(base.TestCase): diff --git a/ironic/tests/unit/conductor/test_manager.py b/ironic/tests/unit/conductor/test_manager.py index 74483b8153..6553afa7dc 100644 --- a/ironic/tests/unit/conductor/test_manager.py +++ b/ironic/tests/unit/conductor/test_manager.py @@ -285,6 +285,51 @@ class UpdateNodeTestCase(mgr_utils.ServiceSetUpMixin, node.refresh() 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 class VendorPassthruTestCase(mgr_utils.ServiceSetUpMixin, diff --git a/releasenotes/notes/add-network-interface-api-a3a56b8d0c796d88.yaml b/releasenotes/notes/add-network-interface-api-a3a56b8d0c796d88.yaml new file mode 100644 index 0000000000..d778eb480e --- /dev/null +++ b/releasenotes/notes/add-network-interface-api-a3a56b8d0c796d88.yaml @@ -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.