Merge "Foundation for boot/network management for in-band inspection"

This commit is contained in:
Zuul 2019-11-26 10:50:27 +00:00 committed by Gerrit Code Review
commit 65133ae982
7 changed files with 546 additions and 26 deletions

View File

@ -532,3 +532,31 @@ def validate_conductor_group(conductor_group):
raise exception.InvalidConductorGroup(group=conductor_group) raise exception.InvalidConductorGroup(group=conductor_group)
if not re.match(r'^[a-zA-Z0-9_\-\.]*$', conductor_group): if not re.match(r'^[a-zA-Z0-9_\-\.]*$', conductor_group):
raise exception.InvalidConductorGroup(group=conductor_group) raise exception.InvalidConductorGroup(group=conductor_group)
def set_node_nested_field(node, collection, field, value):
"""Set a value in a dictionary field of a node.
:param node: Node object.
:param collection: Name of the field with the dictionary.
:param field: Nested field name.
:param value: New value.
"""
col = getattr(node, collection)
col[field] = value
setattr(node, collection, col)
def pop_node_nested_field(node, collection, field, default=None):
"""Pop a value from a dictionary field of a node.
:param node: Node object.
:param collection: Name of the field with the dictionary.
:param field: Nested field name.
:param default: The default value to return.
:return: The removed value or the default.
"""
col = getattr(node, collection)
result = col.pop(field, default)
setattr(node, collection, col)
return result

View File

@ -21,6 +21,23 @@ opts = [
cfg.IntOpt('status_check_period', default=60, cfg.IntOpt('status_check_period', default=60,
help=_('period (in seconds) to check status of nodes ' help=_('period (in seconds) to check status of nodes '
'on inspection')), 'on inspection')),
cfg.StrOpt('extra_kernel_params', default='',
help=_('extra kernel parameters to pass to the inspection '
'ramdisk when boot is managed by ironic (not '
'ironic-inspector). Pairs key=value separated by '
'spaces.')),
cfg.BoolOpt('power_off', default=True,
help=_('whether to power off a node after inspection '
'finishes')),
cfg.StrOpt('callback_endpoint_override',
help=_('endpoint to use as a callback for posting back '
'introspection data when boot is managed by ironic. '
'Standard keystoneauth options are used by default.')),
cfg.BoolOpt('require_managed_boot', default=False,
help=_('require that the in-band inspection boot is fully '
'managed by ironic. Set this to True if your '
'installation of ironic-inspector does not have a '
'separate PXE boot environment.')),
] ]

View File

@ -559,6 +559,17 @@ class BootInterface(BaseInterface):
raise exception.UnsupportedDriverExtension( raise exception.UnsupportedDriverExtension(
driver=task.node.driver, extension='validate_rescue') driver=task.node.driver, extension='validate_rescue')
def validate_inspection(self, task):
"""Validate that the node has required properties for inspection.
:param task: A TaskManager instance with the node being checked
:raises: MissingParameterValue if node is missing one or more required
parameters
:raises: UnsupportedDriverExtension
"""
raise exception.UnsupportedDriverExtension(
driver=task.node.driver, extension='validate_inspection')
class PowerInterface(BaseInterface): class PowerInterface(BaseInterface):
"""Interface for power-related actions.""" """Interface for power-related actions."""
@ -1493,6 +1504,38 @@ class NetworkInterface(BaseInterface):
""" """
pass pass
def validate_inspection(self, task):
"""Validate that the node has required properties for inspection.
:param task: A TaskManager instance with the node being checked
:raises: MissingParameterValue if node is missing one or more required
parameters
:raises: UnsupportedDriverExtension
"""
raise exception.UnsupportedDriverExtension(
driver=task.node.driver, extension='validate_inspection')
def add_inspection_network(self, task):
"""Add the inspection network to the node.
:param task: A TaskManager instance.
:returns: a dictionary in the form {port.uuid: neutron_port['id']}
:raises: NetworkError
:raises: InvalidParameterValue, if the network interface configuration
is invalid.
"""
return {}
def remove_inspection_network(self, task):
"""Removes the inspection network from a node.
:param task: A TaskManager instance.
:raises: NetworkError
:raises: InvalidParameterValue, if the network interface configuration
is invalid.
:raises: MissingParameterValue, if some parameters are missing.
"""
def need_power_on(self, task): def need_power_on(self, task):
"""Check if ironic node must be powered on before applying network changes """Check if ironic node must be powered on before applying network changes

