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:
parent
2b6ff862c1
commit
d8bdbda030
@ -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
|
||||||
|
|
||||||
|
@ -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.")
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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):
|
||||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user