No node interface settings for classic drivers

Classic drivers have their interfaces (except for
network and storage) pre-determined. Unlike dynamic
drivers, it is not possible to change these interfaces
for nodes with a classic driver. If that is attempted
(when creating or updating a node), an exception
MustBeNone is raised (and HTTP status 400 is returned).

So that we don't break existing ironic clusters that
have nodes with classic drivers and interfaces specified,
a warning is logged (instead of raising an exception)
when a TaskManager is created.

Change-Id: I290b10f735d0da9710d1ee3b50c3252f73956428
Partial-Bug: #1524745
This commit is contained in:
Ruby Loo 2017-02-07 20:44:18 +00:00
parent 2b6ff862c1
commit d8bdbda030
6 changed files with 135 additions and 10 deletions

View File

@ -55,7 +55,17 @@ def build_driver_for_task(task, driver_name=None):
driver_name = driver_name or node.driver driver_name = driver_name or node.driver
driver_or_hw_type = get_driver_or_hardware_type(driver_name) driver_or_hw_type = get_driver_or_hardware_type(driver_name)
check_and_update_node_interfaces(node, driver_or_hw_type=driver_or_hw_type) try:
check_and_update_node_interfaces(
node, driver_or_hw_type=driver_or_hw_type)
except exception.MustBeNone as e:
# NOTE(rloo). This was raised because nodes with classic drivers
# cannot have any interfaces (except for network and
# storage) set. However, there was a small window
# where this was possible so instead of breaking those
# users totally, we'll spam them with warnings instead.
LOG.warning(_LW('%s They will be ignored. To avoid this warning, '
'please set them to None.'), e)
bare_driver = driver_base.BareDriver() bare_driver = driver_base.BareDriver()
_attach_interfaces_to_driver(bare_driver, node, driver_or_hw_type) _attach_interfaces_to_driver(bare_driver, node, driver_or_hw_type)
@ -225,6 +235,8 @@ def check_and_update_node_interfaces(node, driver_or_hw_type=None):
:raises: NoValidDefaultForInterface if the default value cannot be :raises: NoValidDefaultForInterface if the default value cannot be
calculated and is not provided in the configuration calculated and is not provided in the configuration
:raises: DriverNotFound if the node's driver or hardware type is not found :raises: DriverNotFound if the node's driver or hardware type is not found
:raises: MustBeNone if one or more of the node's interface
fields were specified when they should not be.
""" """
if driver_or_hw_type is None: if driver_or_hw_type is None:
driver_or_hw_type = get_driver_or_hardware_type(node.driver) driver_or_hw_type = get_driver_or_hardware_type(node.driver)
@ -237,6 +249,25 @@ def check_and_update_node_interfaces(node, driver_or_hw_type=None):
# Only network and storage interfaces are dynamic for classic drivers # Only network and storage interfaces are dynamic for classic drivers
factories = ['network', 'storage'] factories = ['network', 'storage']
# These are interfaces that cannot be specified via the node. E.g.,
# for classic drivers, none are allowed except for network & storage.
not_allowed_ifaces = driver_base.ALL_INTERFACES - set(factories)
bad_interface_fields = []
for iface in not_allowed_ifaces:
field_name = '%s_interface' % iface
# NOTE(dtantsur): objects raise NotImplementedError on accessing fields
# that are known, but missing from an object. Thus, we cannot just use
# getattr(node, field_name, None) here.
if field_name in node:
impl_name = getattr(node, field_name)
if impl_name is not None:
bad_interface_fields.append(field_name)
if bad_interface_fields:
raise exception.MustBeNone(node=node.uuid, driver=node.driver,
node_fields=','.join(bad_interface_fields))
# Result - whether the node object was modified # Result - whether the node object was modified
result = False result = False

View File

@ -350,6 +350,11 @@ class NoValidDefaultForInterface(InvalidParameterValue):
"value found for %(interface_type)s interface.") "value found for %(interface_type)s interface.")
class MustBeNone(InvalidParameterValue):
_msg_fmt = _("For node %(node)s with driver %(driver)s, these node "
"fields must be set to None: %(node_fields)s.")
class ImageNotFound(NotFound): class ImageNotFound(NotFound):
_msg_fmt = _("Image %(image_id)s could not be found.") _msg_fmt = _("Image %(image_id)s could not be found.")

View File

