Provide hook into process when a node isn't found

Nodes that don't exist in the inspector node cache maybe still be
booted and return inspection information to process, this patch allows
an operator to hook into the point when the not found error is thrown
so that they may handle that information in anyway they decide to.

Change-Id: Ib17491a8201cf534d498c68d916b5b37b07f5ff6
Implements: blueprint node-not-found-hook
This commit is contained in:
Sam Betts 2015-06-10 12:16:02 +01:00
parent 3464210adf
commit 5b969e0a31
9 changed files with 122 additions and 13 deletions

View File

@ -360,6 +360,11 @@
# Deprecated group/name - [discoverd]/always_store_ramdisk_logs
#always_store_ramdisk_logs = false
# The name of the hook to run when inspector receives inspection
# information from a node it isn't already aware of. This hook is
# ignored by default. (string value)
#node_not_found_hook = <None>
[swift]

View File

@ -145,6 +145,11 @@ PROCESSING_OPTS = [
'an error message (dependent upon "ramdisk_logs_dir" option '
'being set).',
deprecated_group='discoverd'),
cfg.StrOpt('node_not_found_hook',
default=None,
help='The name of the hook to run when inspector receives '
'inspection information from a node it isn\'t already '
'aware of. This hook is ignored by default.')
]

View File

@ -256,8 +256,8 @@ def find_node(**attributes):
found.update(item[0] for item in rows)
if not found:
raise utils.Error(_(
'Could not find a node for attributes %s') % attributes, code=404)
raise utils.NotFoundInCacheError(_(
'Could not find a node for attributes %s') % attributes)
elif len(found) > 1:
raise utils.Error(_(
'Multiple matching nodes found for attributes %(attr)s: %(found)s')

View File

@ -17,6 +17,7 @@ import abc
from oslo_config import cfg
import six
from stevedore import driver
from stevedore import named
@ -71,6 +72,7 @@ class ProcessingHook(object): # pragma: no cover
_HOOKS_MGR = None
_NOT_FOUND_HOOK_MGR = None
def processing_hooks_manager(*args):
@ -90,3 +92,15 @@ def processing_hooks_manager(*args):
invoke_args=args,
name_order=True)
return _HOOKS_MGR
def node_not_found_hook_manager(*args):
global _NOT_FOUND_HOOK_MGR
if _NOT_FOUND_HOOK_MGR is None:
name = CONF.processing.node_not_found_hook
if name:
_NOT_FOUND_HOOK_MGR = driver.DriverManager(
'ironic_inspector.hooks.node_not_found',
name=name)
return _NOT_FOUND_HOOK_MGR

View File

@ -29,3 +29,14 @@ class ExampleProcessingHook(base.ProcessingHook): # pragma: no cover
ports_patches, **kwargs):
LOG.debug('before_update: %s (node %s)', introspection_data,
node_info.uuid)
def example_not_found_hook(self, introspection_data):
"""Hook to run when the node cache query returns not found.
:param node_info: raw information sent by the ramdisk, shouldn't be
modified by this hook.
:returns: NodeInfo object representing a newly cached node
object or None
"""
LOG.debug('Processing node not found %s', introspection_data)

View File

