Merge "Port physical network CIDR map hook"
This commit is contained in:
commit
48391d62a4
@ -249,6 +249,14 @@ Here are some plugins that can be additionally enabled:
|
|||||||
Processes LLDP data returned from inspection and parses TLVs from the
|
Processes LLDP data returned from inspection and parses TLVs from the
|
||||||
Basic Management (802.1AB), 802.1Q, and 802.3 sets and stores the
|
Basic Management (802.1AB), 802.1Q, and 802.3 sets and stores the
|
||||||
processed data back to the Ironic inspector data in Swift.
|
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
|
Refer to :ref:`contributing_link` for information on how to write your
|
||||||
own plugin.
|
own plugin.
|
||||||
|
@ -20,6 +20,7 @@ from ironic_inspector.conf import dnsmasq_pxe_filter
|
|||||||
from ironic_inspector.conf import iptables
|
from ironic_inspector.conf import iptables
|
||||||
from ironic_inspector.conf import ironic
|
from ironic_inspector.conf import ironic
|
||||||
from ironic_inspector.conf import pci_devices
|
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 processing
|
||||||
from ironic_inspector.conf import pxe_filter
|
from ironic_inspector.conf import pxe_filter
|
||||||
from ironic_inspector.conf import service_catalog
|
from ironic_inspector.conf import service_catalog
|
||||||
@ -37,6 +38,7 @@ dnsmasq_pxe_filter.register_opts(CONF)
|
|||||||
iptables.register_opts(CONF)
|
iptables.register_opts(CONF)
|
||||||
ironic.register_opts(CONF)
|
ironic.register_opts(CONF)
|
||||||
pci_devices.register_opts(CONF)
|
pci_devices.register_opts(CONF)
|
||||||
|
port_physnet.register_opts(CONF)
|
||||||
processing.register_opts(CONF)
|
processing.register_opts(CONF)
|
||||||
pxe_filter.register_opts(CONF)
|
pxe_filter.register_opts(CONF)
|
||||||
service_catalog.register_opts(CONF)
|
service_catalog.register_opts(CONF)
|
||||||
|
@ -68,6 +68,7 @@ def list_opts():
|
|||||||
('swift', ironic_inspector.conf.swift.list_opts()),
|
('swift', ironic_inspector.conf.swift.list_opts()),
|
||||||
('ironic', ironic_inspector.conf.ironic.list_opts()),
|
('ironic', ironic_inspector.conf.ironic.list_opts()),
|
||||||
('iptables', ironic_inspector.conf.iptables.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()),
|
('processing', ironic_inspector.conf.processing.list_opts()),
|
||||||
('pci_devices', ironic_inspector.conf.pci_devices.list_opts()),
|
('pci_devices', ironic_inspector.conf.pci_devices.list_opts()),
|
||||||
('pxe_filter', ironic_inspector.conf.pxe_filter.list_opts()),
|
('pxe_filter', ironic_inspector.conf.pxe_filter.list_opts()),
|
||||||
|
36
ironic_inspector/conf/port_physnet.py
Normal file
36
ironic_inspector/conf/port_physnet.py
Normal 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
|
65
ironic_inspector/plugins/physnet_cidr_map.py
Normal file
65
ironic_inspector/plugins/physnet_cidr_map.py
Normal 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
|
155
ironic_inspector/test/unit/test_plugins_physnet_cidr_map.py
Normal file
155
ironic_inspector/test/unit/test_plugins_physnet_cidr_map.py
Normal 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)
|
@ -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``.
|
@ -43,6 +43,7 @@ ironic_inspector.hooks.processing =
|
|||||||
local_link_connection = ironic_inspector.plugins.local_link_connection:GenericLocalLinkConnectionHook
|
local_link_connection = ironic_inspector.plugins.local_link_connection:GenericLocalLinkConnectionHook
|
||||||
lldp_basic = ironic_inspector.plugins.lldp_basic:LLDPBasicProcessingHook
|
lldp_basic = ironic_inspector.plugins.lldp_basic:LLDPBasicProcessingHook
|
||||||
pci_devices = ironic_inspector.plugins.pci_devices:PciDevicesHook
|
pci_devices = ironic_inspector.plugins.pci_devices:PciDevicesHook
|
||||||
|
physnet_cidr_map = ironic_inspector.plugins.physnet_cidr_map:PhysnetCidrMapHook
|
||||||
ironic_inspector.hooks.node_not_found =
|
ironic_inspector.hooks.node_not_found =
|
||||||
example = ironic_inspector.plugins.example:example_not_found_hook
|
example = ironic_inspector.plugins.example:example_not_found_hook
|
||||||
enroll = ironic_inspector.plugins.discovery:enroll_node_not_found_hook
|
enroll = ironic_inspector.plugins.discovery:enroll_node_not_found_hook
|
||||||
|
Loading…
Reference in New Issue
Block a user