@ -96,10 +96,12 @@ class ConductorManager(base_manager.BaseConductorManager):
self.power_state_sync_count = collections.defaultdict(int) self.power_state_sync_count = collections.defaultdict(int)
@METRICS.timer('ConductorManager.create_node') @METRICS.timer('ConductorManager.create_node')
# No need to add these since they are subclasses of InvalidParameterValue:
# InterfaceNotFoundInEntrypoint
# IncompatibleInterface,
# NoValidDefaultForInterface
# MustBeNone
@messaging.expected_exceptions(exception.InvalidParameterValue, @messaging.expected_exceptions(exception.InvalidParameterValue,
exception.InterfaceNotFoundInEntrypoint,
exception.IncompatibleInterface,
exception.NoValidDefaultForInterface,
exception.DriverNotFound) exception.DriverNotFound)
def create_node(self, context, node_obj): def create_node(self, context, node_obj):
"""Create a node in database. """Create a node in database.
@ -114,6 +116,8 @@ class ConductorManager(base_manager.BaseConductorManager):
:raises: NoValidDefaultForInterface if no default can be calculated :raises: NoValidDefaultForInterface if no default can be calculated
for some interfaces, and explicit values must be provided. for some interfaces, and explicit values must be provided.
:raises: InvalidParameterValue if some fields fail validation. :raises: InvalidParameterValue if some fields fail validation.
:raises: MustBeNone if one or more of the node's interface
fields were specified when they should not be.
:raises: DriverNotFound if the driver or hardware type is not found. :raises: DriverNotFound if the driver or hardware type is not found.
""" """
LOG.debug("RPC create_node called for node %s.", node_obj.uuid) LOG.debug("RPC create_node called for node %s.", node_obj.uuid)
@ -122,12 +126,14 @@ class ConductorManager(base_manager.BaseConductorManager):
return node_obj return node_obj
@METRICS.timer('ConductorManager.update_node') @METRICS.timer('ConductorManager.update_node')
# No need to add these since they are subclasses of InvalidParameterValue:
# InterfaceNotFoundInEntrypoint
# IncompatibleInterface,
# NoValidDefaultForInterface
# MustBeNone
@messaging.expected_exceptions(exception.InvalidParameterValue, @messaging.expected_exceptions(exception.InvalidParameterValue,
exception.NodeLocked, exception.NodeLocked,
exception.InvalidState, exception.InvalidState,
exception.InterfaceNotFoundInEntrypoint,
exception.IncompatibleInterface,
exception.NoValidDefaultForInterface,
exception.DriverNotFound) exception.DriverNotFound)
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.
@ -140,7 +146,8 @@ class ConductorManager(base_manager.BaseConductorManager):
:param node_obj: a changed (but not saved) node object. :param node_obj: a changed (but not saved) node object.
:raises: NoValidDefaultForInterface if no default can be calculated :raises: NoValidDefaultForInterface if no default can be calculated
for some interfaces, and explicit values must be provided. for some interfaces, and explicit values must be provided.
:raises: MustBeNone if one or more of the node's interface
fields were specified when they should not be.
""" """
node_id = node_obj.uuid node_id = node_obj.uuid
LOG.debug("RPC update_node called for node %s.", node_id) LOG.debug("RPC update_node called for node %s.", node_id)

View File

