ironic-inspector/ironic_inspector/common/lldp_parsers.py

340 lines
14 KiB
Python

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