Merge "Expose node's network_interface field in API"

This commit is contained in:
Jenkins 2016-07-15 08:27:18 +00:00 committed by Gerrit Code Review
commit cb5988350a
11 changed files with 294 additions and 7 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.