Merge "Add inspection hooks"

This commit is contained in:
Zuul 2023-10-10 08:12:32 +00:00 committed by Gerrit Code Review
commit 33a471e7ac
8 changed files with 440 additions and 1 deletions

View File

@ -113,7 +113,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.'))
]

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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'])

View File

@ -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)

View File

@ -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')

View File

@ -206,6 +206,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 =