Add the initial skeleton of the agent inspect interface

No real inspection is done: it only accepts data and returns success.

Common code has been extracted from the existing inspector-based
implementation.

Change-Id: I7462bb2e0449fb1098fe59e394b5c583fea89bac
This commit is contained in:
Dmitry Tantsur 2023-03-15 13:06:39 +01:00
parent 349b1f7c41
commit 6efa2119e4
10 changed files with 286 additions and 55 deletions

View File

@ -115,6 +115,7 @@ def continue_inspection(task, inventory, plugin_data):
node = task.node
LOG.debug('Inventory for node %(node)s: %(data)s',
{'node': node.uuid, 'data': inventory})
plugin_data = plugin_data or {}
try:
result = task.driver.inspect.continue_inspection(

View File

@ -17,6 +17,19 @@ from oslo_config import cfg
from ironic.common.i18n import _
from ironic.conf import auth
VALID_ADD_PORTS_VALUES = {
'all': _('all MAC addresses'),
'active': _('MAC addresses of NIC\'s with IP addresses'),
'pxe': _('only the MAC address of the PXE NIC'),
'disabled': _('do not create any ports'),
}
VALID_KEEP_PORTS_VALUES = {
'all': _('keep even ports with MAC\'s not present in the inventory'),
'present': _('keep only ports with MAC\'s present in the inventory'),
'added': _('keep only ports determined by the add_ports option'),
}
opts = [
cfg.IntOpt('status_check_period', default=60,
help=_('period (in seconds) to check status of nodes '

View File

@ -58,7 +58,7 @@ class GenericHardware(hardware_type.AbstractHardwareType):
"""List of supported inspect interfaces."""
# Inspector support should be the default if it's enabled by an
# operator (implying that the service is installed).
return [inspector.Inspector, noop.NoInspect]
return [inspector.Inspector, inspector.AgentInspect, noop.NoInspect]
@property
def supported_network_interfaces(self):

View File

@ -18,7 +18,6 @@ of FUJITSU PRIMERGY servers, and above servers.
from ironic.drivers import generic
from ironic.drivers.modules import agent
from ironic.drivers.modules import inspector
from ironic.drivers.modules import ipmitool
from ironic.drivers.modules import ipxe
from ironic.drivers.modules.irmc import bios
@ -61,8 +60,7 @@ class IRMCHardware(generic.GenericHardware):
@property
def supported_inspect_interfaces(self):
"""List of supported inspect interfaces."""
return [inspect.IRMCInspect, inspector.Inspector,
noop.NoInspect]
return [inspect.IRMCInspect] + super().supported_inspect_interfaces
@property
def supported_management_interfaces(self):

View File

@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from ironic.drivers.modules.inspector.agent import AgentInspect
from ironic.drivers.modules.inspector.interface import Inspector
__all__ = ['Inspector']
__all__ = ['AgentInspect', 'Inspector']

View File

@ -0,0 +1,83 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
In-band inspection implementation.
"""
from oslo_log import log as logging
from ironic.common import boot_devices
from ironic.common.i18n import _
from ironic.common import states
from ironic.conductor import utils as cond_utils
from ironic.drivers.modules import deploy_utils
from ironic.drivers.modules import inspect_utils
from ironic.drivers.modules.inspector import interface as common
LOG = logging.getLogger(__name__)
class AgentInspect(common.Common):
"""In-band inspection."""
def _start_managed_inspection(self, task):
"""Start inspection managed by ironic."""
ep = deploy_utils.get_ironic_api_url().rstrip('/')
if ep.endswith('/v1'):
ep = f'{ep}/continue_inspection'
else:
ep = f'{ep}/v1/continue_inspection'
common.prepare_managed_inspection(task, ep)
cond_utils.node_power_action(task, states.POWER_ON)
def _start_unmanaged_inspection(self, task):
"""Start unmanaged inspection."""
try:
cond_utils.node_power_action(task, states.POWER_OFF)
# Only network boot is supported for unmanaged inspection.
cond_utils.node_set_boot_device(task, boot_devices.PXE,
persistent=False)
cond_utils.node_power_action(task, states.POWER_ON)
except Exception as exc:
LOG.exception('Unable to start unmanaged inspection for node '
'%(uuid)s: %(err)s',
{'uuid': task.node.uuid, 'err': exc})
error = _('unable to start inspection: %s') % exc
common.inspection_error_handler(task, error, raise_exc=True,
clean_up=False)
def abort(self, task):
"""Abort hardware inspection.
:param task: a task from TaskManager.
"""
error = _("By request, the inspection operation has been aborted")
inspect_utils.clear_lookup_addresses(task.node)
common.inspection_error_handler(task, error, raise_exc=False,
clean_up=True)
def continue_inspection(self, task, inventory, plugin_data):
"""Continue in-band hardware inspection.
This implementation simply defers to ironic-inspector. It only exists
to simplify the transition to Ironic-native in-band inspection.
:param task: a task from TaskManager.
:param inventory: hardware inventory from the node.
:param plugin_data: optional plugin-specific data.
"""
# TODO(dtantsur): migrate the whole pipeline from ironic-inspector
LOG.error('Meaningful inspection is not implemented yet in the "agent"'
' inspect interface')
common.clean_up(task, finish=False)

View File

@ -62,7 +62,7 @@ def _get_callback_endpoint(client):
parts.query, parts.fragment))
def _tear_down_managed_boot(task):
def tear_down_managed_boot(task):
errors = []
ironic_manages_boot = utils.pop_node_nested_field(
@ -94,9 +94,9 @@ def _tear_down_managed_boot(task):
return errors
def _inspection_error_handler(task, error, raise_exc=False, clean_up=True):
def inspection_error_handler(task, error, raise_exc=False, clean_up=True):
if clean_up:
_tear_down_managed_boot(task)
tear_down_managed_boot(task)
task.node.last_error = error
if raise_exc:
@ -106,7 +106,7 @@ def _inspection_error_handler(task, error, raise_exc=False, clean_up=True):
task.process_event('fail')
def _ironic_manages_boot(task, raise_exc=False):
def ironic_manages_boot(task, raise_exc=False):
"""Whether ironic should manage boot for this node."""
try:
task.driver.boot.validate_inspection(task)
@ -137,32 +137,21 @@ def _ironic_manages_boot(task, raise_exc=False):
return True
def _start_managed_inspection(task):
"""Start inspection managed by ironic."""
try:
cli = client.get_client(task.context)
endpoint = _get_callback_endpoint(cli)
params = dict(
utils.parse_kernel_params(CONF.inspector.extra_kernel_params),
**{'ipa-inspection-callback-url': endpoint})
if utils.fast_track_enabled(task.node):
params['ipa-api-url'] = deploy_utils.get_ironic_api_url()
def prepare_managed_inspection(task, endpoint):
"""Prepare the boot interface for managed inspection."""
params = dict(
utils.parse_kernel_params(CONF.inspector.extra_kernel_params),
**{'ipa-inspection-callback-url': endpoint})
if utils.fast_track_enabled(task.node):
params['ipa-api-url'] = deploy_utils.get_ironic_api_url()
cond_utils.node_power_action(task, states.POWER_OFF)
with cond_utils.power_state_for_network_configuration(task):
task.driver.network.add_inspection_network(task)
task.driver.boot.prepare_ramdisk(task, ramdisk_params=params)
cli.start_introspection(task.node.uuid, manage_boot=False)
cond_utils.node_power_action(task, states.POWER_ON)
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)
cond_utils.node_power_action(task, states.POWER_OFF)
with cond_utils.power_state_for_network_configuration(task):
task.driver.network.add_inspection_network(task)
task.driver.boot.prepare_ramdisk(task, ramdisk_params=params)
class Inspector(base.InspectInterface):
"""In-band inspection via ironic-inspector project."""
class Common(base.InspectInterface):
def __init__(self):
super().__init__()
@ -189,7 +178,7 @@ class Inspector(base.InspectInterface):
"""
utils.parse_kernel_params(CONF.inspector.extra_kernel_params)
if CONF.inspector.require_managed_boot:
_ironic_manages_boot(task, raise_exc=True)
ironic_manages_boot(task, raise_exc=True)
def inspect_hardware(self, task):
"""Inspect hardware to obtain the hardware properties.
@ -207,12 +196,11 @@ class Inspector(base.InspectInterface):
LOG.debug('Pre-creating ports prior to inspection not supported'
' on node %s.', task.node.uuid)
ironic_manages_boot = _ironic_manages_boot(
manage_boot = ironic_manages_boot(
task, raise_exc=CONF.inspector.require_managed_boot)
utils.set_node_nested_field(task.node, 'driver_internal_info',
_IRONIC_MANAGES_BOOT,
ironic_manages_boot)
_IRONIC_MANAGES_BOOT, manage_boot)
# Make this interface work with the Ironic own /continue_inspection
# endpoint to simplify migration to the new in-band inspection
# implementation.
@ -222,18 +210,40 @@ class Inspector(base.InspectInterface):
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'})
'project': 'ironic' if manage_boot else 'ironic-inspector'})
if ironic_manages_boot:
_start_managed_inspection(task)
if manage_boot:
try:
self._start_managed_inspection(task)
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)
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)
self._start_unmanaged_inspection(task)
return states.INSPECTWAIT
class Inspector(Common):
"""In-band inspection via ironic-inspector project."""
def _start_managed_inspection(self, task):
"""Start inspection managed by ironic."""
cli = client.get_client(task.context)
endpoint = _get_callback_endpoint(cli)
prepare_managed_inspection(task, endpoint)
cli.start_introspection(task.node.uuid, manage_boot=False)
cond_utils.node_power_action(task, states.POWER_ON)
def _start_unmanaged_inspection(self, task):
"""Call to inspector to start inspection."""
# 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)
def abort(self, task):
"""Abort hardware inspection.
@ -253,7 +263,8 @@ class Inspector(base.InspectInterface):
)
def _periodic_check_result(self, task, manager, context):
"""Periodic task checking results of inspection."""
_check_status(task)
if isinstance(task.driver.inspect, self.__class__):
_check_status(task)
def continue_inspection(self, task, inventory, plugin_data=None):
"""Continue in-band hardware inspection.
@ -288,7 +299,7 @@ def _start_inspection(node_uuid, context):
with task_manager.acquire(context, node_uuid,
purpose=lock_purpose) as task:
error = _('Failed to start inspection: %s') % exc
_inspection_error_handler(task, error)
inspection_error_handler(task, error)
else:
LOG.info('Node %s was sent to inspection to ironic-inspector',
node_uuid)
@ -330,9 +341,9 @@ def _check_status(task):
LOG.error('Inspection failed for node %(uuid)s with error: %(err)s',
{'uuid': node.uuid, 'err': status.error})
error = _('ironic-inspector inspection failed: %s') % status.error
_inspection_error_handler(task, error)
inspection_error_handler(task, error)
elif status.is_finished:
_clean_up(task)
clean_up(task)
if CONF.inventory.data_backend == 'none':
LOG.debug('Inspection data storage is disabled, the data will '
'not be saved for node %s', node.uuid)
@ -346,15 +357,15 @@ def _check_status(task):
task.context)
def _clean_up(task):
errors = _tear_down_managed_boot(task)
def clean_up(task, finish=True):
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:
inspection_error_handler(task, msg, raise_exc=False, clean_up=False)
elif finish:
LOG.info('Inspection finished successfully for node %s',
task.node.uuid)
task.process_event('done')

View File

@ -15,7 +15,6 @@
from ironic.drivers import generic
from ironic.drivers.modules import agent
from ironic.drivers.modules import inspector
from ironic.drivers.modules import ipxe
from ironic.drivers.modules import noop
from ironic.drivers.modules import noop_mgmt
@ -50,8 +49,8 @@ class RedfishHardware(generic.GenericHardware):
@property
def supported_inspect_interfaces(self):
"""List of supported power interfaces."""
return [redfish_inspect.RedfishInspect, inspector.Inspector,
noop.NoInspect]
return ([redfish_inspect.RedfishInspect]
+ super().supported_inspect_interfaces)
@property
def supported_boot_interfaces(self):

View File

@ -0,0 +1,124 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from unittest import mock
from ironic.common import boot_devices
from ironic.common import exception
from ironic.common import states
from ironic.conductor import task_manager
from ironic.conf import CONF
from ironic.drivers.modules import deploy_utils
from ironic.drivers.modules import inspect_utils
from ironic.drivers.modules.inspector import agent as inspector
from ironic.drivers.modules.inspector import interface as common
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.objects import utils as obj_utils
@mock.patch.object(inspect_utils, 'create_ports_if_not_exist', autospec=True)
class InspectHardwareTestCase(db_base.DbTestCase):
def setUp(self):
super().setUp()
self.node = obj_utils.create_test_node(self.context,
inspect_interface='agent')
self.iface = inspector.AgentInspect()
self.task = mock.MagicMock(spec=task_manager.TaskManager)
self.task.context = self.context
self.task.shared = False
self.task.node = self.node
self.task.driver = mock.Mock(
spec=['boot', 'network', 'inspect', 'power', 'management'],
inspect=self.iface)
self.driver = self.task.driver
def test_unmanaged_ok(self, mock_create_ports_if_not_exist):
self.driver.boot.validate_inspection.side_effect = (
exception.UnsupportedDriverExtension(''))
self.assertEqual(states.INSPECTWAIT,
self.iface.inspect_hardware(self.task))
mock_create_ports_if_not_exist.assert_called_once_with(self.task)
self.assertFalse(self.driver.boot.prepare_ramdisk.called)
self.assertFalse(self.driver.network.add_inspection_network.called)
self.driver.management.set_boot_device.assert_called_once_with(
self.task, device=boot_devices.PXE, persistent=False)
self.driver.power.set_power_state.assert_has_calls([
mock.call(self.task, states.POWER_OFF, timeout=None),
mock.call(self.task, states.POWER_ON, timeout=None),
])
self.assertFalse(self.driver.network.remove_inspection_network.called)
self.assertFalse(self.driver.boot.clean_up_ramdisk.called)
@mock.patch.object(deploy_utils, 'get_ironic_api_url', autospec=True)
def test_managed_ok(self, mock_get_url, mock_create_ports_if_not_exist):
endpoint = 'http://192.169.0.42:6385/v1'
mock_get_url.return_value = endpoint
self.assertEqual(states.INSPECTWAIT,
self.iface.inspect_hardware(self.task))
self.driver.boot.prepare_ramdisk.assert_called_once_with(
self.task, ramdisk_params={
'ipa-inspection-callback-url':
f'{endpoint}/continue_inspection',
})
self.driver.network.add_inspection_network.assert_called_once_with(
self.task)
self.driver.power.set_power_state.assert_has_calls([
mock.call(self.task, states.POWER_OFF, timeout=None),
mock.call(self.task, states.POWER_ON, timeout=None),
])
self.assertFalse(self.driver.network.remove_inspection_network.called)
self.assertFalse(self.driver.boot.clean_up_ramdisk.called)
@mock.patch.object(deploy_utils, 'get_ironic_api_url', autospec=True)
def test_managed_unversion_url(self, mock_get_url,
mock_create_ports_if_not_exist):
endpoint = 'http://192.169.0.42:6385/'
mock_get_url.return_value = endpoint
self.assertEqual(states.INSPECTWAIT,
self.iface.inspect_hardware(self.task))
mock_create_ports_if_not_exist.assert_called_once_with(self.task)
self.driver.boot.prepare_ramdisk.assert_called_once_with(
self.task, ramdisk_params={
'ipa-inspection-callback-url':
f'{endpoint}v1/continue_inspection',
})
self.driver.network.add_inspection_network.assert_called_once_with(
self.task)
self.driver.power.set_power_state.assert_has_calls([
mock.call(self.task, states.POWER_OFF, timeout=None),
mock.call(self.task, states.POWER_ON, timeout=None),
])
self.assertFalse(self.driver.network.remove_inspection_network.called)
self.assertFalse(self.driver.boot.clean_up_ramdisk.called)
@mock.patch.object(common, 'tear_down_managed_boot', autospec=True)
class ContinueInspectionTestCase(db_base.DbTestCase):
def setUp(self):
super().setUp()
CONF.set_override('enabled_inspect_interfaces',
['agent', 'no-inspect'])
self.node = obj_utils.create_test_node(
self.context,
inspect_interface='agent',
provision_state=states.INSPECTING)
self.iface = inspector.AgentInspect()
def test(self, mock_tear_down):
mock_tear_down.return_value = None
with task_manager.acquire(self.context, self.node.id) as task:
result = self.iface.continue_inspection(
task, mock.sentinel.inventory, mock.sentinel.plugin_data)
mock_tear_down.assert_called_once_with(task)
self.assertEqual(states.INSPECTING, task.node.provision_state)
self.assertIsNone(result)

View File

@ -94,6 +94,7 @@ ironic.hardware.interfaces.deploy =
ramdisk = ironic.drivers.modules.ramdisk:RamdiskDeploy
ironic.hardware.interfaces.inspect =
agent = ironic.drivers.modules.inspector:AgentInspect
fake = ironic.drivers.modules.fake:FakeInspect
idrac = ironic.drivers.modules.drac.inspect:DracInspect
idrac-redfish = ironic.drivers.modules.drac.inspect:DracRedfishInspect