@ -31,6 +31,31 @@ _CREDENTIALS_WAIT_RETRIES = 10
_CREDENTIALS_WAIT_PERIOD = 3
def _find_node_info(introspection_data, failures):
try:
return node_cache.find_node(
bmc_address=introspection_data.get('ipmi_address'),
mac=introspection_data.get('macs'))
except utils.NotFoundInCacheError as exc:
not_found_hook = plugins_base.node_not_found_hook_manager()
if not_found_hook is None:
failures.append(_('Look up error: %s') % exc)
return
# NOTE(sambetts): If not_found_hook is not none it means that we were
# unable to find the node in the node cache and there is a node not
# found hook defined so we should try to send the introspection data
# to that hook to generate the node info before bubbling up the error.
try:
node_info = not_found_hook.driver(introspection_data)
if node_info:
return node_info
failures.append(_("Node not found hook returned nothing"))
except Exception as exc:
failures.append(_("Node not found hook failed: %s") % exc)
except utils.Error as exc:
failures.append(_('Look up error: %s') % exc)
def process(introspection_data):
"""Process data from the ramdisk.
@ -56,16 +81,7 @@ def process(introspection_data):
failures.append(_('Unexpected exception during preprocessing '
'in hook %s') % hook_ext.name)
try:
node_info = node_cache.find_node(
bmc_address=introspection_data.get('ipmi_address'),
mac=introspection_data.get('macs'))
except utils.Error as exc:
if failures:
failures.append(_('Look up error: %s') % exc)
node_info = None
else:
raise
node_info = _find_node_info(introspection_data, failures)
if failures and node_info:
msg = _('The following failures happened during running '
@ -75,7 +91,7 @@ def process(introspection_data):
}
node_info.finished(error=_('Data pre-processing failed'))
raise utils.Error(msg)
elif failures:
elif not node_info:
msg = _('The following failures happened during running '
'pre-processing hooks for unknown node:\n%(failures)s') % {
'failures': '\n'.join(failures)

View File

@ -304,6 +304,54 @@ class TestProcess(BaseTest):
self.assertFalse(pop_mock.return_value.finished.called)
@prepare_mocks
def test_error_if_node_not_found_hook(self, cli, pop_mock, process_mock):
plugins_base._NOT_FOUND_HOOK_MGR = None
pop_mock.side_effect = utils.NotFoundInCacheError('BOOM')
self.assertRaisesRegexp(utils.Error,
'Look up error: BOOM',
process.process, self.data)
@prepare_mocks
def test_node_not_found_hook_run_ok(self, cli, pop_mock, process_mock):
CONF.set_override('node_not_found_hook', 'example', 'processing')
plugins_base._NOT_FOUND_HOOK_MGR = None
pop_mock.side_effect = utils.NotFoundInCacheError('BOOM')
with mock.patch.object(example_plugin,
'example_not_found_hook') as hook_mock:
hook_mock.return_value = node_cache.NodeInfo(
uuid=self.node.uuid,
started_at=self.started_at)
res = process.process(self.data)
self.assertEqual(self.fake_result_json, res)
hook_mock.assert_called_once_with(self.data)
@prepare_mocks
def test_node_not_found_hook_run_none(self, cli, pop_mock, process_mock):
CONF.set_override('node_not_found_hook', 'example', 'processing')
plugins_base._NOT_FOUND_HOOK_MGR = None
pop_mock.side_effect = utils.NotFoundInCacheError('BOOM')
with mock.patch.object(example_plugin,
'example_not_found_hook') as hook_mock:
hook_mock.return_value = None
self.assertRaisesRegexp(utils.Error,
'Node not found hook returned nothing',
process.process, self.data)
hook_mock.assert_called_once_with(self.data)
@prepare_mocks
def test_node_not_found_hook_exception(self, cli, pop_mock, process_mock):
CONF.set_override('node_not_found_hook', 'example', 'processing')
plugins_base._NOT_FOUND_HOOK_MGR = None
pop_mock.side_effect = utils.NotFoundInCacheError('BOOM')
with mock.patch.object(example_plugin,
'example_not_found_hook') as hook_mock:
hook_mock.side_effect = Exception('Hook Error')
self.assertRaisesRegexp(utils.Error,
'Node not found hook failed: Hook Error',
process.process, self.data)
hook_mock.assert_called_once_with(self.data)
@mock.patch.object(node_cache.NodeInfo, 'invalidate_cache', lambda self: None)
@mock.patch.object(utils, 'spawn_n',

View File

@ -46,6 +46,13 @@ class Error(Exception):
self.http_code = code
class NotFoundInCacheError(Error):
"""Exception when node was not found in cache during processing."""
def __init__(self, msg, code=404):
super(NotFoundInCacheError, self).__init__(msg, code)
def spawn_n(*args, **kwargs):
global GREEN_POOL
if not GREEN_POOL:

View File

@ -42,6 +42,9 @@ setup(
"extra_hardware = ironic_inspector.plugins.extra_hardware:ExtraHardwareHook",
"root_device_hint = ironic_inspector.plugins.root_device_hint:RootDeviceHintHook",
],
'ironic_inspector.hooks.node_not_found': [
"example = ironic_inspector.plugins.example:example_not_found_hook",
],
'openstack.cli.extension': [
'baremetal-introspection = ironic_inspector.shell',
],