Merge "Add plugin to process basic management LLDP TLVs"
This commit is contained in:
commit
b455e238cd
@ -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
Normal file
339
ironic_inspector/common/lldp_parsers.py
Normal file
@ -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
Normal file
366
ironic_inspector/common/lldp_tlvs.py
Normal file
@ -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
Normal file
88
ironic_inspector/plugins/lldp_basic.py
Normal file
@ -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
|
@ -109,7 +109,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]},
|
||||
|
@ -60,6 +60,8 @@ debug = True
|
||||
auth_strategy = noauth
|
||||
[database]
|
||||
connection = sqlite:///%(db_file)s
|
||||
[processing]
|
||||
processing_hooks=$default_processing_hooks,lldp_basic
|
||||
"""
|
||||
|
||||
|
||||
@ -741,6 +743,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
Normal file
329
ironic_inspector/test/unit/test_plugins_lldp_basic.py
Normal file
@ -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)
|
@ -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.
|
@ -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
|
||||
|
@ -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…
x
Reference in New Issue
Block a user