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:
Vasyl Saienko 2016-05-17 13:59:50 +03:00 committed by Devananda van der Veen
parent 6d846590bc
commit c62e1bee29
11 changed files with 310 additions and 13 deletions

View File

@ -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.

View File

@ -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>

View File

@ -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'}]

View File

@ -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.

View File

@ -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)

View File

@ -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 '

View File

@ -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,

View File

@ -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):

View File

@ -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):

View File

@ -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,

View File

@ -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.