Merge "Add inspection hooks"
This commit is contained in:
commit
33a471e7ac
@ -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.'))
|
||||
]
|
||||
|
||||
|
||||
|
41
ironic/drivers/modules/inspector/hooks/memory.py
Normal file
41
ironic/drivers/modules/inspector/hooks/memory.py
Normal 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)
|
84
ironic/drivers/modules/inspector/hooks/pci_devices.py
Normal file
84
ironic/drivers/modules/inspector/hooks/pci_devices.py
Normal 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()
|
99
ironic/drivers/modules/inspector/hooks/physical_network.py
Normal file
99
ironic/drivers/modules/inspector/hooks/physical_network.py
Normal 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)
|
@ -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'])
|
@ -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)
|
@ -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')
|
@ -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 =
|
||||
|
Loading…
Reference in New Issue
Block a user