From 8834927d4c5b448aba0c77402f6531b5e60fb305 Mon Sep 17 00:00:00 2001 From: Bob Fournier Date: Sat, 3 Dec 2016 11:35:22 -0500 Subject: [PATCH] Add plugin to process basic management LLDP TLVs This adds a plugin to process the raw LLDP TLVs stored in Swift for the Basic Mgmt, 802.1, and 802.3 data sets and store the parsed data back in Swift. It implements the TLV processing as described in the specification: http://specs.openstack.org/openstack/ironic-inspector-specs/specs/lldp-reporting.html Change-Id: I854826787ff045ffb2807970deaba8b77cbe277d Closes-Bug: 1647515 Related-Bug: 1626253 --- doc/source/usage.rst | 4 + ironic_inspector/common/lldp_parsers.py | 339 ++++++++++++++++ ironic_inspector/common/lldp_tlvs.py | 366 ++++++++++++++++++ ironic_inspector/plugins/lldp_basic.py | 88 +++++ ironic_inspector/test/base.py | 6 +- ironic_inspector/test/functional.py | 37 ++ .../test/unit/test_plugins_lldp_basic.py | 329 ++++++++++++++++ ...dd-lldp-basic-plugin-98aebcf43e60931b.yaml | 4 + requirements.txt | 1 + setup.cfg | 1 + 10 files changed, 1174 insertions(+), 1 deletion(-) create mode 100644 ironic_inspector/common/lldp_parsers.py create mode 100644 ironic_inspector/common/lldp_tlvs.py create mode 100644 ironic_inspector/plugins/lldp_basic.py create mode 100644 ironic_inspector/test/unit/test_plugins_lldp_basic.py create mode 100644 releasenotes/notes/add-lldp-basic-plugin-98aebcf43e60931b.yaml diff --git a/doc/source/usage.rst b/doc/source/usage.rst index ac5fc7a18..ecfffc6c4 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -192,6 +192,10 @@ Here are some plugins that can be additionally enabled: information on the nodes Ironic ports with that data. To enable LLDP in the inventory from IPA ``ipa-collect-lldp=1`` should be passed as a kernel parameter to the IPA ramdisk. +``lldp_basic`` + 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. Refer to :ref:`contributing_link` for information on how to write your own plugin. diff --git a/ironic_inspector/common/lldp_parsers.py b/ironic_inspector/common/lldp_parsers.py new file mode 100644 index 000000000..49f4615e6 --- /dev/null +++ b/ironic_inspector/common/lldp_parsers.py @@ -0,0 +1,339 @@ +# 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. + +""" Names and mapping functions used to map LLDP TLVs to name/value pairs """ + +import binascii + +from construct import core +import netaddr + +from ironic_inspector.common.i18n import _, _LW +from ironic_inspector.common import lldp_tlvs as tlv +from ironic_inspector import utils + +LOG = utils.getProcessingLogger(__name__) + + +# Names used in name/value pair from parsed TLVs +LLDP_CHASSIS_ID_NM = 'switch_chassis_id' +LLDP_PORT_ID_NM = 'switch_port_id' +LLDP_PORT_DESC_NM = 'switch_port_description' +LLDP_SYS_NAME_NM = 'switch_system_name' +LLDP_SYS_DESC_NM = 'switch_system_description' +LLDP_SWITCH_CAP_NM = 'switch_capabilities' +LLDP_CAP_SUPPORT_NM = 'switch_capabilities_support' +LLDP_CAP_ENABLED_NM = 'switch_capabilities_enabled' +LLDP_MGMT_ADDRESSES_NM = 'switch_mgmt_addresses' +LLDP_PORT_VLANID_NM = 'switch_port_untagged_vlan_id' +LLDP_PORT_PROT_NM = 'switch_port_protocol' +LLDP_PORT_PROT_VLAN_ENABLED_NM = 'switch_port_protocol_vlan_enabled' +LLDP_PORT_PROT_VLAN_SUPPORT_NM = 'switch_port_protocol_vlan_support' +LLDP_PORT_PROT_VLANIDS_NM = 'switch_port_protocol_vlan_ids' +LLDP_PORT_VLANS_NM = 'switch_port_vlans' +LLDP_PROTOCOL_IDENTITIES_NM = 'switch_protocol_identities' +LLDP_PORT_MGMT_VLANID_NM = 'switch_port_management_vlan_id' +LLDP_PORT_LINK_AGG_NM = 'switch_port_link_aggregation' +LLDP_PORT_LINK_AGG_ENABLED_NM = 'switch_port_link_aggregation_enabled' +LLDP_PORT_LINK_AGG_SUPPORT_NM = 'switch_port_link_aggregation_support' +LLDP_PORT_LINK_AGG_ID_NM = 'switch_port_link_aggregation_id' +LLDP_PORT_MAC_PHY_NM = 'switch_port_mac_phy_config' +LLDP_PORT_LINK_AUTONEG_ENABLED_NM = 'switch_port_autonegotiation_enabled' +LLDP_PORT_LINK_AUTONEG_SUPPORT_NM = 'switch_port_autonegotiation_support' +LLDP_PORT_CAPABILITIES_NM = 'switch_port_physical_capabilities' +LLDP_PORT_MAU_TYPE_NM = 'switch_port_mau_type' +LLDP_MTU_NM = 'switch_port_mtu' + + +class LLDPParser(object): + """Base class to handle parsing of LLDP TLVs""" + + def __init__(self, node_info, nv=None): + """Create LLDPParser + + :param node_info - node being introspected + :param nv - dictionary of name/value pairs to use + """ + if not nv: + self.nv_dict = {} + else: + self.nv_dict = nv + + self.node_info = node_info + + # Parser maps are used to associate a LLDP TLV with a function handler + # and arguments necessary to parse the TLV and generate one or more + # name/value pairs. Each LLDP TLV maps to a tuple with the values: + # function - handler function to generate name/value pairs + # construct - name of construct definition for TLV + # name - user-friendly name of TLV. For TLVs that generate only + # one name/value pair this is the name used + # len_check - boolean that indicates whether a len check + # should be done on the construct + # + # Its valid to have a function handler of None, this is for TLVs that + # are not mapped to a name/value pair (e.g. LLDP_TLV_TTL). + # + # Each class that inherits from this base class must provide a + # parser map. + + self.parser_map = {} + + def set_value(self, name, value): + """Set name value pair in dictionary""" + self.nv_dict.setdefault(name, value) # don't change key if it exists + + def append_value(self, name, value): + """Add value to a list mapped to name""" + self.nv_dict.setdefault(name, []).append(value) + + def add_single_value(self, struct, name, data): + """Add a single name/value pair the the nv dict""" + self.set_value(name, struct.value) + + def parse_tlv(self, tlv_type, data): + """Parse TLVs from mapping table + + :param: tlv_type - type identifier for TLV + :param: data - raw TLV value + """ + + # The handler function will generate name/value pairs using the + # tlv construct definition. If the function does not exist, then no + # name/value pairs will be added, but since the TLV was handled, + # True will be returned + s = self.parser_map.get(tlv_type) + if s: + func = s[0] # handler + if func: + try: + tlv_parser = s[1] + name = s[2] + check_len = s[3] + except KeyError as e: + LOG.warning(_LW("Key error in TLV table: %s"), e, + node_info=self.node_info) + return False + + # Some constructs require a length validation to ensure the + # proper number of bytes has been provided, for example + # when a BitStruct is used. + if check_len and (tlv_parser.sizeof() != len(data)): + LOG.warning(_LW('Invalid data for %(name)s ' + 'expected len %(expect)d, got %(actual)d'), + {'name': name, 'expect': tlv_parser.sizeof(), + 'actual': len(data)}) + return False + + # Use the construct parser to parse TLV so that it's + # individual fields can be accessed + try: + struct = tlv_parser.parse(data) + except (core.RangeError, core.FieldError, core.MappingError, + netaddr.AddrFormatError) as e: + LOG.warning(_LW("TLV parse error: %s"), e, + node_info=self.node_info) + return False + + # Call functions with parsed structure + try: + func(struct, name, data) + except ValueError as e: + LOG.warning(_LW("TLV value error: %s"), e, + node_info=self.node_info) + return True + + return False + + # This method is in base class since it can be used by both dot1 and dot3 + def add_dot1_link_aggregation(self, struct, name, data): + + self.set_value(LLDP_PORT_LINK_AGG_ENABLED_NM, + struct.status.enabled) + self.set_value(LLDP_PORT_LINK_AGG_SUPPORT_NM, + struct.status.supported) + self.set_value(LLDP_PORT_LINK_AGG_ID_NM, struct.portid) + + +class LLDPBasicMgmtParser(LLDPParser): + """Class to handle parsing of 802.1AB Basic Management set + + This class will also handle 802.1Q and 802.3 OUI TLVs + """ + def __init__(self, nv=None): + super(LLDPBasicMgmtParser, self).__init__(nv) + + self.parser_map = { + tlv.LLDP_TLV_CHASSIS_ID: + (self.add_single_value, tlv.ChassisId, + LLDP_CHASSIS_ID_NM, False), + tlv.LLDP_TLV_PORT_ID: + (self.add_single_value, tlv.PortId, LLDP_PORT_ID_NM, False), + tlv.LLDP_TLV_TTL: (None, None, None, False), + tlv.LLDP_TLV_PORT_DESCRIPTION: + (self.add_single_value, tlv.PortDesc, LLDP_PORT_DESC_NM, + False), + tlv.LLDP_TLV_SYS_NAME: + (self.add_single_value, tlv.SysName, LLDP_SYS_NAME_NM, False), + tlv.LLDP_TLV_SYS_DESCRIPTION: + (self.add_single_value, tlv.SysDesc, LLDP_SYS_DESC_NM, False), + tlv.LLDP_TLV_SYS_CAPABILITIES: + (self.add_capabilities, tlv.SysCapabilities, + LLDP_SWITCH_CAP_NM, True), + tlv.LLDP_TLV_MGMT_ADDRESS: + (self.add_mgmt_address, tlv.MgmtAddress, + LLDP_MGMT_ADDRESSES_NM, False), + tlv.LLDP_TLV_ORG_SPECIFIC: + (self.handle_org_specific_tlv, tlv.OrgSpecific, None, False), + tlv.LLDP_TLV_END_LLDPPDU: (None, None, None, False) + } + + def add_mgmt_address(self, struct, name, data): + """Handle LLDP_TLV_MGMT_ADDRESS""" + # There may be multiple Mgmt Address TLVs so store in list + self.append_value(name, struct.address) + + def _get_capabilities_list(self, caps): + """Get capabilities from bit map""" + cap_map = [ + (caps.repeater, 'Repeater'), + (caps.bridge, 'Bridge'), + (caps.wlan, 'WLAN'), + (caps.router, 'Router'), + (caps.telephone, 'Telephone'), + (caps.docsis, 'DOCSIS cable device'), + (caps.station, 'Station only'), + (caps.cvlan, 'C-Vlan'), + (caps.svlan, 'S-Vlan'), + (caps.tpmr, 'TPMR')] + + return [cap for (bit, cap) in cap_map if bit] + + def add_capabilities(self, struct, name, data): + """Handle LLDP_TLV_SYS_CAPABILITIES""" + self.set_value(LLDP_CAP_SUPPORT_NM, + self._get_capabilities_list(struct.system)) + self.set_value(LLDP_CAP_ENABLED_NM, + self._get_capabilities_list(struct.enabled)) + + def handle_org_specific_tlv(self, struct, name, data): + """Handle Organizationally Unique ID TLVs + + This class supports 802.1Q and 802.3 OUI TLVs + See http://www.ieee802.org/1/pages/802.1Q-2014.html, Annex D + and http: // standards.ieee.org / about / get / 802 / 802.3.html + """ + oui = binascii.hexlify(struct.oui).decode() + subtype = struct.subtype + oui_data = data[4:] + + if oui == tlv.LLDP_802dot1_OUI: + parser = LLDPdot1Parser(self.node_info, self.nv_dict) + if parser.parse_tlv(subtype, oui_data): + LOG.debug("Handled 802.1 subtype %d", subtype) + else: + LOG.debug("Subtype %d not found for 802.1", subtype) + elif oui == tlv.LLDP_802dot3_OUI: + parser = LLDPdot3Parser(self.node_info, self.nv_dict) + if parser.parse_tlv(subtype, oui_data): + LOG.debug("Handled 802.3 subtype %d", subtype) + else: + LOG.debug("Subtype %d not found for 802.3", subtype) + else: + LOG.debug("Organizationally Unique ID %s not " + "recognized", oui) + + +class LLDPdot1Parser(LLDPParser): + """Class to handle parsing of 802.1Q TLVs""" + def __init__(self, node_info, nv=None): + super(LLDPdot1Parser, self).__init__(node_info, nv) + + self.parser_map = { + tlv.dot1_PORT_VLANID: + (self.add_single_value, tlv.Dot1_UntaggedVlanId, + LLDP_PORT_VLANID_NM, False), + tlv.dot1_PORT_PROTOCOL_VLANID: + (self.add_dot1_port_protocol_vlan, tlv.Dot1_PortProtocolVlan, + LLDP_PORT_PROT_NM, True), + tlv.dot1_VLAN_NAME: + (self.add_dot1_vlans, tlv.Dot1_VlanName, None, False), + tlv.dot1_PROTOCOL_IDENTITY: + (self.add_dot1_protocol_identities, tlv.Dot1_ProtocolIdentity, + LLDP_PROTOCOL_IDENTITIES_NM, False), + tlv.dot1_MANAGEMENT_VID: + (self.add_single_value, tlv.Dot1_MgmtVlanId, + LLDP_PORT_MGMT_VLANID_NM, False), + tlv.dot1_LINK_AGGREGATION: + (self.add_dot1_link_aggregation, tlv.Dot1_LinkAggregationId, + LLDP_PORT_LINK_AGG_NM, True) + } + + def add_dot1_port_protocol_vlan(self, struct, name, data): + """Handle dot1_PORT_PROTOCOL_VLANID""" + self.set_value(LLDP_PORT_PROT_VLAN_ENABLED_NM, struct.flags.enabled) + self.set_value(LLDP_PORT_PROT_VLAN_SUPPORT_NM, struct.flags.supported) + # There can be multiple port/protocol vlans TLVs, store in list + self.append_value(LLDP_PORT_PROT_VLANIDS_NM, struct.vlanid) + + def add_dot1_vlans(self, struct, name, data): + """Handle dot1_VLAN_NAME""" + + # There can be multiple vlan TLVs, add dictionary entry with id/vlan + vlan_dict = {} + vlan_dict['name'] = struct.vlan_name + vlan_dict['id'] = struct.vlanid + self.append_value(LLDP_PORT_VLANS_NM, vlan_dict) + + def add_dot1_protocol_identities(self, struct, name, data): + """handle dot1_PROTOCOL_IDENTITY""" + + # There can be multiple protocol ids TLVs, store in list + self.append_value(LLDP_PROTOCOL_IDENTITIES_NM, + binascii.b2a_hex(struct.protocol).decode()) + + +class LLDPdot3Parser(LLDPParser): + """Class to handle parsing of 802.3 TLVs""" + def __init__(self, node_info, nv=None): + super(LLDPdot3Parser, self).__init__(node_info, nv) + + # Note that 802.3 link Aggregation has been deprecated and moved to + # 802.1 spec, but it is in the same format. Use the same function as + # dot1 handler. + self.parser_map = { + tlv.dot3_MACPHY_CONFIG_STATUS: + (self.add_dot3_macphy_config, tlv.Dot3_MACPhy_Config_Status, + LLDP_PORT_MAC_PHY_NM, True), + tlv.dot3_LINK_AGGREGATION: + (self.add_dot1_link_aggregation, tlv.Dot1_LinkAggregationId, + LLDP_PORT_LINK_AGG_NM, True), + tlv.dot3_MTU: + (self.add_single_value, tlv.Dot3_MTU, LLDP_MTU_NM, False) + } + + def add_dot3_macphy_config(self, struct, name, data): + """Handle dot3_MACPHY_CONFIG_STATUS""" + + try: + mau_type = tlv.OPER_MAU_TYPES[struct.mau_type] + except KeyError: + raise ValueError(_('Invalid index for mau type')) + + self.set_value(LLDP_PORT_LINK_AUTONEG_ENABLED_NM, + struct.autoneg.enabled) + self.set_value(LLDP_PORT_LINK_AUTONEG_SUPPORT_NM, + struct.autoneg.supported) + self.set_value(LLDP_PORT_CAPABILITIES_NM, + tlv.get_autoneg_cap(struct.pmd_autoneg)) + self.set_value(LLDP_PORT_MAU_TYPE_NM, mau_type) diff --git a/ironic_inspector/common/lldp_tlvs.py b/ironic_inspector/common/lldp_tlvs.py new file mode 100644 index 000000000..9b85861b5 --- /dev/null +++ b/ironic_inspector/common/lldp_tlvs.py @@ -0,0 +1,366 @@ +# 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. + +""" Link Layer Discovery Protocol TLVs """ + +import functools + +# See http://construct.readthedocs.io/en/latest/index.html +import construct +from construct import core +import netaddr + +from ironic_inspector import utils + +LOG = utils.getProcessingLogger(__name__) + +# Constants defined according to 802.1AB-2016 LLDP spec +# https://standards.ieee.org/findstds/standard/802.1AB-2016.html + +# TLV types +LLDP_TLV_END_LLDPPDU = 0 +LLDP_TLV_CHASSIS_ID = 1 +LLDP_TLV_PORT_ID = 2 +LLDP_TLV_TTL = 3 +LLDP_TLV_PORT_DESCRIPTION = 4 +LLDP_TLV_SYS_NAME = 5 +LLDP_TLV_SYS_DESCRIPTION = 6 +LLDP_TLV_SYS_CAPABILITIES = 7 +LLDP_TLV_MGMT_ADDRESS = 8 +LLDP_TLV_ORG_SPECIFIC = 127 + +# 802.1Q defines from http://www.ieee802.org/1/pages/802.1Q-2014.html, Annex D +LLDP_802dot1_OUI = "0080c2" +# subtypes +dot1_PORT_VLANID = 1 +dot1_PORT_PROTOCOL_VLANID = 2 +dot1_VLAN_NAME = 3 +dot1_PROTOCOL_IDENTITY = 4 +dot1_MANAGEMENT_VID = 6 +dot1_LINK_AGGREGATION = 7 + +# 802.3 defines from http://standards.ieee.org/about/get/802/802.3.html, +# section 79 +LLDP_802dot3_OUI = "00120f" +# Subtypes +dot3_MACPHY_CONFIG_STATUS = 1 +dot3_LINK_AGGREGATION = 3 # Deprecated, but still in use +dot3_MTU = 4 + + +def bytes_to_int(obj): + """Convert bytes to an integer + + :param: obj - array of bytes + """ + return functools.reduce(lambda x, y: x << 8 | y, obj) + + +def mapping_for_enum(mapping): + """Return tuple used for keys as a dict + + :param: mapping - dict with tuple as keys + """ + return dict(mapping.keys()) + + +def mapping_for_switch(mapping): + """Return dict from values + + :param: mapping - dict with tuple as keys + """ + return {key[0]: value for key, value in mapping.items()} + + +IPv4Address = core.ExprAdapter( + core.Byte[4], + encoder=lambda obj, ctx: netaddr.IPAddress(obj).words, + decoder=lambda obj, ctx: str(netaddr.IPAddress(bytes_to_int(obj))) +) + +IPv6Address = core.ExprAdapter( + core.Byte[16], + encoder=lambda obj, ctx: netaddr.IPAddress(obj).words, + decoder=lambda obj, ctx: str(netaddr.IPAddress(bytes_to_int(obj))) +) + +MACAddress = core.ExprAdapter( + core.Byte[6], + encoder=lambda obj, ctx: netaddr.EUI(obj).words, + decoder=lambda obj, ctx: str(netaddr.EUI(bytes_to_int(obj), + dialect=netaddr.mac_unix_expanded)) +) + +IANA_ADDRESS_FAMILY_ID_MAPPING = { + ('ipv4', 1): IPv4Address, + ('ipv6', 2): IPv6Address, + ('mac', 6): MACAddress, +} + +IANAAddress = core.Embedded(core.Struct( + 'family' / core.Enum(core.Int8ub, **mapping_for_enum( + IANA_ADDRESS_FAMILY_ID_MAPPING)), + 'value' / core.Switch(construct.this.family, mapping_for_switch( + IANA_ADDRESS_FAMILY_ID_MAPPING)))) + +# Note that 'GreedyString()' is used in cases where string len is not defined +CHASSIS_ID_MAPPING = { + ('entPhysAlias_c', 1): core.Struct('value' / core.GreedyString("utf8")), + ('ifAlias', 2): core.Struct('value' / core.GreedyString("utf8")), + ('entPhysAlias_p', 3): core.Struct('value' / core.GreedyString("utf8")), + ('mac_address', 4): core.Struct('value' / MACAddress), + ('IANA_address', 5): IANAAddress, + ('ifName', 6): core.Struct('value' / core.GreedyString("utf8")), + ('local', 7): core.Struct('value' / core.GreedyString("utf8")) +} + +# +# Basic Management Set TLV field definitions +# + +# Chassis ID value is based on the subtype +ChassisId = core.Struct( + 'subtype' / core.Enum(core.Byte, **mapping_for_enum( + CHASSIS_ID_MAPPING)), + 'value' / + core.Embedded(core.Switch(construct.this.subtype, + mapping_for_switch(CHASSIS_ID_MAPPING))) +) + +PORT_ID_MAPPING = { + ('ifAlias', 1): core.Struct('value' / core.GreedyString("utf8")), + ('entPhysicalAlias', 2): core.Struct('value' / core.GreedyString("utf8")), + ('mac_address', 3): core.Struct('value' / MACAddress), + ('IANA_address', 4): IANAAddress, + ('ifName', 5): core.Struct('value' / core.GreedyString("utf8")), + ('local', 7): core.Struct('value' / core.GreedyString("utf8")) +} + +# Port ID value is based on the subtype +PortId = core.Struct( + 'subtype' / core.Enum(core.Byte, **mapping_for_enum( + PORT_ID_MAPPING)), + 'value' / + core.Embedded(core.Switch(construct.this.subtype, + mapping_for_switch(PORT_ID_MAPPING))) +) + +PortDesc = core.Struct('value' / core.GreedyString("utf8")) + +SysName = core.Struct('value' / core.GreedyString("utf8")) + +SysDesc = core.Struct('value' / core.GreedyString("utf8")) + +MgmtAddress = core.Struct( + 'len' / core.Int8ub, + 'family' / core.Enum(core.Int8ub, **mapping_for_enum( + IANA_ADDRESS_FAMILY_ID_MAPPING)), + 'address' / core.Switch(construct.this.family, mapping_for_switch( + IANA_ADDRESS_FAMILY_ID_MAPPING)) +) + +Capabilities = core.BitStruct( + core.Padding(5), + 'tpmr' / core.Bit, + 'svlan' / core.Bit, + 'cvlan' / core.Bit, + 'station' / core.Bit, + 'docsis' / core.Bit, + 'telephone' / core.Bit, + 'router' / core.Bit, + 'wlan' / core.Bit, + 'bridge' / core.Bit, + 'repeater' / core.Bit, + core.Padding(1) +) + +SysCapabilities = core.Struct( + 'system' / Capabilities, + 'enabled' / Capabilities +) + +OrgSpecific = core.Struct( + 'oui' / core.Bytes(3), + 'subtype' / core.Int8ub +) + +# +# 802.1Q TLV field definitions +# See http://www.ieee802.org/1/pages/802.1Q-2014.html, Annex D +# + +Dot1_UntaggedVlanId = core.Struct('value' / core.Int16ub) + +Dot1_PortProtocolVlan = core.Struct( + 'flags' / core.BitStruct( + core.Padding(5), + 'enabled' / core.Flag, + 'supported' / core.Flag, + core.Padding(1), + ), + 'vlanid' / core.Int16ub +) + +Dot1_VlanName = core.Struct( + 'vlanid' / core.Int16ub, + 'name_len' / core.Rebuild(core.Int8ub, + construct.len_(construct.this.value)), + 'vlan_name' / core.String(construct.this.name_len, "utf8") +) + +Dot1_ProtocolIdentity = core.Struct( + 'len' / core.Rebuild(core.Int8ub, construct.len_(construct.this.value)), + 'protocol' / core.Bytes(construct.this.len) +) + +Dot1_MgmtVlanId = core.Struct('value' / core.Int16ub) + +Dot1_LinkAggregationId = core.Struct( + 'status' / core.BitStruct( + core.Padding(6), + 'enabled' / core.Flag, + 'supported' / core.Flag + ), + 'portid' / core.Int32ub +) + +# +# 802.3 TLV field definitions +# See http://standards.ieee.org/about/get/802/802.3.html, +# section 79 +# + + +def get_autoneg_cap(pmd): + """Get autonegotiated capability strings + + This returns a list of capability strings from the Physical Media + Dependent (PMD) capability bits. + + :param pmd: PMD bits + :return: Sorted ist containing capability strings + """ + caps_set = set() + + pmd_map = [ + (pmd._10base_t_hdx, '10BASE-T hdx'), + (pmd._10base_t_hdx, '10BASE-T fdx'), + (pmd._10base_t4, '10BASE-T4'), + (pmd._100base_tx_hdx, '100BASE-TX hdx'), + (pmd._100base_tx_fdx, '100BASE-TX fdx'), + (pmd._100base_t2_hdx, '100BASE-T2 hdx'), + (pmd._100base_t2_fdx, '100BASE-T2 fdx'), + (pmd.pause_fdx, 'PAUSE fdx'), + (pmd.asym_pause, 'Asym PAUSE fdx'), + (pmd.sym_pause, 'Sym PAUSE fdx'), + (pmd.asym_sym_pause, 'Asym and Sym PAUSE fdx'), + (pmd._1000base_x_hdx, '1000BASE-X hdx'), + (pmd._1000base_x_fdx, '1000BASE-X fdx'), + (pmd._1000base_t_hdx, '1000BASE-T hdx'), + (pmd._1000base_t_fdx, '1000BASE-T fdx')] + + for bit, cap in pmd_map: + if bit: + caps_set.add(cap) + + return sorted(caps_set) + +Dot3_MACPhy_Config_Status = core.Struct( + 'autoneg' / core.BitStruct( + core.Padding(6), + 'enabled' / core.Flag, + 'supported' / core.Flag, + ), + # See IANAifMauAutoNegCapBits + # RFC 4836, Definitions of Managed Objects for IEEE 802.3 + 'pmd_autoneg' / core.BitStruct( + core.Padding(1), + '_10base_t_hdx' / core.Bit, + '_10base_t_fdx' / core.Bit, + '_10base_t4' / core.Bit, + '_100base_tx_hdx' / core.Bit, + '_100base_tx_fdx' / core.Bit, + '_100base_t2_hdx' / core.Bit, + '_100base_t2_fdx' / core.Bit, + 'pause_fdx' / core.Bit, + 'asym_pause' / core.Bit, + 'sym_pause' / core.Bit, + 'asym_sym_pause' / core.Bit, + '_1000base_x_hdx' / core.Bit, + '_1000base_x_fdx' / core.Bit, + '_1000base_t_hdx' / core.Bit, + '_1000base_t_fdx' / core.Bit + ), + 'mau_type' / core.Int16ub +) + +# See ifMauTypeList in +# RFC 4836, Definitions of Managed Objects for IEEE 802.3 +OPER_MAU_TYPES = { + 0: "Unknown", + 1: "AUI", + 2: "10BASE-5", + 3: "FOIRL", + 4: "10BASE-2", + 5: "10BASE-T duplex mode unknown", + 6: "10BASE-FP", + 7: "10BASE-FB", + 8: "10BASE-FL duplex mode unknown", + 9: "10BROAD36", + 10: "10BASE-T half duplex", + 11: "10BASE-T full duplex", + 12: "10BASE-FL half duplex", + 13: "10BASE-FL full duplex", + 14: "100 BASE-T4", + 15: "100BASE-TX half duplex", + 16: "100BASE-TX full duplex", + 17: "100BASE-FX half duplex", + 18: "100BASE-FX full duplex", + 19: "100BASE-T2 half duplex", + 20: "100BASE-T2 full duplex", + 21: "1000BASE-X half duplex", + 22: "1000BASE-X full duplex", + 23: "1000BASE-LX half duplex", + 24: "1000BASE-LX full duplex", + 25: "1000BASE-SX half duplex", + 26: "1000BASE-SX full duplex", + 27: "1000BASE-CX half duplex", + 28: "1000BASE-CX full duplex", + 29: "1000BASE-T half duplex", + 30: "1000BASE-T full duplex", + 31: "10GBASE-X", + 32: "10GBASE-LX4", + 33: "10GBASE-R", + 34: "10GBASE-ER", + 35: "10GBASE-LR", + 36: "10GBASE-SR", + 37: "10GBASE-W", + 38: "10GBASE-EW", + 39: "10GBASE-LW", + 40: "10GBASE-SW", + 41: "10GBASE-CX4", + 42: "2BASE-TL", + 43: "10PASS-TS", + 44: "100BASE-BX10D", + 45: "100BASE-BX10U", + 46: "100BASE-LX10", + 47: "1000BASE-BX10D", + 48: "1000BASE-BX10U", + 49: "1000BASE-LX10", + 50: "1000BASE-PX10D", + 51: "1000BASE-PX10U", + 52: "1000BASE-PX20D", + 53: "1000BASE-PX20U", +} + +Dot3_MTU = core.Struct('value' / core.Int16ub) diff --git a/ironic_inspector/plugins/lldp_basic.py b/ironic_inspector/plugins/lldp_basic.py new file mode 100644 index 000000000..f75a2facd --- /dev/null +++ b/ironic_inspector/plugins/lldp_basic.py @@ -0,0 +1,88 @@ +# 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. + +"""LLDP Processing Hook for basic TLVs""" + +import binascii + +from ironic_inspector.common.i18n import _LW +from ironic_inspector.common import lldp_parsers +from ironic_inspector.plugins import base +from ironic_inspector import utils + +LOG = utils.getProcessingLogger(__name__) + + +class LLDPBasicProcessingHook(base.ProcessingHook): + """Process mandatory and optional LLDP packet fields + + Loop through raw LLDP TLVs and parse those from the + basic management, 802.1, and 802.3 TLV sets. + Store parsed data back to the ironic-inspector database. + """ + + def _parse_lldp_tlvs(self, tlvs, node_info): + """Parse LLDP TLVs into dictionary of name/value pairs + + :param tlvs: list of raw TLVs + :param node_info: node being introspected + :returns nv: dictionary of name/value pairs. The + LLDP user-friendly names, e.g. + "switch_port_id" are the keys + """ + + # Generate name/value pairs for each TLV supported by this plugin. + parser = lldp_parsers.LLDPBasicMgmtParser(node_info) + + for tlv_type, tlv_value in tlvs: + try: + data = bytearray(binascii.a2b_hex(tlv_value)) + except TypeError as e: + LOG.warning(_LW( + "TLV value for TLV type %(tlv_type)d not in correct " + "format, value must be in hexadecimal: %(msg)s"), + {'tlv_type': tlv_type, 'msg': e}) + continue + + if parser.parse_tlv(tlv_type, data): + LOG.debug("Handled TLV type %d", + tlv_type, node_info=node_info) + else: + LOG.debug("LLDP TLV type %d not handled", + tlv_type, node_info=node_info) + + return parser.nv_dict + + def before_update(self, introspection_data, node_info, **kwargs): + """Process LLDP data and update all_interfaces with processed data""" + + inventory = utils.get_inventory(introspection_data) + + for iface in inventory['interfaces']: + if_name = iface['name'] + + tlvs = iface.get('lldp') + if tlvs is None: + LOG.warning(_LW("No LLDP Data found for interface %s"), + if_name, node_info=node_info) + continue + + LOG.debug("Processing LLDP Data for interface %s", + if_name, node_info=node_info) + + nv = self._parse_lldp_tlvs(tlvs, node_info) + + if nv: + # Store lldp data per interface in "all_interfaces" + iface_to_update = introspection_data['all_interfaces'][if_name] + iface_to_update['lldp_processed'] = nv diff --git a/ironic_inspector/test/base.py b/ironic_inspector/test/base.py index 11dabdd35..d7bb4e6ec 100644 --- a/ironic_inspector/test/base.py +++ b/ironic_inspector/test/base.py @@ -100,7 +100,11 @@ class InventoryTest(BaseTest): 'inventory': { 'interfaces': [ {'name': 'eth1', 'mac_address': self.macs[0], - 'ipv4_address': self.ips[0]}, + 'ipv4_address': self.ips[0], + 'lldp': [ + [1, "04112233aabbcc"], + [2, "07373334"], + [3, "003c"]]}, {'name': 'eth2', 'mac_address': self.inactive_mac}, {'name': 'eth3', 'mac_address': self.macs[1], 'ipv4_address': self.ips[1]}, diff --git a/ironic_inspector/test/functional.py b/ironic_inspector/test/functional.py index 213a8c3d7..9611137b0 100644 --- a/ironic_inspector/test/functional.py +++ b/ironic_inspector/test/functional.py @@ -60,6 +60,8 @@ debug = True auth_strategy = noauth [database] connection = sqlite:///%(db_file)s +[processing] +processing_hooks=$default_processing_hooks,lldp_basic """ @@ -713,6 +715,41 @@ class Test(Base): status = self.call_get_status(self.uuid) self.check_status(status, finished=True) + @mock.patch.object(swift, 'store_introspection_data', autospec=True) + @mock.patch.object(swift, 'get_introspection_data', autospec=True) + def test_lldp_plugin(self, get_mock, store_mock): + cfg.CONF.set_override('store_data', 'swift', 'processing') + + ramdisk_data = json.dumps(copy.deepcopy(self.data)) + get_mock.return_value = ramdisk_data + + self.call_introspect(self.uuid) + eventlet.greenthread.sleep(DEFAULT_SLEEP) + self.cli.node.set_power_state.assert_called_once_with(self.uuid, + 'reboot') + + status = self.call_get_status(self.uuid) + self.check_status(status, finished=False) + + res = self.call_continue(self.data) + self.assertEqual({'uuid': self.uuid}, res) + eventlet.greenthread.sleep(DEFAULT_SLEEP) + + status = self.call_get_status(self.uuid) + self.check_status(status, finished=True) + + # Verify that the lldp_processed data is written to swift + # as expected by the lldp plugin + updated_data = store_mock.call_args[0][0] + lldp_out = updated_data['all_interfaces']['eth1'] + + expected_chassis_id = "11:22:33:aa:bb:cc" + expected_port_id = "734" + self.assertEqual(expected_chassis_id, + lldp_out['lldp_processed']['switch_chassis_id']) + self.assertEqual(expected_port_id, + lldp_out['lldp_processed']['switch_port_id']) + @contextlib.contextmanager def mocked_server(): diff --git a/ironic_inspector/test/unit/test_plugins_lldp_basic.py b/ironic_inspector/test/unit/test_plugins_lldp_basic.py new file mode 100644 index 000000000..ce58932f8 --- /dev/null +++ b/ironic_inspector/test/unit/test_plugins_lldp_basic.py @@ -0,0 +1,329 @@ +# 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 ironic_inspector.common import lldp_parsers as nv +from ironic_inspector.plugins import lldp_basic +from ironic_inspector.test import base as test_base + + +class TestLLDPBasicProcessingHook(test_base.NodeTest): + hook = lldp_basic.LLDPBasicProcessingHook() + + def setUp(self): + super(TestLLDPBasicProcessingHook, self).setUp() + self.data = { + 'inventory': { + 'interfaces': [{ + 'name': 'em1', + }], + 'cpu': 1, + 'disks': 1, + 'memory': 1 + }, + 'all_interfaces': + { + 'em1': {'mac': self.macs[0], 'ip': self.ips[0]} + } + } + + self.expected = {"em1": {"ip": self.ips[0], "mac": self.macs[0]}} + + def test_all_valid_data(self): + + self.data['inventory']['interfaces'] = [{ + 'name': 'em1', + 'lldp': [ + [1, "04112233aabbcc"], # ChassisId + [2, "07373334"], # PortId + [3, "003c"], # TTL + [4, "686f737430322e6c61622e656e6720706f7274203320" + "28426f6e6429"], # PortDesc + [5, "737730312d646973742d31622d623132"], # SysName + [6, "4e6574776f726b732c20496e632e20353530302c2076657273696f" + "6e203132204275696c6420646174653a20323031342d30332d31332030" + "383a33383a33302055544320"], # SysDesc + [7, "00140014"], # SysCapabilities + [8, "0501c000020f020000000000"], # MgmtAddress + [8, "110220010db885a3000000008a2e03707334020000000000"], + [8, "0706aa11bb22cc3302000003e900"], # MgmtAddress + [127, "00120f01036c110010"], # dot3 MacPhyConfigStatus + [127, "00120f030300000002"], # dot3 LinkAggregation + [127, "00120f0405ea"], # dot3 MTU + [127, "0080c2010066"], # dot1 PortVlan + [127, "0080c20206000a"], # dot1 PortProtocolVlanId + [127, "0080c202060014"], # dot1 PortProtocolVlanId + [127, "0080c204080026424203000000"], # dot1 ProtocolIdentity + [127, "0080c203006507766c616e313031"], # dot1 VlanName + [127, "0080c203006607766c616e313032"], # dot1 VlanName + [127, "0080c203006807766c616e313034"], # dot1 VlanName + [127, "0080c2060058"], # dot1 MgmtVID + [0, ""]] + }] + + expected = { + nv.LLDP_CAP_ENABLED_NM: ['Bridge', 'Router'], + nv.LLDP_CAP_SUPPORT_NM: ['Bridge', 'Router'], + nv.LLDP_CHASSIS_ID_NM: "11:22:33:aa:bb:cc", + nv.LLDP_MGMT_ADDRESSES_NM: ['192.0.2.15', + '2001:db8:85a3::8a2e:370:7334', + 'aa:11:bb:22:cc:33'], + nv.LLDP_PORT_LINK_AUTONEG_ENABLED_NM: True, + nv.LLDP_PORT_LINK_AUTONEG_ENABLED_NM: True, + nv.LLDP_PORT_DESC_NM: 'host02.lab.eng port 3 (Bond)', + nv.LLDP_PORT_ID_NM: '734', + nv.LLDP_PORT_LINK_AGG_ENABLED_NM: True, + nv.LLDP_PORT_LINK_AGG_ID_NM: 2, + nv.LLDP_PORT_LINK_AGG_SUPPORT_NM: True, + nv.LLDP_PORT_MGMT_VLANID_NM: 88, + nv.LLDP_PORT_MAU_TYPE_NM: '100BASE-TX full duplex', + nv.LLDP_MTU_NM: 1514, + nv.LLDP_PORT_CAPABILITIES_NM: ['1000BASE-T fdx', + '100BASE-TX fdx', + '100BASE-TX hdx', + '10BASE-T fdx', + '10BASE-T hdx', + 'Asym and Sym PAUSE fdx'], + nv.LLDP_PORT_PROT_VLAN_ENABLED_NM: True, + nv.LLDP_PORT_PROT_VLANIDS_NM: [10, 20], + nv.LLDP_PORT_PROT_VLAN_SUPPORT_NM: True, + nv.LLDP_PORT_VLANID_NM: 102, + nv.LLDP_PORT_VLANS_NM: [{'id': 101, 'name': 'vlan101'}, + {'id': 102, 'name': 'vlan102'}, + {'id': 104, "name": 'vlan104'}], + nv.LLDP_PROTOCOL_IDENTITIES_NM: ['0026424203000000'], + nv.LLDP_SYS_DESC_NM: 'Networks, Inc. 5500, version 12' + ' Build date: 2014-03-13 08:38:30 UTC ', + nv.LLDP_SYS_NAME_NM: 'sw01-dist-1b-b12' + } + + self.hook.before_update(self.data, self.node_info) + + actual_all_int = self.data['all_interfaces'] + actual = actual_all_int['em1']['lldp_processed'] + + for name, value in expected.items(): + if name is nv.LLDP_PORT_VLANS_NM: + for d1, d2 in zip(expected[name], actual[name]): + for key, value in d1.items(): + self.assertEqual(d2[key], value) + else: + self.assertEqual(actual[name], expected[name]) + + def test_multiple_interfaces(self): + self.data = { + 'inventory': { + 'interfaces': [ + {'name': 'em1', + 'lldp': [ + [1, "04112233aabbcc"], + [2, "07373334"], + [3, "003c"]]}, + {'name': 'em2', + 'lldp': [ + [1, "04112233aabbdd"], + [2, "07373838"], + [3, "003c"]]}, + {'name': 'em3', + 'lldp': [ + [1, "04112233aabbee"], + [2, "07373939"], + [3, "003c"]]}], + 'cpu': 1, + 'disks': 1, + 'memory': 1 + }, + 'all_interfaces': + { + 'em1': {'mac': self.macs[0], 'ip': self.ips[0]}, + 'em2': {'mac': self.macs[0], 'ip': self.ips[0]}, + 'em3': {'mac': self.macs[0], 'ip': self.ips[0]} + } + } + + expected = {"em1": {"ip": self.ips[0], "mac": self.macs[0], + "lldp_processed": { + nv.LLDP_CHASSIS_ID_NM: "11:22:33:aa:bb:cc", + nv.LLDP_PORT_ID_NM: "734"}}, + "em2": {"ip": self.ips[0], "mac": self.macs[0], + "lldp_processed": { + nv.LLDP_CHASSIS_ID_NM: "11:22:33:aa:bb:dd", + nv.LLDP_PORT_ID_NM: "788"}}, + "em3": {"ip": self.ips[0], "mac": self.macs[0], + "lldp_processed": { + nv.LLDP_CHASSIS_ID_NM: "11:22:33:aa:bb:ee", + nv.LLDP_PORT_ID_NM: "799"}}} + + self.hook.before_update(self.data, self.node_info) + self.assertEqual(expected, self.data['all_interfaces']) + + def test_chassis_ids(self): + # Test IPv4 address + self.data['inventory']['interfaces'] = [{ + 'name': 'em1', + 'lldp': [ + [1, "0501c000020f"], + ]}] + + self.expected['em1']['lldp_processed'] = { + nv.LLDP_CHASSIS_ID_NM: "192.0.2.15" + } + self.hook.before_update(self.data, self.node_info) + self.assertEqual(self.expected, self.data['all_interfaces']) + + # Test name + self.data['inventory']['interfaces'] = [{ + 'name': 'em1', + 'lldp': [ + [1, "0773773031"], + ]}] + + self.expected['em1']['lldp_processed'] = { + nv.LLDP_CHASSIS_ID_NM: "sw01" + } + self.hook.before_update(self.data, self.node_info) + self.assertEqual(self.expected, self.data['all_interfaces']) + + def test_duplicate_tlvs(self): + self.data['inventory']['interfaces'] = [{ + 'name': 'em1', + 'lldp': [ + [1, "04112233aabbcc"], # ChassisId + [1, "04332211ddeeff"], # ChassisId + [1, "04556677aabbcc"], # ChassisId + [2, "07373334"], # PortId + [2, "07373435"], # PortId + [2, "07373536"] # PortId + ]}] + + # Only the first unique TLV is processed + self.expected['em1']['lldp_processed'] = { + nv.LLDP_CHASSIS_ID_NM: "11:22:33:aa:bb:cc", + nv.LLDP_PORT_ID_NM: "734" + } + + self.hook.before_update(self.data, self.node_info) + self.assertEqual(self.expected, self.data['all_interfaces']) + + def test_unhandled_tlvs(self): + self.data['inventory']['interfaces'] = [{ + 'name': 'em1', + 'lldp': [ + [10, "04112233aabbcc"], + [12, "07373334"], + [128, "00120f080300010000"]]}] + + # nothing should be written to lldp_processed + self.hook.before_update(self.data, self.node_info) + self.assertEqual(self.expected, self.data['all_interfaces']) + + def test_unhandled_oui(self): + self.data['inventory']['interfaces'] = [{ + 'name': 'em1', + 'lldp': [ + [127, "00906901425030323134323530393236"], + [127, "23ac0074657374"], + [127, "00120e010300010000"]]}] + + # nothing should be written to lldp_processed + self.hook.before_update(self.data, self.node_info) + self.assertEqual(self.expected, self.data['all_interfaces']) + + @mock.patch('ironic_inspector.common.lldp_parsers.LOG') + def test_null_strings(self, mock_log): + self.data['inventory']['interfaces'] = [{ + 'name': 'em1', + 'lldp': [ + [1, "04"], + [4, ""], # PortDesc + [5, ""], # SysName + [6, ""], # SysDesc + [127, "0080c203006507"] # dot1 VlanName + ]}] + + self.expected['em1']['lldp_processed'] = { + nv.LLDP_PORT_DESC_NM: '', + nv.LLDP_SYS_DESC_NM: '', + nv.LLDP_SYS_NAME_NM: '' + } + + self.hook.before_update(self.data, self.node_info) + self.assertEqual(self.expected, self.data['all_interfaces']) + self.assertEqual(2, mock_log.warning.call_count) + + @mock.patch('ironic_inspector.common.lldp_parsers.LOG') + def test_truncated_int(self, mock_log): + self.data['inventory']['interfaces'] = [{ + 'name': 'em1', + 'lldp': [ + [127, "00120f04"], # dot3 MTU + [127, "0080c201"], # dot1 PortVlan + [127, "0080c206"], # dot1 MgmtVID + ]}] + + # nothing should be written to lldp_processed + self.hook.before_update(self.data, self.node_info) + self.assertEqual(self.expected, self.data['all_interfaces']) + self.assertEqual(3, mock_log.warning.call_count) + + @mock.patch('ironic_inspector.common.lldp_parsers.LOG') + def test_invalid_ip(self, mock_log): + self.data['inventory']['interfaces'] = [{ + 'name': 'em1', + 'lldp': [ + [8, "0501"], # truncated + [8, "0507c000020f020000000000"]] # invalid id + }] + self.hook.before_update(self.data, self.node_info) + self.assertEqual(self.expected, self.data['all_interfaces']) + self.assertEqual(2, mock_log.warning.call_count) + + @mock.patch('ironic_inspector.common.lldp_parsers.LOG') + def test_truncated_mac(self, mock_log): + self.data['inventory']['interfaces'] = [{ + 'name': 'em1', + 'lldp': [ + [8, "0506"]] + }] + + self.hook.before_update(self.data, self.node_info) + self.assertEqual(self.expected, self.data['all_interfaces']) + self.assertEqual(1, mock_log.warning.call_count) + + @mock.patch('ironic_inspector.common.lldp_parsers.LOG') + def test_bad_value_macphy(self, mock_log): + self.data['inventory']['interfaces'] = [{ + 'name': 'em1', + 'lldp': [ + [127, "00120f01036c11FFFF"], # invalid mau type + [127, "00120f01036c11"], # truncated + [127, "00120f01036c"] # truncated + ]}] + + self.hook.before_update(self.data, self.node_info) + self.assertEqual(self.expected, self.data['all_interfaces']) + self.assertEqual(3, mock_log.warning.call_count) + + @mock.patch('ironic_inspector.common.lldp_parsers.LOG') + def test_bad_value_linkagg(self, mock_log): + self.data['inventory']['interfaces'] = [{ + 'name': 'em1', + 'lldp': [ + [127, "00120f0303"], # dot3 LinkAggregation + [127, "00120f03"] # truncated + ]}] + + self.hook.before_update(self.data, self.node_info) + self.assertEqual(self.expected, self.data['all_interfaces']) + self.assertEqual(2, mock_log.warning.call_count) diff --git a/releasenotes/notes/add-lldp-basic-plugin-98aebcf43e60931b.yaml b/releasenotes/notes/add-lldp-basic-plugin-98aebcf43e60931b.yaml new file mode 100644 index 000000000..cb88d45ef --- /dev/null +++ b/releasenotes/notes/add-lldp-basic-plugin-98aebcf43e60931b.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added a plugin to parse raw LLDP Basic Management, 802.1, and + 802.3 TLVs and store the data back in Swift. diff --git a/requirements.txt b/requirements.txt index 921a6daff..938790ff5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ automaton>=0.5.0 # Apache-2.0 alembic>=0.8.10 # MIT Babel>=2.3.4 # BSD +construct>=2.8.10 # MIT eventlet!=0.18.3,>=0.18.2 # MIT Flask!=0.11,<1.0,>=0.10 # BSD futurist!=0.15.0,>=0.11.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index ebdd6af35..91437ef90 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,6 +35,7 @@ ironic_inspector.hooks.processing = raid_device = ironic_inspector.plugins.raid_device:RaidDeviceDetection capabilities = ironic_inspector.plugins.capabilities:CapabilitiesHook 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 ironic_inspector.hooks.node_not_found = example = ironic_inspector.plugins.example:example_not_found_hook