Port physical network CIDR map hook

The physnet_cidr_map hook adds functionality to
populate the physical_network of a baremetal port based
on a cidr-to-physnet mapping in configuration.

Related-Bug: #1870529
Change-Id: I43cdac5ccd8c46836b26f6e4bc0d4509958e2e79
This commit is contained in:
Harald Jensås 2020-04-09 01:45:16 +02:00
parent f3a1ca956a
commit 92e2d26f15
8 changed files with 274 additions and 0 deletions

View File

@ -249,6 +249,14 @@ Here are some plugins that can be additionally enabled:
Processes LLDP data returned from inspection and parses TLVs from the
Basic Management (802.1AB), 802.1Q, and 802.3 sets and stores the
processed data back to the Ironic inspector data in Swift.
``physnet_cidr_map``
Configures the ``physical_network`` property of the nodes Ironic port when
the IP address is in a configured CIDR mapping. CIDR to physical network
mappings is set in configuration using the ``[port_physnet]/cidr_map``
option, for example::
[port_physnet]
cidr_map = 10.10.10.0/24:physnet_a, 2001:db8::/64:physnet_b
Refer to :ref:`contributing_link` for information on how to write your
own plugin.

View File

@ -20,6 +20,7 @@ from ironic_inspector.conf import dnsmasq_pxe_filter
from ironic_inspector.conf import iptables
from ironic_inspector.conf import ironic
from ironic_inspector.conf import pci_devices
from ironic_inspector.conf import port_physnet
from ironic_inspector.conf import processing
from ironic_inspector.conf import pxe_filter
from ironic_inspector.conf import service_catalog
@ -37,6 +38,7 @@ dnsmasq_pxe_filter.register_opts(CONF)
iptables.register_opts(CONF)
ironic.register_opts(CONF)
pci_devices.register_opts(CONF)
port_physnet.register_opts(CONF)
processing.register_opts(CONF)
pxe_filter.register_opts(CONF)
service_catalog.register_opts(CONF)

View File

@ -68,6 +68,7 @@ def list_opts():
('swift', ironic_inspector.conf.swift.list_opts()),
('ironic', ironic_inspector.conf.ironic.list_opts()),
('iptables', ironic_inspector.conf.iptables.list_opts()),
('port_physnet', ironic_inspector.conf.port_physnet.list_opts()),
('processing', ironic_inspector.conf.processing.list_opts()),
('pci_devices', ironic_inspector.conf.pci_devices.list_opts()),
('pxe_filter', ironic_inspector.conf.pxe_filter.list_opts()),

View File

@ -0,0 +1,36 @@
# 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_config import cfg
from ironic_inspector.common.i18n import _
_OPTS = [
cfg.ListOpt('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 physnet_cidr_map processing hook is enabled the '
'physical_network property of baremetal ports is '
'populated based on this mapping.')),
]
def register_opts(conf):
conf.register_opts(_OPTS, group='port_physnet')
def list_opts():
return _OPTS

View File

@ -0,0 +1,65 @@
# 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 ironic_inspector.plugins import base_physnet
CONF = cfg.CONF
class PhysnetCidrMapHook(base_physnet.BasePhysnetHook):
"""Process port physical network
Set the physical_network field of baremetal ports based on a cidr to
physical network mapping in the configuration.
"""
def get_physnet(self, port, iface_name, introspection_data):
"""Return a physical network to apply to a port.
:param port: The ironic port to patch.
:param iface_name: Name of the interface.
:param introspection_data: Introspection data.
:returns: The physical network to set, or None.
"""
def get_iface_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 dict with ip_networks as keys
cidr_map = {
ipaddress.ip_network(x.rsplit(':', 1)[0]): x.rsplit(':', 1)[1]
for x in CONF.port_physnet.cidr_map}
iface = [i for i in introspection_data['inventory']['interfaces']
if i['name'] == iface_name][0]
ips = get_iface_ips(iface)
for ip in ips:
try:
return [cidr_map[cidr] for cidr in cidr_map if ip in cidr][0]
except IndexError:
# No mapping found for any of the ip addresses
return None

View File

