Merge "Expose node's network_interface field in API"
This commit is contained in:
commit
cb5988350a
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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'}]
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 '
|
||||
|
@ -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,
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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…
Reference in New Issue
Block a user