Pass reset_interfaces when updating a driver from the rules

Otherwise updating a driver requires listing all interfaces with their
new values. This patch is designed to be backportable, so it has
a fall-back to the previous behavior if the required API version and/or
ironicclient versions are not available.

Story: #2005148
Task: #29854
Depends-On: https://review.openstack.org/#/c/643264/
Change-Id: I115b92b9d7bcdbb1c03f0d259dbba6efd2baec2a
This commit is contained in:
Dmitry Tantsur 2019-03-06 13:20:32 +01:00
parent b087e94d81
commit ada6b106c5
7 changed files with 85 additions and 6 deletions

View File

@ -447,6 +447,7 @@ elif [[ "$1" == "stack" && "$2" == "test-config" ]]; then
if [ -n "$IRONIC_INSPECTOR_NODE_NOT_FOUND_HOOK" ]; then if [ -n "$IRONIC_INSPECTOR_NODE_NOT_FOUND_HOOK" ]; then
iniset $TEMPEST_CONFIG baremetal_introspection auto_discovery_feature True iniset $TEMPEST_CONFIG baremetal_introspection auto_discovery_feature True
iniset $TEMPEST_CONFIG baremetal_introspection auto_discovery_default_driver fake-hardware iniset $TEMPEST_CONFIG baremetal_introspection auto_discovery_default_driver fake-hardware
iniset $TEMPEST_CONFIG baremetal_introspection auto_discovery_target_driver ipmi
fi fi
fi fi
fi fi

View File

@ -31,11 +31,12 @@ VALID_STATES = {'enroll', 'manageable', 'inspecting', 'inspect wait',
'inspect failed'} 'inspect failed'}
# 1.38 is the latest API version in the Queens release series, 10.1.0. # 1.38 is the latest API version in the Queens release series, 10.1.0.
# 1.46 is the latest API version in the Rocky release series, 11.1.0.
# NOTE(mgoddard): This should be updated with each release to ensure that # NOTE(mgoddard): This should be updated with each release to ensure that
# inspector is able to use the latest ironic API. In particular, this version # inspector is able to use the latest ironic API. In particular, this version
# is used when processing introspection rules, and is the default version used # is used when processing introspection rules, and is the default version used
# by processing plugins. # by processing plugins.
DEFAULT_IRONIC_API_VERSION = '1.38' DEFAULT_IRONIC_API_VERSION = ['1.38', '1.46']
IRONIC_SESSION = None IRONIC_SESSION = None

View File

@ -409,13 +409,14 @@ class NodeInfo(object):
else: else:
self._ports[mac] = port self._ports[mac] = port
def patch(self, patches, ironic=None): def patch(self, patches, ironic=None, **kwargs):
"""Apply JSON patches to a node. """Apply JSON patches to a node.
Refreshes cached node instance. Refreshes cached node instance.
:param patches: JSON patches to apply :param patches: JSON patches to apply
:param ironic: Ironic client to use instead of self.ironic :param ironic: Ironic client to use instead of self.ironic
:param kwargs: Arguments to pass to ironicclient.
:raises: ironicclient exceptions :raises: ironicclient exceptions
""" """
ironic = ironic or self.ironic ironic = ironic or self.ironic
@ -426,7 +427,7 @@ class NodeInfo(object):
patch['path'] = '/' + patch['path'] patch['path'] = '/' + patch['path']
LOG.debug('Updating node with patches %s', patches, node_info=self) LOG.debug('Updating node with patches %s', patches, node_info=self)
self._node = ironic.node.update(self.uuid, patches) self._node = ironic.node.update(self.uuid, patches, **kwargs)
def patch_port(self, port, patches, ironic=None): def patch_port(self, port, patches, ironic=None):
"""Apply JSON patches to a port. """Apply JSON patches to a port.

View File

