Foundation for boot/network management for in-band inspection
This change the required base driver interface additions and inspector interface changes to support in-band inspection driven by ironic. Change-Id: Ibf9a80d0f72d5f128bf46ddca4cb9762c9a8191b Story: #1528920 Task: #10404
This commit is contained in:
parent
6582471c05
commit
c5dfa1bd9f
@ -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
|
||||||
|
@ -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.')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,13 +216,28 @@ 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)
|
||||||
|
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)
|
eventlet.spawn_n(_start_inspection, task.node.uuid, task.context)
|
||||||
return states.INSPECTWAIT
|
return states.INSPECTWAIT
|
||||||
|
|
||||||
@ -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')
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user