View File

@ -15,6 +15,8 @@ Modules required to work with ironic_inspector:
https://pypi.org/project/ironic-inspector https://pypi.org/project/ironic-inspector
""" """
import shlex
import eventlet import eventlet
from futurist import periodics from futurist import periodics
import openstack import openstack
@ -24,7 +26,9 @@ from ironic.common import exception
from ironic.common.i18n import _ from ironic.common.i18n import _
from ironic.common import keystone from ironic.common import keystone
from ironic.common import states from ironic.common import states
from ironic.common import utils
from ironic.conductor import task_manager from ironic.conductor import task_manager
from ironic.conductor import utils as cond_utils
from ironic.conf import CONF from ironic.conf import CONF
from ironic.drivers import base from ironic.drivers import base
@ -32,6 +36,8 @@ from ironic.drivers import base
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
_INSPECTOR_SESSION = None _INSPECTOR_SESSION = None
# Internal field to mark whether ironic or inspector manages boot for the node
_IRONIC_MANAGES_BOOT = 'inspector_manage_boot'
def _get_inspector_session(**kwargs): def _get_inspector_session(**kwargs):
@ -62,6 +68,124 @@ def _get_client(context):
oslo_conf=conf).baremetal_introspection oslo_conf=conf).baremetal_introspection
def _get_callback_endpoint(client):
root = CONF.inspector.callback_endpoint_override or client.get_endpoint()
if root == 'mdns':
return root
root = root.rstrip('/')
# NOTE(dtantsur): the IPA side is quite picky about the exact format.
if root.endswith('/v1'):
return '%s/continue' % root
else:
return '%s/v1/continue' % root
def _tear_down_managed_boot(task):
errors = []
ironic_manages_boot = utils.pop_node_nested_field(
task.node, 'driver_internal_info', _IRONIC_MANAGES_BOOT)
if not ironic_manages_boot:
return errors
try:
task.driver.boot.clean_up_ramdisk(task)
except Exception as exc:
errors.append(_('unable to clean up ramdisk boot: %s') % exc)
LOG.exception('Unable to clean up ramdisk boot for node %s',
task.node.uuid)
try:
task.driver.network.remove_inspection_network(task)
except Exception as exc:
errors.append(_('unable to remove inspection ports: %s') % exc)
LOG.exception('Unable to remove inspection network for node %s',
task.node.uuid)
if CONF.inspector.power_off:
try:
cond_utils.node_power_action(task, states.POWER_OFF)
except Exception as exc:
errors.append(_('unable to power off the node: %s') % exc)
LOG.exception('Unable to power off node %s', task.node.uuid)
return errors
def _inspection_error_handler(task, error, raise_exc=False, clean_up=True):
if clean_up:
_tear_down_managed_boot(task)
task.node.last_error = error
if raise_exc:
task.node.save()
raise exception.HardwareInspectionFailure(error=error)
else:
task.process_event('fail')
def _ironic_manages_boot(task, raise_exc=False):
"""Whether ironic should manage boot for this node."""
try:
task.driver.boot.validate_inspection(task)
except exception.UnsupportedDriverExtension as e:
LOG.debug('The boot interface %(iface)s of the node %(node)s does '
'not support managed boot for in-band inspection or '
'the required options are not populated: %(exc)s',
{'node': task.node.uuid,
'iface': task.node.boot_interface,
'exc': e})
if raise_exc:
raise
return False
try:
task.driver.network.validate_inspection(task)
except exception.UnsupportedDriverExtension as e:
LOG.debug('The network interface %(iface)s of the node %(node)s does '
'not support managed boot for in-band inspection or '
'the required options are not populated: %(exc)s',
{'node': task.node.uuid,
'iface': task.node.network_interface,
'exc': e})
if raise_exc:
raise
return False
return True
def _parse_kernel_params():
"""Parse kernel params from the configuration."""
result = {}
for s in shlex.split(CONF.inspector.extra_kernel_params):
try:
key, value = s.split('=', 1)
except ValueError:
raise exception.InvalidParameterValue(
_('Invalid key-value pair in extra_kernel_params: %s') % s)
result[key] = value
return result
def _start_managed_inspection(task):
"""Start inspection managed by ironic."""
try:
client = _get_client(task.context)
endpoint = _get_callback_endpoint(client)
params = dict(_parse_kernel_params(),
**{'ipa-inspection-callback-url': endpoint})
task.driver.network.add_inspection_network(task)
task.driver.boot.prepare_ramdisk(task, ramdisk_params=params)
client.start_introspection(task.node.uuid, manage_boot=False)
cond_utils.node_power_action(task, states.REBOOT)
except Exception as exc:
LOG.exception('Unable to start managed inspection for node %(uuid)s: '
'%(err)s', {'uuid': task.node.uuid, 'err': exc})
error = _('unable to start inspection: %s') % exc
_inspection_error_handler(task, error, raise_exc=True)
class Inspector(base.InspectInterface): class Inspector(base.InspectInterface):
"""In-band inspection via ironic-inspector project.""" """In-band inspection via ironic-inspector project."""
@ -78,10 +202,11 @@ class Inspector(base.InspectInterface):
If invalid, raises an exception; otherwise returns None. If invalid, raises an exception; otherwise returns None.
:param task: a task from TaskManager. :param task: a task from TaskManager.
:raises: UnsupportedDriverExtension
""" """
# NOTE(deva): this is not callable if inspector is disabled _parse_kernel_params()
# so don't raise an exception -- just pass. if CONF.inspector.require_managed_boot:
pass _ironic_manages_boot(task, raise_exc=True)
def inspect_hardware(self, task): def inspect_hardware(self, task):
"""Inspect hardware to obtain the hardware properties. """Inspect hardware to obtain the hardware properties.
@ -91,14 +216,29 @@ class Inspector(base.InspectInterface):
:param task: a task from TaskManager. :param task: a task from TaskManager.
:returns: states.INSPECTWAIT :returns: states.INSPECTWAIT
:raises: HardwareInspectionFailure on failure
""" """
LOG.debug('Starting inspection for node %(uuid)s using ' ironic_manages_boot = _ironic_manages_boot(
'ironic-inspector', {'uuid': task.node.uuid}) task, raise_exc=CONF.inspector.require_managed_boot)
# NOTE(dtantsur): we're spawning a short-living green thread so that utils.set_node_nested_field(task.node, 'driver_internal_info',
# we can release a lock as soon as possible and allow ironic-inspector _IRONIC_MANAGES_BOOT,
# to operate on a node. ironic_manages_boot)
eventlet.spawn_n(_start_inspection, task.node.uuid, task.context) task.node.save()
LOG.debug('Starting inspection for node %(uuid)s using '
'ironic-inspector, booting is managed by %(project)s',
{'uuid': task.node.uuid,
'project': 'ironic' if ironic_manages_boot
else 'ironic-inspector'})
if ironic_manages_boot:
_start_managed_inspection(task)
else:
# NOTE(dtantsur): spawning a short-living green thread so that
# we can release a lock as soon as possible and allow
# ironic-inspector to operate on the node.
eventlet.spawn_n(_start_inspection, task.node.uuid, task.context)
return states.INSPECTWAIT return states.INSPECTWAIT
def abort(self, task): def abort(self, task):
@ -133,16 +273,16 @@ def _start_inspection(node_uuid, context):
try: try:
_get_client(context).start_introspection(node_uuid) _get_client(context).start_introspection(node_uuid)
except Exception as exc: except Exception as exc:
LOG.exception('Exception during contacting ironic-inspector ' LOG.error('Error contacting ironic-inspector for inspection of node '
'for inspection of node %(node)s: %(err)s', '%(node)s: %(cls)s: %(err)s',
{'node': node_uuid, 'err': exc}) {'node': node_uuid, 'cls': type(exc).__name__, 'err': exc})
# NOTE(dtantsur): if acquire fails our last option is to rely on # NOTE(dtantsur): if acquire fails our last option is to rely on
# timeout # timeout
lock_purpose = 'recording hardware inspection error' lock_purpose = 'recording hardware inspection error'
with task_manager.acquire(context, node_uuid, with task_manager.acquire(context, node_uuid,
purpose=lock_purpose) as task: purpose=lock_purpose) as task:
task.node.last_error = _('Failed to start inspection: %s') % exc error = _('Failed to start inspection: %s') % exc
task.process_event('fail') _inspection_error_handler(task, error)
else: else:
LOG.info('Node %s was sent to inspection to ironic-inspector', LOG.info('Node %s was sent to inspection to ironic-inspector',
node_uuid) node_uuid)
@ -180,9 +320,21 @@ def _check_status(task):
if status.error: if status.error:
LOG.error('Inspection failed for node %(uuid)s with error: %(err)s', LOG.error('Inspection failed for node %(uuid)s with error: %(err)s',
{'uuid': node.uuid, 'err': status.error}) {'uuid': node.uuid, 'err': status.error})
node.last_error = (_('ironic-inspector inspection failed: %s') error = _('ironic-inspector inspection failed: %s') % status.error
% status.error) _inspection_error_handler(task, error)
task.process_event('fail')
elif status.is_finished: elif status.is_finished:
LOG.info('Inspection finished successfully for node %s', node.uuid) _clean_up(task)
def _clean_up(task):
errors = _tear_down_managed_boot(task)
if errors:
errors = ', '.join(errors)
LOG.error('Inspection clean up failed for node %(uuid)s: %(err)s',
{'uuid': task.node.uuid, 'err': errors})
msg = _('Inspection clean up failed: %s') % errors
_inspection_error_handler(task, msg, raise_exc=False, clean_up=False)
else:
LOG.info('Inspection finished successfully for node %s',
task.node.uuid)
task.process_event('done') task.process_event('done')