@ -16,6 +16,7 @@
import operator import operator
import re import re
from ironicclient import exceptions as ironic_exc
import netaddr import netaddr
from ironic_inspector.common.i18n import _ from ironic_inspector.common.i18n import _
@ -23,6 +24,9 @@ from ironic_inspector.plugins import base
from ironic_inspector import utils from ironic_inspector import utils
LOG = utils.getProcessingLogger(__name__)
def coerce(value, expected): def coerce(value, expected):
if isinstance(expected, float): if isinstance(expected, float):
return float(value) return float(value)
@ -117,15 +121,36 @@ class FailAction(base.RuleActionPlugin):
class SetAttributeAction(base.RuleActionPlugin): class SetAttributeAction(base.RuleActionPlugin):
# NOTE(iurygregory): set as optional to accept None as value, check # NOTE(iurygregory): set as optional to accept None as value, check
# that the key 'value' is present, otherwise will raise ValueError. # that the key 'value' is present, otherwise will raise ValueError.
OPTIONAL_PARAMS = {'value'} OPTIONAL_PARAMS = {'value', 'reset_interfaces'}
REQUIRED_PARAMS = {'path'} REQUIRED_PARAMS = {'path'}
# TODO(dtantsur): proper validation of path # TODO(dtantsur): proper validation of path
FORMATTED_PARAMS = ['value'] FORMATTED_PARAMS = ['value']
def apply(self, node_info, params, **kwargs): def apply(self, node_info, params, **kwargs):
node_info.patch([{'op': 'add', 'path': params['path'], kwargs = {}
'value': params['value']}]) if (params['path'].strip('/') == 'driver'
and ('reset_interfaces' not in params
or params['reset_interfaces'])):
# Using at least Rocky
kwargs['reset_interfaces'] = True
try:
node_info.patch([{'op': 'add', 'path': params['path'],
'value': params['value']}], **kwargs)
except (TypeError, ironic_exc.NotAcceptable):
# TODO(dtantsur): remove support for old ironicclient and Queens
if 'reset_interfaces' in params:
# An explicit request, report an error.
raise utils.Error(
_('Cannot pass reset_interfaces to set-attribute, '
'requires API 1.46 and ironicclient >= 2.5.0'))
LOG.warning('Not passing reset_interfaces to Ironic, since '
' API 1.46 and/or ironicclient >= 2.5.0 are '
'not available', node_info=node_info)
node_info.patch([{'op': 'add', 'path': params['path'],
'value': params['value']}])
def validate(self, params, **kwargs): def validate(self, params, **kwargs):
if 'value' in params: if 'value' in params:

View File

@ -666,6 +666,16 @@ class TestUpdate(test_base.NodeTest):
self.ironic.node.update.assert_called_once_with(self.uuid, patch) self.ironic.node.update.assert_called_once_with(self.uuid, patch)
self.assertIs(mock.sentinel.node, self.node_info.node()) self.assertIs(mock.sentinel.node, self.node_info.node())
def test_patch_with_args(self):
self.ironic.node.update.return_value = mock.sentinel.node
self.node_info.patch([{'patch': 'patch'}], reset_interfaces=True)
self.ironic.node.update.assert_called_once_with(self.uuid,
[{'patch': 'patch'}],
reset_interfaces=True)
self.assertIs(mock.sentinel.node, self.node_info.node())
def test_update_properties(self): def test_update_properties(self):
self.ironic.node.update.return_value = mock.sentinel.node self.ironic.node.update.return_value = mock.sentinel.node

View File

@ -168,6 +168,37 @@ class TestSetAttributeAction(test_base.NodeTest):
'path': '/extra/value', 'path': '/extra/value',
'value': 42}]) 'value': 42}])
@mock.patch.object(node_cache.NodeInfo, 'patch')
def test_apply_driver(self, mock_patch):
params = {'path': '/driver', 'value': 'ipmi'}
self.act.apply(self.node_info, params)
mock_patch.assert_called_once_with([{'op': 'add',
'path': '/driver',
'value': 'ipmi'}],
reset_interfaces=True)
@mock.patch.object(node_cache.NodeInfo, 'patch')
def test_apply_driver_no_reset_interfaces(self, mock_patch):
params = {'path': '/driver', 'value': 'ipmi',
'reset_interfaces': False}
self.act.apply(self.node_info, params)
mock_patch.assert_called_once_with([{'op': 'add',
'path': '/driver',
'value': 'ipmi'}])
@mock.patch.object(node_cache.NodeInfo, 'patch')
def test_apply_driver_not_supported(self, mock_patch):
for exc in (TypeError, exceptions.NotAcceptable):
mock_patch.reset_mock()
mock_patch.side_effect = [exc, None]
params = {'path': '/driver', 'value': 'ipmi'}
self.act.apply(self.node_info, params)
mock_patch.assert_has_calls([
mock.call([{'op': 'add', 'path': '/driver', 'value': 'ipmi'}],
reset_interfaces=True),
mock.call([{'op': 'add', 'path': '/driver', 'value': 'ipmi'}])
])
@mock.patch('ironic_inspector.common.ironic.get_client', new=mock.Mock()) @mock.patch('ironic_inspector.common.ironic.get_client', new=mock.Mock())
class TestSetCapabilityAction(test_base.NodeTest): class TestSetCapabilityAction(test_base.NodeTest):

View File

@ -0,0 +1,10 @@
---
upgrade:
- |
The ``set-attribute`` action now automatically sets ``reset_interfaces`` to
``True`` if the driver is updated. If it's not desired, set it explicitly
to ``False``.
fixes:
- |
Fixes updating a driver with the ``set-attribute`` introspection rule
action by providing ``reset_interfaces``.