8834927d4c
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
340 lines
14 KiB
Python
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)
|