Browse Source
For Ironic multi-tenant networking support, we need to be able to discover the nodes local connectivity. To do this IPA can try to pull LLDP packets for every NIC. This patch adds a processing hook to handle the data from these packets in ironic-inspector so that we can populate the correct fields fields in Ironic. The generic lldp hook only handles the mandatory fields port id and chassis id, set on port_id and switch_id in local_link_connection. Further LLDP fields should be handled by additional vendor specific LLDP processing hooks, that populate the switch_info field in a non-generic way. Change-Id: I884eaaa9cc54cd08c21147da438b1dabc10d3a40 Related-Bug: #1526403 Depends-On: Ie655fd59b06de7b84fba3b438d5e4c2ecd8075c3 Depends-On: Idae9b1ede1797029da1bd521501b121957ca1f1achanges/82/321082/15
8 changed files with 313 additions and 24 deletions
@ -0,0 +1,122 @@
|
||||
# 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. |
||||
|
||||
"""Generic LLDP Processing Hook""" |
||||
|
||||
import binascii |
||||
|
||||
from ironicclient import exc as client_exc |
||||
import netaddr |
||||
from oslo_config import cfg |
||||
|
||||
from ironic_inspector.common.i18n import _LW, _LE |
||||
from ironic_inspector.common import ironic |
||||
from ironic_inspector.plugins import base |
||||
from ironic_inspector import utils |
||||
|
||||
LOG = utils.getProcessingLogger(__name__) |
||||
|
||||
# NOTE(sambetts) Constants defined according to IEEE standard for LLDP |
||||
# http://standards.ieee.org/getieee802/download/802.1AB-2009.pdf |
||||
LLDP_TLV_TYPE_CHASSIS_ID = 1 |
||||
LLDP_TLV_TYPE_PORT_ID = 2 |
||||
PORT_ID_SUBTYPE_MAC = 3 |
||||
PORT_ID_SUBTYPE_IFNAME = 5 |
||||
PORT_ID_SUBTYPE_LOCAL = 7 |
||||
STRING_PORT_SUBTYPES = [PORT_ID_SUBTYPE_IFNAME, PORT_ID_SUBTYPE_LOCAL] |
||||
CHASSIS_ID_SUBTYPE_MAC = 4 |
||||
|
||||
CONF = cfg.CONF |
||||
|
||||
REQUIRED_IRONIC_VERSION = '1.19' |
||||
|
||||
|
||||
class GenericLocalLinkConnectionHook(base.ProcessingHook): |
||||
"""Process mandatory LLDP packet fields |
||||
|
||||
Non-vendor specific LLDP packet fields processed for each NIC found for a |
||||
baremetal node, port ID and chassis ID. These fields if found and if valid |
||||
will be saved into the local link connection info port id and switch id |
||||
fields on the Ironic port that represents that NIC. |
||||
""" |
||||
|
||||
def _get_local_link_patch(self, tlv_type, tlv_value, port): |
||||
try: |
||||
data = bytearray(binascii.unhexlify(tlv_value)) |
||||
except TypeError: |
||||
LOG.warning(_LW("TLV value for TLV type %d not in correct" |
||||
"format, ensure TLV value is in " |
||||
"hexidecimal format when sent to " |
||||
"inspector"), tlv_type) |
||||
return |
||||
|
||||
item = value = None |
||||
if tlv_type == LLDP_TLV_TYPE_PORT_ID: |
||||
# Check to ensure the port id is an allowed type |
||||
item = "port_id" |
||||
if data[0] in STRING_PORT_SUBTYPES: |
||||
value = data[1:].decode() |
||||
if data[0] == PORT_ID_SUBTYPE_MAC: |
||||
value = str(netaddr.EUI( |
||||
binascii.hexlify(data[1:]).decode())) |
||||
elif tlv_type == LLDP_TLV_TYPE_CHASSIS_ID: |
||||
# Check to ensure the chassis id is the allowed type |
||||
if data[0] == CHASSIS_ID_SUBTYPE_MAC: |
||||
item = "switch_id" |
||||
value = str(netaddr.EUI( |
||||
binascii.hexlify(data[1:]).decode())) |
||||
|
||||
if item and value: |
||||
if (not CONF.processing.overwrite_existing and |
||||
item in port.local_link_connection): |
||||
return |
||||
return {'op': 'add', |
||||
'path': '/local_link_connection/%s' % item, |
||||
'value': value} |
||||
|
||||
def before_update(self, introspection_data, node_info, **kwargs): |
||||
"""Process LLDP data and patch Ironic port local link connection""" |
||||
inventory = utils.get_inventory(introspection_data) |
||||
|
||||
ironic_ports = node_info.ports() |
||||
|
||||
for iface in inventory['interfaces']: |
||||
if iface['name'] not in introspection_data['all_interfaces']: |
||||
continue |
||||
port = ironic_ports[iface['mac_address']] |
||||
|
||||
lldp_data = iface.get('lldp') |
||||
if lldp_data is None: |
||||
LOG.warning(_LW("No LLDP Data found for interface %s"), iface) |
||||
continue |
||||
|
||||
patches = [] |
||||
for tlv_type, tlv_value in lldp_data: |
||||
patch = self._get_local_link_patch(tlv_type, tlv_value, port) |
||||
if patch is not None: |
||||
patches.append(patch) |
||||
|
||||
try: |
||||
# NOTE(sambetts) We need a newer version of Ironic API for this |
||||
# transaction, so create a new ironic client and explicitly |
||||
# pass it into the function. |
||||
cli = ironic.get_client(api_version=REQUIRED_IRONIC_VERSION) |
||||
node_info.patch_port(iface['mac_address'], patches, ironic=cli) |
||||
except client_exc.NotAcceptable: |
||||
LOG.error(_LE("Unable to set Ironic port local link " |
||||
"connection information because Ironic does not " |
||||
"support the required version")) |
||||
# NOTE(sambetts) May as well break out out of the loop here |
||||
# because Ironic version is not going to change for the other |
||||
# interfaces. |
||||
break |
@ -0,0 +1,138 @@
|
||||
# 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 oslo_config import cfg |
||||
|
||||
from ironic_inspector import node_cache |
||||
from ironic_inspector.plugins import local_link_connection |
||||
from ironic_inspector.test import base as test_base |
||||
from ironic_inspector import utils |
||||
|
||||
|
||||
class TestGenericLocalLinkConnectionHook(test_base.NodeTest): |
||||
hook = local_link_connection.GenericLocalLinkConnectionHook() |
||||
|
||||
def setUp(self): |
||||
super(TestGenericLocalLinkConnectionHook, self).setUp() |
||||
self.data = { |
||||
'inventory': { |
||||
'interfaces': [{ |
||||
'name': 'em1', 'mac_address': '11:11:11:11:11:11', |
||||
'ipv4_address': '1.1.1.1', |
||||
'lldp': [ |
||||
(0, ''), |
||||
(1, '04885a92ec5459'), |
||||
(2, '0545746865726e6574312f3138'), |
||||
(3, '0078')] |
||||
}], |
||||
'cpu': 1, |
||||
'disks': 1, |
||||
'memory': 1 |
||||
}, |
||||
'all_interfaces': { |
||||
'em1': {}, |
||||
} |
||||
} |
||||
|
||||
llc = { |
||||
'port_id': '56' |
||||
} |
||||
|
||||
ports = [mock.Mock(spec=['address', 'uuid', 'local_link_connection'], |
||||
address=a, local_link_connection=llc) |
||||
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): |
||||
patches = [ |
||||
{'path': '/local_link_connection/port_id', |
||||
'value': 'Ethernet1/18', 'op': 'add'}, |
||||
{'path': '/local_link_connection/switch_id', |
||||
'value': '88-5A-92-EC-54-59', '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_invalid_chassis_id_subtype(self, mock_patch): |
||||
# First byte of TLV value is processed to calculate the subtype for the |
||||
# chassis ID, Subtype 5 ('05...') isn't a subtype supported by this |
||||
# plugin, so we expect it to skip this TLV. |
||||
self.data['inventory']['interfaces'][0]['lldp'][1] = ( |
||||
1, '05885a92ec5459') |
||||
patches = [ |
||||
{'path': '/local_link_connection/port_id', |
||||
'value': 'Ethernet1/18', '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_invalid_port_id_subtype(self, mock_patch): |
||||
# First byte of TLV value is processed to calculate the subtype for the |
||||
# port ID, Subtype 6 ('06...') isn't a subtype supported by this |
||||
# plugin, so we expect it to skip this TLV. |
||||
self.data['inventory']['interfaces'][0]['lldp'][2] = ( |
||||
2, '0645746865726e6574312f3138') |
||||
patches = [ |
||||
{'path': '/local_link_connection/switch_id', |
||||
'value': '88-5A-92-EC-54-59', '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_port_id_subtype_mac(self, mock_patch): |
||||
self.data['inventory']['interfaces'][0]['lldp'][2] = ( |
||||
2, '03885a92ec5458') |
||||
patches = [ |
||||
{'path': '/local_link_connection/port_id', |
||||
'value': '88-5A-92-EC-54-58', 'op': 'add'}, |
||||
{'path': '/local_link_connection/switch_id', |
||||
'value': '88-5A-92-EC-54-59', '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_lldp_none(self, mock_patch): |
||||
self.data['inventory']['interfaces'][0]['lldp'] = None |
||||
patches = [] |
||||
self.hook.before_update(self.data, self.node_info) |
||||
self.assertCalledWithPatch(patches, mock_patch) |
||||
|
||||
@mock.patch.object(node_cache.NodeInfo, 'patch_port') |
||||
def test_interface_not_in_all_interfaces(self, mock_patch): |
||||
self.data['all_interfaces'] = {} |
||||
patches = [] |
||||
self.hook.before_update(self.data, self.node_info) |
||||
self.assertCalledWithPatch(patches, mock_patch) |
||||
|
||||
def test_no_inventory(self): |
||||
del self.data['inventory'] |
||||
self.assertRaises(utils.Error, self.hook.before_update, |
||||
self.data, self.node_info) |
||||
|
||||
@mock.patch.object(node_cache.NodeInfo, 'patch_port') |
||||
def test_no_overwrite(self, mock_patch): |
||||
cfg.CONF.set_override('overwrite_existing', False, group='processing') |
||||
patches = [ |
||||
{'path': '/local_link_connection/switch_id', |
||||
'value': '88-5A-92-EC-54-59', 'op': 'add'} |
||||
] |
||||
self.hook.before_update(self.data, self.node_info) |
||||
self.assertCalledWithPatch(patches, mock_patch) |
@ -0,0 +1,5 @@
|
||||
--- |
||||
features: |
||||
- Added GenericLocalLinkConnectionHook processing plugin to process LLDP data |
||||
returned during inspection and set port ID and switch ID in an Ironic |
||||
node's port local link connection information using that data. |
Loading…
Reference in new issue