From 7f053dc70d88890f94de2c27dc35b010a62ca5e0 Mon Sep 17 00:00:00 2001 From: Mahnoor Asghar Date: Thu, 31 Aug 2023 08:22:25 -0200 Subject: [PATCH] Add inspection hooks Adds the 'memory', 'pci-devices', and 'physical-network' inspection hooks in the agent inspect interface for processing data received from the ramdisk at the /v1/continue_inspection endpoint. Change-Id: I67631ec5b94d1b29afcdc9a971b1052cf35bda1f Story: #2010275 --- ironic/conf/inspector.py | 17 +++- .../drivers/modules/inspector/hooks/memory.py | 41 ++++++++ .../modules/inspector/hooks/pci_devices.py | 84 ++++++++++++++++ .../inspector/hooks/physical_network.py | 99 +++++++++++++++++++ .../modules/inspector/hooks/test_memory.py | 38 +++++++ .../inspector/hooks/test_pci_devices.py | 73 ++++++++++++++ .../inspector/hooks/test_physical_network.py | 86 ++++++++++++++++ setup.cfg | 3 + 8 files changed, 440 insertions(+), 1 deletion(-) create mode 100644 ironic/drivers/modules/inspector/hooks/memory.py create mode 100644 ironic/drivers/modules/inspector/hooks/pci_devices.py create mode 100644 ironic/drivers/modules/inspector/hooks/physical_network.py create mode 100644 ironic/tests/unit/drivers/modules/inspector/hooks/test_memory.py create mode 100644 ironic/tests/unit/drivers/modules/inspector/hooks/test_pci_devices.py create mode 100644 ironic/tests/unit/drivers/modules/inspector/hooks/test_physical_network.py diff --git a/ironic/conf/inspector.py b/ironic/conf/inspector.py index 7766c69b7e..88a517bd38 100644 --- a/ironic/conf/inspector.py +++ b/ironic/conf/inspector.py @@ -111,7 +111,22 @@ opts = [ 'if at least one record is too short. Additionally, ' 'remove the incoming "data" even if parsing failed. ' 'This configuration option is used by the ' - '"extra-hardware" inspection hook.')) + '"extra-hardware" inspection hook.')), + cfg.MultiStrOpt('pci_device_alias', + default=[], + help=_('An alias for a PCI device identified by ' + '\'vendor_id\' and \'product_id\' fields. Format: ' + '{"vendor_id": "1234", "product_id": "5678", ' + '"name": "pci_dev1"}. Use double quotes for the ' + 'keys and values.')), + cfg.ListOpt('physical_network_cidr_map', + default=[], + sample_default=('10.10.10.0/24:physnet_a,' + '2001:db8::/64:physnet_b'), + help=_('Mapping of IP subnet CIDR to physical network. When ' + 'the phyical-network inspection hook is enabled, the ' + '"physical_network" property of corresponding ' + 'baremetal ports is populated based on this mapping.')) ] diff --git a/ironic/drivers/modules/inspector/hooks/memory.py b/ironic/drivers/modules/inspector/hooks/memory.py new file mode 100644 index 0000000000..07a212a99a --- /dev/null +++ b/ironic/drivers/modules/inspector/hooks/memory.py @@ -0,0 +1,41 @@ +# 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 oslo_log import log as logging + +from ironic.common import exception +from ironic.common.i18n import _ +from ironic.drivers.modules.inspector.hooks import base + +LOG = logging.getLogger(__name__) + + +class MemoryHook(base.InspectionHook): + """Hook to set the node's memory_mb property based on the inventory.""" + + def __call__(self, task, inventory, plugin_data): + """Update node properties with memory information.""" + + try: + memory = inventory['memory']['physical_mb'] + LOG.info('Discovered memory: %s for node %s', memory, + task.node.uuid) + task.node.set_property('memory_mb', memory) + task.node.save() + except (KeyError, ValueError, TypeError): + msg = _('Inventory has missing memory information: %(memory)s for ' + 'node %(node)s.') % {'memory': inventory.get('memory'), + 'node': task.node.uuid} + LOG.error(msg) + raise exception.InvalidNodeInventory(node=task.node.uuid, + reason=msg) diff --git a/ironic/drivers/modules/inspector/hooks/pci_devices.py b/ironic/drivers/modules/inspector/hooks/pci_devices.py new file mode 100644 index 0000000000..ded3fd5575 --- /dev/null +++ b/ironic/drivers/modules/inspector/hooks/pci_devices.py @@ -0,0 +1,84 @@ +# 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. + +"""Gather and distinguish PCI devices from plugin_data.""" + +import collections +import json + +from oslo_config import cfg +from oslo_log import log as logging + +from ironic.common import utils +from ironic.drivers.modules.inspector.hooks import base + + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +def _parse_pci_alias_entries(): + parsed_pci_devices = [] + + for pci_alias_entry in CONF.inspector.pci_device_alias: + try: + parsed_entry = json.loads(pci_alias_entry) + if set(parsed_entry) != {'vendor_id', 'product_id', 'name'}: + raise KeyError('The "pci_device_alias" entry should contain ' + 'exactly the "vendor_id", "product_id" and ' + '"name" keys.') + parsed_pci_devices.append(parsed_entry) + except (ValueError, KeyError) as exc: + LOG.error("Error parsing 'pci_device_alias' option: %s", exc) + + return {(dev['vendor_id'], dev['product_id']): dev['name'] + for dev in parsed_pci_devices} + + +class PciDevicesHook(base.InspectionHook): + """Hook to count various PCI devices, and set the node's capabilities. + + This information can later be used by nova for node scheduling. + """ + _aliases = _parse_pci_alias_entries() + + def _found_pci_devices_count(self, found_pci_devices): + return collections.Counter([(dev['vendor_id'], dev['product_id']) + for dev in found_pci_devices + if (dev['vendor_id'], dev['product_id']) + in self._aliases]) + + def __call__(self, task, inventory, plugin_data): + """Update node capabilities with PCI devices.""" + + if 'pci_devices' not in plugin_data: + if CONF.inspector.pci_device_alias: + LOG.warning('No information about PCI devices was received ' + 'from the ramdisk.') + return + + alias_count = {self._aliases[id_pair]: count for id_pair, count in + self._found_pci_devices_count( + plugin_data['pci_devices']).items()} + if alias_count: + LOG.info('Found the following PCI devices: %s', alias_count) + + old_capabilities = task.node.properties.get('capabilities') + LOG.debug('Old capabilities for node %s: %s', task.node.uuid, + old_capabilities) + new_capabilities = utils.get_updated_capabilities(old_capabilities, + alias_count) + task.node.set_property('capabilities', new_capabilities) + LOG.debug('New capabilities for node %s: %s', task.node.uuid, + new_capabilities) + task.node.save() diff --git a/ironic/drivers/modules/inspector/hooks/physical_network.py b/ironic/drivers/modules/inspector/hooks/physical_network.py new file mode 100644 index 0000000000..2d078f5223 --- /dev/null +++ b/ironic/drivers/modules/inspector/hooks/physical_network.py @@ -0,0 +1,99 @@ +# 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. + +"""Port Physical Network Hook""" + +import ipaddress + +from oslo_config import cfg +from oslo_log import log as logging + +from ironic.drivers.modules.inspector.hooks import base +from ironic import objects + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class PhysicalNetworkHook(base.InspectionHook): + """Hook to set the port's physical_network field. + + Set the ironic port's physical_network field based on a CIDR to physical + network mapping in the configuration. + """ + + dependencies = ['validate-interfaces'] + + def get_physical_network(self, interface): + """Return a physical network to apply to an ironic port. + + :param interface: The interface from the inventory. + :returns: The physical network to set, or None. + """ + + def get_interface_ips(iface): + ips = [] + for addr_version in ['ipv4_address', 'ipv6_address']: + try: + ips.append(ipaddress.ip_address(iface.get(addr_version))) + except ValueError: + pass + return ips + + # Convert list config to a dictionary with ip_networks as keys + cidr_map = { + ipaddress.ip_network(x.rsplit(':', 1)[0]): x.rsplit(':', 1)[1] + for x in CONF.inspector.physical_network_cidr_map} + ips = get_interface_ips(interface) + for ip in ips: + try: + return [cidr_map[cidr] for cidr in cidr_map if ip in cidr][0] + except IndexError: + # This IP address is not present in the CIDR map + pass + # No mapping found for any of the IP addresses + return None + + def __call__(self, task, inventory, plugin_data): + """Process inspection data and patch the port's physical network.""" + + node_ports = objects.Port.list_by_node_id(task.context, task.node.id) + ports_dict = {p.address: p for p in node_ports} + + for interface in inventory['interfaces']: + if interface['name'] not in plugin_data['all_interfaces']: + continue + + mac_address = interface['mac_address'] + port = ports_dict.get(mac_address) + if not port: + LOG.debug("Skipping physical network processing for interface " + "%s on node %s - matching port not found in Ironic.", + mac_address, task.node.uuid) + continue + + # Determine the physical network for this port, using the interface + # IPs and CIDR map configuration. + phys_network = self.get_physical_network(interface) + if phys_network is None: + LOG.debug("Skipping physical network processing for interface " + "%s on node %s - no physical network mapping.", + mac_address, + task.node.uuid) + continue + + if getattr(port, 'physical_network', '') != phys_network: + port.physical_network = phys_network + port.save() + LOG.debug('Updated physical_network of port %s to %s', + port.uuid, port.physical_network) diff --git a/ironic/tests/unit/drivers/modules/inspector/hooks/test_memory.py b/ironic/tests/unit/drivers/modules/inspector/hooks/test_memory.py new file mode 100644 index 0000000000..05d59ecfb4 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/inspector/hooks/test_memory.py @@ -0,0 +1,38 @@ +# 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 ironic.conductor import task_manager +from ironic.conf import CONF +from ironic.drivers.modules.inspector.hooks import memory as memory_hook +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.objects import utils as obj_utils + + +class MemoryTestCase(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') + self.inventory = {'memory': {'physical_mb': 45000}} + self.plugin_data = {'fake': 'fake-plugin-data'} + + def test_memory(self): + with task_manager.acquire(self.context, self.node.id) as task: + memory_hook.MemoryHook().__call__(task, self.inventory, + self.plugin_data) + self.node.refresh() + result = self.node.properties + self.assertEqual(self.inventory['memory']['physical_mb'], + result['memory_mb']) diff --git a/ironic/tests/unit/drivers/modules/inspector/hooks/test_pci_devices.py b/ironic/tests/unit/drivers/modules/inspector/hooks/test_pci_devices.py new file mode 100644 index 0000000000..566123e82e --- /dev/null +++ b/ironic/tests/unit/drivers/modules/inspector/hooks/test_pci_devices.py @@ -0,0 +1,73 @@ +# 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 ironic.conductor import task_manager +from ironic.conf import CONF +from ironic.drivers.modules.inspector.hooks import pci_devices as \ + pci_devices_hook +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.objects import utils as obj_utils + +_PLUGIN_DATA = { + 'pci_devices': [ + { + 'vendor_id': '8086', 'product_id': '2922', 'class': '010601', + 'revision': '02', 'bus': '0000:00:1f.2' + }, + { + 'vendor_id': '8086', 'product_id': '2918', 'class': '060100', + 'revision': '02', 'bus': '0000:00:1f.0' + }, + { + 'vendor_id': '8086', 'product_id': '2930', 'class': '0c0500', + 'revision': '02', 'bus': '0000:00:1f.3' + }, + { + 'vendor_id': '1b36', 'product_id': '000c', 'class': '060400', + 'revision': '00', 'bus': '0000:00:01.2' + }, + { + 'vendor_id': '1b36', 'product_id': '000c', 'class': '060400', + 'revision': '00', 'bus': '0000:00:01.0' + }, + { + 'vendor_id': '1b36', 'product_id': '000d', 'class': '0c0330', + 'revision': '01', 'bus': '0000:02:00.0' + } + ] +} + +_ALIASES = {('8086', '2922'): 'EightyTwentyTwo', + ('8086', '2918'): 'EightyEighteen', + ('1b36', '000c'): 'OneBZeroC'} + + +class PciDevicesTestCase(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') + self.inventory = {'fake': 'fake-inventory'} + self.plugin_data = _PLUGIN_DATA + self.pci_devices_hook = pci_devices_hook.PciDevicesHook() + self.pci_devices_hook._aliases = _ALIASES + + def test_pci_devices(self): + with task_manager.acquire(self.context, self.node.id) as task: + self.pci_devices_hook.__call__(task, self.inventory, + self.plugin_data) + self.node.refresh() + result = self.node.properties.get('capabilities', '') + expected = 'EightyTwentyTwo:1,EightyEighteen:1,OneBZeroC:2' + self.assertEqual(expected, result) diff --git a/ironic/tests/unit/drivers/modules/inspector/hooks/test_physical_network.py b/ironic/tests/unit/drivers/modules/inspector/hooks/test_physical_network.py new file mode 100644 index 0000000000..df2d7dc1b7 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/inspector/hooks/test_physical_network.py @@ -0,0 +1,86 @@ +# 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.conductor import task_manager +from ironic.conf import CONF +from ironic.drivers.modules.inspector.hooks import physical_network as \ + physical_network_hook +from ironic import objects +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.objects import utils as obj_utils + + +_INTERFACE_1 = { + 'name': 'em0', + 'mac_address': '11:11:11:11:11:11', + 'ipv4_address': '192.168.10.1', + 'ipv6_address': '2001:db8::1' +} + +_INTERFACE_2 = { + 'name': 'em1', + 'mac_address': '22:22:22:22:22:22', + 'ipv4_address': '192.168.12.2', + 'ipv6_address': 'fe80:5054::', +} + +_INTERFACE_3 = { + 'name': 'em2', + 'mac_address': '33:33:33:33:33:33', + 'ipv4_address': '192.168.12.3', + 'ipv6_address': 'fe80::5054:ff:fea7:87:6482', +} + +_INVENTORY = { + 'interfaces': [_INTERFACE_1, _INTERFACE_2, _INTERFACE_3] +} + +_PLUGIN_DATA = { + 'all_interfaces': {'em0': _INTERFACE_1, 'em1': _INTERFACE_2} +} + + +class PhysicalNetworkTestCase(db_base.DbTestCase): + def setUp(self): + super().setUp() + CONF.set_override('enabled_inspect_interfaces', + ['agent', 'no-inspect']) + CONF.set_override('physical_network_cidr_map', + '192.168.10.0/24:network-a,fe80::/16:network-b', + 'inspector') + self.node = obj_utils.create_test_node(self.context, + inspect_interface='agent') + self.inventory = _INVENTORY + self.plugin_data = _PLUGIN_DATA + + @mock.patch.object(objects.Port, 'list_by_node_id', autospec=True) + def test_physical_network(self, mock_list_by_nodeid): + with task_manager.acquire(self.context, self.node.id) as task: + port1 = obj_utils.create_test_port(self.context, + address='11:11:11:11:11:11', + node_id=self.node.id) + port2 = obj_utils.create_test_port( + self.context, id=988, + uuid='2be26c0b-03f2-4d2e-ae87-c02d7f33c781', + address='22:22:22:22:22:22', node_id=self.node.id) + ports = [port1, port2] + + mock_list_by_nodeid.return_value = ports + physical_network_hook.PhysicalNetworkHook().__call__( + task, self.inventory, self.plugin_data) + port1.refresh() + port2.refresh() + self.assertEqual(port1.physical_network, 'network-a') + self.assertEqual(port2.physical_network, 'network-b') diff --git a/setup.cfg b/setup.cfg index 0d5cc8268b..4b946be323 100644 --- a/setup.cfg +++ b/setup.cfg @@ -205,6 +205,9 @@ ironic.inspection.hooks = boot-mode = ironic.drivers.modules.inspector.hooks.boot_mode:BootModeHook cpu-capabilities = ironic.drivers.modules.inspector.hooks.cpu_capabilities:CPUCapabilitiesHook extra-hardware = ironic.drivers.modules.inspector.hooks.extra_hardware:ExtraHardwareHook + memory = ironic.drivers.modules.inspector.hooks.memory:MemoryHook + pci-devices = ironic.drivers.modules.inspector.hooks.pci_devices:PciDevicesHook + physical-network = ironic.drivers.modules.inspector.hooks.physical_network:PhysicalNetworkHook [egg_info] tag_build =