@ -0,0 +1,155 @@
# 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.
import mock
from openstack import exceptions
from oslo_config import cfg
from ironic_inspector import node_cache
from ironic_inspector.plugins import physnet_cidr_map
from ironic_inspector.test import base as test_base
from ironic_inspector import utils
class TestPhysnetCidrMapHook(test_base.NodeTest):
hook = physnet_cidr_map.PhysnetCidrMapHook()
def setUp(self):
super(TestPhysnetCidrMapHook, self).setUp()
self.data = {
'inventory': {
'interfaces': [{
'name': 'em1',
'mac_address': '11:11:11:11:11:11',
'ipv4_address': '1.1.1.1',
}],
'cpu': 1,
'disks': 1,
'memory': 1
},
'all_interfaces': {
'em1': {},
}
}
ports = [mock.Mock(spec=['address', 'uuid', 'physical_network'],
address=a) for a in ('11:11:11:11:11:11',)]
self.node_info = node_cache.NodeInfo(uuid=self.uuid, started_at=0,
node=self.node, ports=ports)
@mock.patch.object(node_cache.NodeInfo, 'patch_port')
def test_expected_data(self, mock_patch):
cfg.CONF.set_override('cidr_map', '1.1.1.0/24:physnet_a',
group='port_physnet')
patches = [{'path': '/physical_network',
'value': 'physnet_a',
'op': 'add'}]
self.hook.before_update(self.data, self.node_info)
self.assertCalledWithPatch(patches, mock_patch)
@mock.patch.object(node_cache.NodeInfo, 'patch_port')
def test_no_matching_mapping_config(self, mock_patch):
cfg.CONF.set_override('cidr_map', '2.2.2.0/24:physnet_b',
group='port_physnet')
self.hook.before_update(self.data, self.node_info)
self.assertFalse(mock_patch.called)
@mock.patch.object(node_cache.NodeInfo, 'patch_port')
def test_expected_data_ipv6_lowercase(self, mock_patch):
self.data['inventory']['interfaces'][0].pop('ipv4_address')
self.data['inventory']['interfaces'][0]['ipv6_address'] = '2001:db8::1'
cfg.CONF.set_override('cidr_map', '2001:db8::/64:physnet_b',
group='port_physnet')
patches = [{'path': '/physical_network',
'value': 'physnet_b',
'op': 'add'}]
self.hook.before_update(self.data, self.node_info)
self.assertCalledWithPatch(patches, mock_patch)
@mock.patch.object(node_cache.NodeInfo, 'patch_port')
def test_expected_data_ipv6_uppercase(self, mock_patch):
self.data['inventory']['interfaces'][0].pop('ipv4_address')
self.data['inventory']['interfaces'][0]['ipv6_address'] = '2001:db8::1'
cfg.CONF.set_override('cidr_map', '2001:DB8::/64:physnet_b',
group='port_physnet')
patches = [{'path': '/physical_network',
'value': 'physnet_b',
'op': 'add'}]
self.hook.before_update(self.data, self.node_info)
self.assertCalledWithPatch(patches, mock_patch)
@mock.patch.object(node_cache.NodeInfo, 'patch_port')
def test_no_mapping_in_config(self, mock_patch):
self.hook.before_update(self.data, self.node_info)
self.assertFalse(mock_patch.called)
def test_no_inventory(self):
cfg.CONF.set_override('cidr_map', '1.1.1.0/24:physnet_a',
group='port_physnet')
del self.data['inventory']
self.assertRaises(utils.Error, self.hook.before_update,
self.data, self.node_info)
@mock.patch('ironic_inspector.plugins.base_physnet.LOG')
@mock.patch.object(node_cache.NodeInfo, 'patch_port')
def test_interface_not_in_ironic(self, mock_patch, mock_log):
cfg.CONF.set_override('cidr_map', '1.1.1.0/24:physnet_a',
group='port_physnet')
self.node_info._ports = {}
self.hook.before_update(self.data, self.node_info)
self.assertTrue(mock_log.debug.called)
@mock.patch.object(node_cache.NodeInfo, 'patch_port')
def test_no_overwrite(self, mock_patch):
ports = [mock.Mock(spec=['address', 'uuid', 'physical_network'],
address=a, physical_network='foo')
for a in ('11:11:11:11:11:11',)]
node_info = node_cache.NodeInfo(uuid=self.uuid, started_at=0,
node=self.node, ports=ports)
cfg.CONF.set_override('overwrite_existing', False, group='processing')
cfg.CONF.set_override('cidr_map', '1.1.1.0/24:physnet_a',
group='port_physnet')
self.hook.before_update(self.data, node_info)
self.assertFalse(mock_patch.called)
@mock.patch('ironic_inspector.plugins.base_physnet.LOG')
@mock.patch.object(node_cache.NodeInfo, 'patch_port')
def test_patch_port_exception(self, mock_patch, mock_log):
cfg.CONF.set_override('cidr_map', '1.1.1.0/24:physnet_a',
group='port_physnet')
mock_patch.side_effect = exceptions.BadRequestException('invalid data')
self.hook.before_update(self.data, self.node_info)
log_msg = "Failed to update port %(uuid)s: %(error)s"
mock_log.warning.assert_called_with(log_msg, mock.ANY,
node_info=mock.ANY)
@mock.patch.object(node_cache.NodeInfo, 'patch_port')
def test_no_ip_address_on_interface(self, mock_patch):
cfg.CONF.set_override('cidr_map', '1.1.1.0/24:physnet_a',
group='port_physnet')
data = {
'inventory': {
'interfaces': [{
'name': 'em1',
'mac_address': '11:11:11:11:11:11',
}],
'cpu': 1,
'disks': 1,
'memory': 1
},
'all_interfaces': {
'em1': {},
}
}
self.hook.before_update(data, self.node_info)
self.assertFalse(mock_patch.called)

View File

@ -0,0 +1,6 @@
---
features:
- Added ``physnet_cidr_map`` processing plugin, the plugin uses the IP
address of interfaces returned during inspection and set the port
``physical_network`` via lookup from a CIDR to physical network mapping in
config option ``[port_physnet]/cidr_map``.

View File

@ -43,6 +43,7 @@ ironic_inspector.hooks.processing =
local_link_connection = ironic_inspector.plugins.local_link_connection:GenericLocalLinkConnectionHook
lldp_basic = ironic_inspector.plugins.lldp_basic:LLDPBasicProcessingHook
pci_devices = ironic_inspector.plugins.pci_devices:PciDevicesHook
physnet_cidr_map = ironic_inspector.plugins.physnet_cidr_map:PhysnetCidrMapHook
ironic_inspector.hooks.node_not_found =
example = ironic_inspector.plugins.example:example_not_found_hook
enroll = ironic_inspector.plugins.discovery:enroll_node_not_found_hook