Browse Source

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
changes/96/406496/32
Bob Fournier 6 years ago
parent
commit
8834927d4c
  1. 4
      doc/source/usage.rst
  2. 339
      ironic_inspector/common/lldp_parsers.py
  3. 366
      ironic_inspector/common/lldp_tlvs.py
  4. 88
      ironic_inspector/plugins/lldp_basic.py
  5. 6
      ironic_inspector/test/base.py
  6. 37
      ironic_inspector/test/functional.py
  7. 329
      ironic_inspector/test/unit/test_plugins_lldp_basic.py
  8. 4
      releasenotes/notes/add-lldp-basic-plugin-98aebcf43e60931b.yaml
  9. 1
      requirements.txt
  10. 1
      setup.cfg

4
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.

339
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)

366
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)

88
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

6
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]},

37
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():

329
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)

4
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.

1
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

1
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

Loading…
Cancel
Save