@ -1815,6 +1815,22 @@ class TestPatch(test_api_base.BaseApiTest):
self.assertEqual('application/json', response.content_type) self.assertEqual('application/json', response.content_type)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code) self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
def test_update_classic_driver_interface_fields(self):
headers = {api_base.Version.string: '1.31'}
self.mock_update_node.side_effect = (
exception.MustBeNone('error'))
for field in api_utils.V31_FIELDS:
node = obj_utils.create_test_node(self.context,
uuid=uuidutils.generate_uuid())
response = self.patch_json('/nodes/%s' % node.uuid,
[{'path': '/%s' % field,
'value': 'fake',
'op': 'add'}],
headers=headers,
expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
self.assertEqual('application/json', response.content_type)
def _create_node_locally(node): def _create_node_locally(node):
driver_factory.check_and_update_node_interfaces(node) driver_factory.check_and_update_node_interfaces(node)
@ -1890,10 +1906,13 @@ class TestPost(test_api_base.BaseApiTest):
def test_create_node_specify_interfaces(self): def test_create_node_specify_interfaces(self):
headers = {api_base.Version.string: '1.31'} headers = {api_base.Version.string: '1.31'}
for field in api_utils.V31_FIELDS:
cfg.CONF.set_override('enabled_%ss' % field, ['fake'])
for field in api_utils.V31_FIELDS: for field in api_utils.V31_FIELDS:
node = { node = {
'uuid': uuidutils.generate_uuid(), 'uuid': uuidutils.generate_uuid(),
field: 'fake' field: 'fake',
'driver': 'fake-hardware'
} }
result = self._test_create_node(headers=headers, **node) result = self._test_create_node(headers=headers, **node)
self.assertEqual('fake', result[field]) self.assertEqual('fake', result[field])
@ -1906,6 +1925,21 @@ class TestPost(test_api_base.BaseApiTest):
expect_errors=True) expect_errors=True)
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int) self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_int)
def test_create_node_classic_driver_specify_interface(self):
headers = {api_base.Version.string: '1.31'}
for field in api_utils.V31_FIELDS:
node = {
'uuid': uuidutils.generate_uuid(),
field: 'fake',
}
ndict = test_api_utils.post_get_test_node(**node)
response = self.post_json('/nodes', ndict,
headers=headers,
expect_errors=True)
self.assertEqual(http_client.BAD_REQUEST, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
def test_create_node_name_empty_invalid(self): def test_create_node_name_empty_invalid(self):
ndict = test_api_utils.post_get_test_node(name='') ndict = test_api_utils.post_get_test_node(name='')
response = self.post_json('/nodes', ndict, response = self.post_json('/nodes', ndict,

View File

@ -13,6 +13,7 @@
# under the License. # under the License.
import mock import mock
from oslo_utils import uuidutils
from stevedore import dispatch from stevedore import dispatch
from ironic.common import driver_factory from ironic.common import driver_factory
@ -32,7 +33,7 @@ class FakeEp(object):
name = 'fake' name = 'fake'
class DriverLoadTestCase(base.TestCase): class DriverLoadTestCase(db_base.DbTestCase):
def _fake_init_name_err(self, *args, **kwargs): def _fake_init_name_err(self, *args, **kwargs):
kwargs['on_load_failure_callback'](None, FakeEp, NameError('aaa')) kwargs['on_load_failure_callback'](None, FakeEp, NameError('aaa'))
@ -91,6 +92,33 @@ class DriverLoadTestCase(base.TestCase):
['fake'], driver_factory.DriverFactory._extension_manager.names()) ['fake'], driver_factory.DriverFactory._extension_manager.names())
self.assertTrue(mock_warn.called) self.assertTrue(mock_warn.called)
def test_build_driver_for_task(self):
node = obj_utils.create_test_node(self.context, driver='fake')
with task_manager.acquire(self.context, node.id) as task:
for iface in drivers_base.ALL_INTERFACES:
impl = getattr(task.driver, iface)
self.assertIsNotNone(impl)
@mock.patch.object(driver_factory, '_attach_interfaces_to_driver',
autospec=True)
@mock.patch.object(driver_factory.LOG, 'warning', autospec=True)
def test_build_driver_for_task_incorrect(self, mock_warn, mock_attach):
# Cannot set these node interfaces for classic driver
no_set_interfaces = (drivers_base.ALL_INTERFACES -
set(['network', 'storage']))
for iface in no_set_interfaces:
iface_name = '%s_interface' % iface
node_kwargs = {'uuid': uuidutils.generate_uuid(),
iface_name: 'fake'}
node = obj_utils.create_test_node(self.context, driver='fake',
**node_kwargs)
with task_manager.acquire(self.context, node.id) as task:
mock_warn.assert_called_once_with(mock.ANY, mock.ANY)
mock_warn.reset_mock()
mock_attach.assert_called_once_with(mock.ANY, task.node,
mock.ANY)
mock_attach.reset_mock()
class WarnUnsupportedDriversTestCase(base.TestCase): class WarnUnsupportedDriversTestCase(base.TestCase):
@mock.patch.object(driver_factory.LOG, 'warning', autospec=True) @mock.patch.object(driver_factory.LOG, 'warning', autospec=True)
@ -258,6 +286,21 @@ class CheckAndUpdateNodeInterfacesTestCase(db_base.DbTestCase):
driver_factory.check_and_update_node_interfaces, driver_factory.check_and_update_node_interfaces,
node) node)
def test_classic_setting_interfaces(self):
# Cannot set these node interfaces for classic driver
no_set_interfaces = (drivers_base.ALL_INTERFACES -
set(['network', 'storage']))
for iface in no_set_interfaces:
iface_name = '%s_interface' % iface
node_kwargs = {'uuid': uuidutils.generate_uuid(),
iface_name: 'fake'}
node = obj_utils.create_test_node(self.context, driver='fake',
**node_kwargs)
self.assertRaisesRegex(
exception.InvalidParameterValue,
'driver fake.*%s' % iface_name,
driver_factory.check_and_update_node_interfaces, node)
class DefaultInterfaceTestCase(db_base.DbTestCase): class DefaultInterfaceTestCase(db_base.DbTestCase):
def setUp(self): def setUp(self):

View File

@ -0,0 +1,5 @@
---
fixes:
- |
Nodes with classic drivers cannot have any interfaces (except for network
and storage) specified. HTTP status 400 is returned in these cases.