View File

@ -119,3 +119,10 @@ class NoopNetwork(base.NetworkInterface):
:param task: A TaskManager instance. :param task: A TaskManager instance.
""" """
pass pass
def validate_inspection(self, task):
"""Validate that the node has required properties for inspection.
:param task: A TaskManager instance with the node being checked
"""
pass

View File

@ -15,13 +15,18 @@ import mock
import openstack import openstack
from ironic.common import context from ironic.common import context
from ironic.common import exception
from ironic.common import states from ironic.common import states
from ironic.common import utils
from ironic.conductor import task_manager from ironic.conductor import task_manager
from ironic.drivers.modules import inspector from ironic.drivers.modules import inspector
from ironic.tests.unit.db import base as db_base from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.objects import utils as obj_utils from ironic.tests.unit.objects import utils as obj_utils
CONF = inspector.CONF
@mock.patch('ironic.common.keystone.get_auth', autospec=True, @mock.patch('ironic.common.keystone.get_auth', autospec=True,
return_value=mock.sentinel.auth) return_value=mock.sentinel.auth)
@mock.patch('ironic.common.keystone.get_session', autospec=True, @mock.patch('ironic.common.keystone.get_session', autospec=True,
@ -57,14 +62,17 @@ class GetClientTestCase(db_base.DbTestCase):
class BaseTestCase(db_base.DbTestCase): class BaseTestCase(db_base.DbTestCase):
def setUp(self): def setUp(self):
super(BaseTestCase, self).setUp() super(BaseTestCase, self).setUp()
self.node = obj_utils.get_test_node(self.context, self.node = obj_utils.create_test_node(self.context,
inspect_interface='inspector') inspect_interface='inspector')
self.iface = inspector.Inspector() self.iface = inspector.Inspector()
self.task = mock.MagicMock(spec=task_manager.TaskManager) self.task = mock.MagicMock(spec=task_manager.TaskManager)
self.task.context = self.context self.task.context = self.context
self.task.shared = False self.task.shared = False
self.task.node = self.node self.task.node = self.node
self.task.driver = mock.Mock(spec=['inspect'], inspect=self.iface) self.task.driver = mock.Mock(
spec=['boot', 'network', 'inspect', 'power'],
inspect=self.iface)
self.driver = self.task.driver
class CommonFunctionsTestCase(BaseTestCase): class CommonFunctionsTestCase(BaseTestCase):
@ -75,25 +83,165 @@ class CommonFunctionsTestCase(BaseTestCase):
res = self.iface.get_properties() res = self.iface.get_properties()
self.assertEqual({}, res) self.assertEqual({}, res)
def test_get_callback_endpoint(self):
for catalog_endp in ['http://192.168.0.42:5050',
'http://192.168.0.42:5050/v1',
'http://192.168.0.42:5050/']:
client = mock.Mock()
client.get_endpoint.return_value = catalog_endp
self.assertEqual('http://192.168.0.42:5050/v1/continue',
inspector._get_callback_endpoint(client))
def test_get_callback_endpoint_override(self):
CONF.set_override('callback_endpoint_override', 'http://url',
group='inspector')
client = mock.Mock()
self.assertEqual('http://url/v1/continue',
inspector._get_callback_endpoint(client))
self.assertFalse(client.get_endpoint.called)
def test_get_callback_endpoint_mdns(self):
CONF.set_override('callback_endpoint_override', 'mdns',
group='inspector')
client = mock.Mock()
self.assertEqual('mdns', inspector._get_callback_endpoint(client))
self.assertFalse(client.get_endpoint.called)
@mock.patch.object(eventlet, 'spawn_n', lambda f, *a, **kw: f(*a, **kw)) @mock.patch.object(eventlet, 'spawn_n', lambda f, *a, **kw: f(*a, **kw))
@mock.patch('ironic.drivers.modules.inspector._get_client', autospec=True) @mock.patch('ironic.drivers.modules.inspector._get_client', autospec=True)
class InspectHardwareTestCase(BaseTestCase): class InspectHardwareTestCase(BaseTestCase):
def test_ok(self, mock_client): def test_validate_ok(self, mock_client):
self.iface.validate(self.task)
def test_validate_invalid_kernel_params(self, mock_client):
CONF.set_override('extra_kernel_params', 'abcdef', group='inspector')
self.assertRaises(exception.InvalidParameterValue,
self.iface.validate, self.task)
def test_validate_require_managed_boot(self, mock_client):
CONF.set_override('require_managed_boot', True, group='inspector')
self.driver.boot.validate_inspection.side_effect = (
exception.UnsupportedDriverExtension(''))
self.assertRaises(exception.UnsupportedDriverExtension,
self.iface.validate, self.task)
def test_unmanaged_ok(self, mock_client):
self.driver.boot.validate_inspection.side_effect = (
exception.UnsupportedDriverExtension(''))
mock_introspect = mock_client.return_value.start_introspection mock_introspect = mock_client.return_value.start_introspection
self.assertEqual(states.INSPECTWAIT, self.assertEqual(states.INSPECTWAIT,
self.iface.inspect_hardware(self.task)) self.iface.inspect_hardware(self.task))
mock_introspect.assert_called_once_with(self.node.uuid) mock_introspect.assert_called_once_with(self.node.uuid)
self.assertFalse(self.driver.boot.prepare_ramdisk.called)
self.assertFalse(self.driver.network.add_inspection_network.called)
self.assertFalse(self.driver.power.reboot.called)
self.assertFalse(self.driver.network.remove_inspection_network.called)
self.assertFalse(self.driver.boot.clean_up_ramdisk.called)
self.assertFalse(self.driver.power.set_power_state.called)
@mock.patch.object(task_manager, 'acquire', autospec=True) @mock.patch.object(task_manager, 'acquire', autospec=True)
def test_error(self, mock_acquire, mock_client): def test_unmanaged_error(self, mock_acquire, mock_client):
mock_acquire.return_value.__enter__.return_value = self.task
self.driver.boot.validate_inspection.side_effect = (
exception.UnsupportedDriverExtension(''))
mock_introspect = mock_client.return_value.start_introspection mock_introspect = mock_client.return_value.start_introspection
mock_introspect.side_effect = RuntimeError('boom') mock_introspect.side_effect = RuntimeError('boom')
self.iface.inspect_hardware(self.task) self.iface.inspect_hardware(self.task)
mock_introspect.assert_called_once_with(self.node.uuid) mock_introspect.assert_called_once_with(self.node.uuid)
task = mock_acquire.return_value.__enter__.return_value self.assertIn('boom', self.task.node.last_error)
self.assertIn('boom', task.node.last_error) self.task.process_event.assert_called_once_with('fail')
task.process_event.assert_called_once_with('fail') self.assertFalse(self.driver.boot.prepare_ramdisk.called)
self.assertFalse(self.driver.network.add_inspection_network.called)
self.assertFalse(self.driver.network.remove_inspection_network.called)
self.assertFalse(self.driver.boot.clean_up_ramdisk.called)
self.assertFalse(self.driver.power.set_power_state.called)
def test_require_managed_boot(self, mock_client):
CONF.set_override('require_managed_boot', True, group='inspector')
self.driver.boot.validate_inspection.side_effect = (
exception.UnsupportedDriverExtension(''))
mock_introspect = mock_client.return_value.start_introspection
self.assertRaises(exception.UnsupportedDriverExtension,
self.iface.inspect_hardware, self.task)
self.assertFalse(mock_introspect.called)
self.assertFalse(self.driver.boot.prepare_ramdisk.called)
self.assertFalse(self.driver.network.add_inspection_network.called)
self.assertFalse(self.driver.power.reboot.called)
self.assertFalse(self.driver.network.remove_inspection_network.called)
self.assertFalse(self.driver.boot.clean_up_ramdisk.called)
self.assertFalse(self.driver.power.set_power_state.called)
def test_managed_ok(self, mock_client):
endpoint = 'http://192.169.0.42:5050/v1'
mock_client.return_value.get_endpoint.return_value = endpoint
mock_introspect = mock_client.return_value.start_introspection
self.assertEqual(states.INSPECTWAIT,
self.iface.inspect_hardware(self.task))
mock_introspect.assert_called_once_with(self.node.uuid,
manage_boot=False)
self.driver.boot.prepare_ramdisk.assert_called_once_with(
self.task, ramdisk_params={
'ipa-inspection-callback-url': endpoint + '/continue',
})
self.driver.network.add_inspection_network.assert_called_once_with(
self.task)
self.driver.power.reboot.assert_called_once_with(
self.task, timeout=None)
self.assertFalse(self.driver.network.remove_inspection_network.called)
self.assertFalse(self.driver.boot.clean_up_ramdisk.called)
self.assertFalse(self.driver.power.set_power_state.called)
def test_managed_custom_params(self, mock_client):
CONF.set_override('extra_kernel_params',
'ipa-inspection-collectors=default,logs '
'ipa-collect-dhcp=1',
group='inspector')
endpoint = 'http://192.169.0.42:5050/v1'
mock_client.return_value.get_endpoint.return_value = endpoint
mock_introspect = mock_client.return_value.start_introspection
self.iface.validate(self.task)
self.assertEqual(states.INSPECTWAIT,
self.iface.inspect_hardware(self.task))
mock_introspect.assert_called_once_with(self.node.uuid,
manage_boot=False)
self.driver.boot.prepare_ramdisk.assert_called_once_with(
self.task, ramdisk_params={
'ipa-inspection-callback-url': endpoint + '/continue',
'ipa-inspection-collectors': 'default,logs',
'ipa-collect-dhcp': '1',
})
self.driver.network.add_inspection_network.assert_called_once_with(
self.task)
self.driver.power.reboot.assert_called_once_with(
self.task, timeout=None)
self.assertFalse(self.driver.network.remove_inspection_network.called)
self.assertFalse(self.driver.boot.clean_up_ramdisk.called)
self.assertFalse(self.driver.power.set_power_state.called)
@mock.patch.object(task_manager, 'acquire', autospec=True)
def test_managed_error(self, mock_acquire, mock_client):
endpoint = 'http://192.169.0.42:5050/v1'
mock_client.return_value.get_endpoint.return_value = endpoint
mock_acquire.return_value.__enter__.return_value = self.task
mock_introspect = mock_client.return_value.start_introspection
mock_introspect.side_effect = RuntimeError('boom')
self.assertRaises(exception.HardwareInspectionFailure,
self.iface.inspect_hardware, self.task)
mock_introspect.assert_called_once_with(self.node.uuid,
manage_boot=False)
self.assertIn('boom', self.task.node.last_error)
self.driver.boot.prepare_ramdisk.assert_called_once_with(
self.task, ramdisk_params={
'ipa-inspection-callback-url': endpoint + '/continue',
})
self.driver.network.add_inspection_network.assert_called_once_with(
self.task)
self.driver.network.remove_inspection_network.assert_called_once_with(
self.task)
self.driver.boot.clean_up_ramdisk.assert_called_once_with(self.task)
self.driver.power.set_power_state.assert_called_once_with(
self.task, 'power off', timeout=None)
@mock.patch('ironic.drivers.modules.inspector._get_client', autospec=True) @mock.patch('ironic.drivers.modules.inspector._get_client', autospec=True)
@ -144,6 +292,43 @@ class CheckStatusTestCase(BaseTestCase):
inspector._check_status(self.task) inspector._check_status(self.task)
mock_get.assert_called_once_with(self.node.uuid) mock_get.assert_called_once_with(self.node.uuid)
self.task.process_event.assert_called_once_with('done') self.task.process_event.assert_called_once_with('done')
self.assertFalse(self.driver.network.remove_inspection_network.called)
self.assertFalse(self.driver.boot.clean_up_ramdisk.called)
self.assertFalse(self.driver.power.set_power_state.called)
def test_status_ok_managed(self, mock_client):
utils.set_node_nested_field(self.node, 'driver_internal_info',
'inspector_manage_boot', True)
self.node.save()
mock_get = mock_client.return_value.get_introspection
mock_get.return_value = mock.Mock(is_finished=True,
error=None,
spec=['is_finished', 'error'])
inspector._check_status(self.task)
mock_get.assert_called_once_with(self.node.uuid)
self.task.process_event.assert_called_once_with('done')
self.driver.network.remove_inspection_network.assert_called_once_with(
self.task)
self.driver.boot.clean_up_ramdisk.assert_called_once_with(self.task)
self.driver.power.set_power_state.assert_called_once_with(
self.task, 'power off', timeout=None)
def test_status_ok_managed_no_power_off(self, mock_client):
CONF.set_override('power_off', False, group='inspector')
utils.set_node_nested_field(self.node, 'driver_internal_info',
'inspector_manage_boot', True)
self.node.save()
mock_get = mock_client.return_value.get_introspection
mock_get.return_value = mock.Mock(is_finished=True,
error=None,
spec=['is_finished', 'error'])
inspector._check_status(self.task)
mock_get.assert_called_once_with(self.node.uuid)
self.task.process_event.assert_called_once_with('done')
self.driver.network.remove_inspection_network.assert_called_once_with(
self.task)
self.driver.boot.clean_up_ramdisk.assert_called_once_with(self.task)
self.assertFalse(self.driver.power.set_power_state.called)
def test_status_error(self, mock_client): def test_status_error(self, mock_client):
mock_get = mock_client.return_value.get_introspection mock_get = mock_client.return_value.get_introspection
@ -154,6 +339,75 @@ class CheckStatusTestCase(BaseTestCase):
mock_get.assert_called_once_with(self.node.uuid) mock_get.assert_called_once_with(self.node.uuid)
self.task.process_event.assert_called_once_with('fail') self.task.process_event.assert_called_once_with('fail')
self.assertIn('boom', self.node.last_error) self.assertIn('boom', self.node.last_error)
self.assertFalse(self.driver.network.remove_inspection_network.called)
self.assertFalse(self.driver.boot.clean_up_ramdisk.called)
self.assertFalse(self.driver.power.set_power_state.called)
def test_status_error_managed(self, mock_client):
utils.set_node_nested_field(self.node, 'driver_internal_info',
'inspector_manage_boot', True)
self.node.save()
mock_get = mock_client.return_value.get_introspection
mock_get.return_value = mock.Mock(is_finished=True,
error='boom',
spec=['is_finished', 'error'])
inspector._check_status(self.task)
mock_get.assert_called_once_with(self.node.uuid)
self.task.process_event.assert_called_once_with('fail')
self.assertIn('boom', self.node.last_error)
self.driver.network.remove_inspection_network.assert_called_once_with(
self.task)
self.driver.boot.clean_up_ramdisk.assert_called_once_with(self.task)
self.driver.power.set_power_state.assert_called_once_with(
self.task, 'power off', timeout=None)
def test_status_error_managed_no_power_off(self, mock_client):
CONF.set_override('power_off', False, group='inspector')
utils.set_node_nested_field(self.node, 'driver_internal_info',
'inspector_manage_boot', True)
self.node.save()
mock_get = mock_client.return_value.get_introspection
mock_get.return_value = mock.Mock(is_finished=True,
error='boom',
spec=['is_finished', 'error'])
inspector._check_status(self.task)
mock_get.assert_called_once_with(self.node.uuid)
self.task.process_event.assert_called_once_with('fail')
self.assertIn('boom', self.node.last_error)
self.driver.network.remove_inspection_network.assert_called_once_with(
self.task)
self.driver.boot.clean_up_ramdisk.assert_called_once_with(self.task)
self.assertFalse(self.driver.power.set_power_state.called)
def _test_status_clean_up_failed(self, mock_client):
utils.set_node_nested_field(self.node, 'driver_internal_info',
'inspector_manage_boot', True)
self.node.save()
mock_get = mock_client.return_value.get_introspection
mock_get.return_value = mock.Mock(is_finished=True,
error=None,
spec=['is_finished', 'error'])
inspector._check_status(self.task)
mock_get.assert_called_once_with(self.node.uuid)
self.task.process_event.assert_called_once_with('fail')
self.assertIn('boom', self.node.last_error)
def test_status_boot_clean_up_failed(self, mock_client):
self.driver.boot.clean_up_ramdisk.side_effect = RuntimeError('boom')
self._test_status_clean_up_failed(mock_client)
self.driver.boot.clean_up_ramdisk.assert_called_once_with(self.task)
def test_status_network_clean_up_failed(self, mock_client):
self.driver.network.remove_inspection_network.side_effect = \
RuntimeError('boom')
self._test_status_clean_up_failed(mock_client)
self.driver.network.remove_inspection_network.assert_called_once_with(
self.task)
self.driver.boot.clean_up_ramdisk.assert_called_once_with(self.task)
@mock.patch('ironic.drivers.modules.inspector._get_client', autospec=True) @mock.patch('ironic.drivers.modules.inspector._get_client', autospec=True)

View File

@ -0,0 +1,19 @@
---
features:
- |
It's now possible to force booting for in-band inspection to be managed
by ironic by setting the new ``[inspector]require_managed_boot`` option
to ``True``. In-band inspection will fail if the node's driver does not
support managing boot for it.
other:
- |
Boot and network interface implementations can now manage boot for in-band
inspection by implementing the new methods:
* ``BootInterface.validate_inspection``
* ``NetworkInterface.validate_inspection``
* ``NetworkInterface.add_inspection_network``
* ``NetworkInterface.remove_inspection_network``
Previously only ironic-inspector itself could manage boot for it. This
change opens a way for non-PXE implementations of in-band inspection.