Merge "Add inspection hooks"

This commit is contained in:
Zuul 2023-10-20 17:51:08 +00:00 committed by Gerrit Code Review
commit 341f80e24d
9 changed files with 1555 additions and 0 deletions

View File

@ -0,0 +1,123 @@
# 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 binascii
from construct import core
import netaddr
from oslo_log import log as logging
from ironic.common import exception
from ironic.drivers.modules.inspector.hooks import base
from ironic.drivers.modules.inspector import lldp_tlvs as tlv
import ironic.objects.port as ironic_port
LOG = logging.getLogger(__name__)
PORT_ID_ITEM_NAME = "port_id"
SWITCH_ID_ITEM_NAME = "switch_id"
class LocalLinkConnectionHook(base.InspectionHook):
"""Hook to process mandatory LLDP packet fields"""
dependencies = ['validate-interfaces']
def _get_local_link_patch(self, lldp_data, port, node_uuid):
local_link_connection = {}
for tlv_type, tlv_value in lldp_data:
try:
data = bytearray(binascii.unhexlify(tlv_value))
except binascii.Error:
LOG.warning('TLV value for TLV type %d is not in correct '
'format. Ensure that the TLV value is in '
'hexidecimal format when sent to ironic. Node: %s',
tlv_type, node_uuid)
return
item = value = None
if tlv_type == tlv.LLDP_TLV_PORT_ID:
try:
port_id = tlv.PortId.parse(data)
except (core.MappingError, netaddr.AddrFormatError) as e:
LOG.warning('TLV parse error for Port ID for node %s: %s',
node_uuid, e)
return
item = PORT_ID_ITEM_NAME
value = port_id.value.value if port_id.value else None
elif tlv_type == tlv.LLDP_TLV_CHASSIS_ID:
try:
chassis_id = tlv.ChassisId.parse(data)
except (core.MappingError, netaddr.AddrFormatError) as e:
LOG.warning('TLV parse error for Chassis ID for node %s: '
'%s', node_uuid, e)
return
# Only accept mac address for chassis ID
if 'mac_address' in chassis_id.subtype:
item = SWITCH_ID_ITEM_NAME
value = chassis_id.value.value
if item is None or value is None:
continue
if item in port.local_link_connection:
continue
local_link_connection[item] = value
try:
LOG.debug('Updating port %s for node %s', port.address, node_uuid)
for item in local_link_connection:
port.set_local_link_connection(item,
local_link_connection[item])
port.save()
except exception.IronicException as e:
LOG.warning('Failed to update port %(uuid)s for node %(node)s. '
'Error: %(error)s', {'uuid': port.id,
'node': node_uuid,
'error': e})
def __call__(self, task, inventory, plugin_data):
"""Process LLDP data and patch Ironic port local link connection.
Process the non-vendor-specific LLDP packet fields for each NIC found
for a baremetal node, port ID and chassis ID. These fields, if found
and if valid, will be saved into the local link connection information
(port id and switch id) fields on the Ironic port that represents that
NIC.
"""
lldp_raw = plugin_data.get('lldp_raw') or {}
for iface in inventory['interfaces']:
# The all_interfaces field in plugin_data is provided by the
# validate-interfaces hook, so it is a dependency for this hook (?)
if iface['name'] not in plugin_data.get('all_interfaces'):
continue
mac_address = iface['mac_address']
port = ironic_port.Port.get_by_address(task.context, mac_address)
if not port:
LOG.debug('Skipping LLDP processing for interface %s of node '
'%s: matching port not found in Ironic.',
mac_address, task.node.uuid)
continue
lldp_data = lldp_raw.get(iface['name']) or iface.get('lldp')
if lldp_data is None:
LOG.warning('No LLDP data found for interface %s of node %s',
mac_address, task.node.uuid)
continue
# Parse raw lldp data
self._get_local_link_patch(lldp_data, port, task.node.uuid)

View File

@ -0,0 +1,87 @@
# 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 oslo_log import log as logging
from ironic.drivers.modules.inspector.hooks import base
from ironic.drivers.modules.inspector import lldp_parsers
LOG = logging.getLogger(__name__)
class ParseLLDPHook(base.InspectionHook):
"""Process LLDP packet fields and store them in plugin_data['parsed_lldp']
Convert binary LLDP information into a readable form. Loop through raw
LLDP TLVs and parse those from the basic management, 802.1, and 802.3 TLV
sets. Store parsed data in the plugin_data as a new parsed_lldp dictionary
with interface names as keys.
"""
def _parse_lldp_tlvs(self, tlvs, node_uuid):
"""Parse LLDP TLVs into a dictionary of name/value pairs
:param tlvs: List of raw TLVs
:param node_uuid: UUID of the node being inspected
:returns: 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_uuid)
for tlv_type, tlv_value in tlvs:
try:
data = bytearray(binascii.a2b_hex(tlv_value))
except TypeError as e:
LOG.warning(
'TLV value for TLV type %(tlv_type)d is not in correct '
'format, value must be in hexadecimal: %(msg)s. Node: '
'%(node)s', {'tlv_type': tlv_type, 'msg': e,
'node': node_uuid})
continue
if parser.parse_tlv(tlv_type, data):
LOG.debug("Handled TLV type %d. Node: %s", tlv_type, node_uuid)
else:
LOG.debug("LLDP TLV type %d not handled. Node: %s", tlv_type,
node_uuid)
return parser.nv_dict
def __call__(self, task, inventory, plugin_data):
"""Process LLDP data and update plugin_data with processed data"""
lldp_raw = plugin_data.get('lldp_raw') or {}
for interface in inventory['interfaces']:
if_name = interface['name']
tlvs = lldp_raw.get(if_name) or interface.get('lldp')
if tlvs is None:
LOG.warning("No LLDP Data found for interface %s of node %s",
if_name, task.node.uuid)
continue
LOG.debug("Processing LLDP Data for interface %s of node %s",
if_name, task.node.uuid)
# Store LLDP data per interface in plugin_data[parsed_lldp]
nv = self._parse_lldp_tlvs(tlvs, task.node.uuid)
if nv:
if plugin_data.get('parsed_lldp'):
plugin_data['parsed_lldp'].update({if_name: nv})
else:
plugin_data['parsed_lldp'] = {if_name: nv}

View File

@ -0,0 +1,364 @@
# 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 oslo_log import log as logging
from ironic.common.i18n import _
from ironic.drivers.modules.inspector import lldp_tlvs as tlv
LOG = logging.getLogger(__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
Each class that inherits from this base class must provide a parser map.
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 following fields:
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 indicating if length check should be done on construct
It is 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).
"""
def __init__(self, node_uuid, nv=None):
"""Create LLDPParser
:param node_uuid - UUID of node being inspected
:param nv - dictionary of name/value pairs to use
"""
self.nv_dict = nv or {}
self.node_uuid = node_uuid
self.parser_map = {}
def set_value(self, name, value):
"""Set name value pair in dictionary
The value for a name should not be changed if it exists.
"""
self.nv_dict.setdefault(name, value)
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 to the nv dictionary"""
self.set_value(name, struct.value)
def add_nested_value(self, struct, name, data):
"""Add a single nested name/value pair to the dictionary"""
self.set_value(name, struct.value.value)
def parse_tlv(self, tlv_type, data):
"""Parse TLVs from mapping table
This functions takes the TLV type and the raw data for this TLV and
gets a tuple from the parser_map. The construct field in the tuple
contains the construct lib definition of the TLV which can be parsed
to access individual fields. Once the TLV is parsed, the handler
function for each TLV will store the individual fields as name/value
pairs in nv_dict.
If the handler function does not exist, then no name/value pairs will
be added to nv_dict, but since the TLV was handled, True will be
returned.
:param: tlv_type - type identifier for TLV
:param: data - raw TLV value
:returns: True if TLV in parser_map and data is valid, otherwise False.
"""
s = self.parser_map.get(tlv_type)
if not s:
return False
func = s[0] # handler
if not func:
return True # TLV is handled
try:
tlv_parser = s[1]
name = s[2]
check_len = s[3]
except KeyError as e:
LOG.warning("Key error in TLV table: %s. Node: %s", e,
self.node_uuid)
return False
# Some constructs require a length validation to ensure that the
# proper number of bytes have been provided, for example when a
# BitStruct is used.
if check_len and (tlv_parser.sizeof() != len(data)):
LOG.warning("Invalid data for %(name)s expected len %(expect)d, "
"got %(actual)d. Node: %(node)s",
{'name': name, 'expect': tlv_parser.sizeof(),
'actual': len(data), 'node': self.node_uuid})
return False
# Use the construct parser to parse the TLV so that its individual
# fields can be accessed
try:
struct = tlv_parser.parse(data)
except (core.ConstructError, netaddr.AddrFormatError) as e:
LOG.warning("TLV parse error: %s. Node: %s", e, self.node_uuid)
return False
# Call functions with parsed structure
try:
func(struct, name, data)
except ValueError as e:
LOG.warning("TLV value error: %s. Node: %s", e, self.node_uuid)
return False
return True
def add_dot1_link_aggregation(self, struct, name, data):
"""Add name/value pairs for TLV Dot1_LinkAggregationId
This is in the base class since it can be used by both dot1 and dot3.
"""
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_nested_value, tlv.ChassisId, LLDP_CHASSIS_ID_NM,
False),
tlv.LLDP_TLV_PORT_ID:
(self.add_nested_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 can be multiple Mgmt Address TLVs, store in list.
"""
if struct.address:
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_uuid, 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_uuid, 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.warning("Organizationally Unique ID %s not recognized for "
"node %s", oui, self.node_uuid)
class LLDPdot1Parser(LLDPParser):
"""Class to handle parsing of 802.1Q TLVs"""
def __init__(self, node_uuid, nv=None):
super(LLDPdot1Parser, self).__init__(node_uuid, 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
to list.
"""
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_uuid, nv=None):
super(LLDPdot3Parser, self).__init__(node_uuid, 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)

View File

@ -0,0 +1,365 @@
# 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 """
# See http://construct.readthedocs.io/en/latest/index.html
import functools
import construct
from construct import core
import netaddr
from oslo_log import log as logging
LOG = logging.getLogger(__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.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.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.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.PaddedString(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)

View File

@ -491,6 +491,18 @@ class Port(base.IronicObject, object_base.VersionedObjectDictCompat):
"""
return cls.supports_version((1, 9))
def set_local_link_connection(self, key, value):
"""Set a `local_link_connection` value.
Setting a `local_link_connection` dict value via this method ensures
that this field will be flagged for saving.
:param key: Key of item to set
:param value: Value of item to set
"""
self.local_link_connection[key] = value
self._changed_fields.add('local_link_connection')
@base.IronicObjectRegistry.register
class PortCRUDNotification(notification.NotificationBase):

View File

@ -0,0 +1,178 @@
# 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 copy
from unittest import mock
from oslo_utils import uuidutils
from ironic.conductor import task_manager
from ironic.conf import CONF
from ironic.drivers.modules.inspector.hooks import local_link_connection as \
hook
from ironic.objects import port
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.objects import utils as obj_utils
_INVENTORY = {
'interfaces': [{
'name': 'em1',
'mac_address': '11:11:11:11:11:11',
'ipv4_address': '1.1.1.1',
'lldp': [(0, ''),
(1, '04885a92ec5459'),
(2, '0545746865726e6574312f3138'),
(3, '0078')]
}]
}
class LocalLinkConnectionTestCase(db_base.DbTestCase):
def setUp(self):
super().setUp()
CONF.set_override('enabled_inspect_interfaces',
['agent', 'no-inspect'])
self.node = obj_utils.create_test_node(self.context,
inspect_interface='agent')
self.inventory = copy.deepcopy(_INVENTORY)
self.plugin_data = {'all_interfaces': {'em1': {}}}
self.port = obj_utils.create_test_port(
self.context, uuid=uuidutils.generate_uuid(), node_id=self.node.id,
address='11:11:11:11:11:11', local_link_connection={})
@mock.patch.object(port.Port, 'save', autospec=True)
@mock.patch.object(port.Port, 'get_by_address', autospec=True)
def test_valid_data(self, mock_get_port, mock_port_save):
mock_get_port.return_value = self.port
with task_manager.acquire(self.context, self.node.id) as task:
hook.LocalLinkConnectionHook().__call__(task, self.inventory,
self.plugin_data)
self.assertTrue(mock_port_save.called)
self.assertEqual({'switch_id': '88:5a:92:ec:54:59',
'port_id': 'Ethernet1/18'},
self.port.local_link_connection)
@mock.patch.object(port.Port, 'save', autospec=True)
@mock.patch.object(port.Port, 'get_by_address', autospec=True)
def test_lldp_none(self, mock_get_port, mock_port_save):
self.inventory['interfaces'][0]['lldp'] = None
mock_get_port.return_value = self.port
with task_manager.acquire(self.context, self.node.id) as task:
hook.LocalLinkConnectionHook().__call__(task, self.inventory,
self.plugin_data)
self.assertFalse(mock_port_save.called)
self.assertEqual(self.port.local_link_connection, {})
@mock.patch.object(port.Port, 'save', autospec=True)
def test_interface_not_in_all_interfaces(self, mock_port_save):
self.plugin_data['all_interfaces'] = {}
with task_manager.acquire(self.context, self.node.id) as task:
hook.LocalLinkConnectionHook().__call__(task, self.inventory,
self.plugin_data)
self.assertFalse(mock_port_save.called)
self.assertEqual(self.port.local_link_connection, {})
@mock.patch.object(hook.LOG, 'debug', autospec=True)
@mock.patch.object(port.Port, 'get_by_address', autospec=True)
@mock.patch.object(port.Port, 'save', autospec=True)
def test_no_port_in_ironic(self, mock_port_save, mock_get_port, mock_log):
mock_get_port.return_value = None
with task_manager.acquire(self.context, self.node.id) as task:
hook.LocalLinkConnectionHook().__call__(task, self.inventory,
self.plugin_data)
self.assertFalse(mock_port_save.called)
self.assertEqual(self.port.local_link_connection, {})
mock_log.assert_called_once_with(
'Skipping LLDP processing for interface %s of node %s: '
'matching port not found in Ironic.',
self.inventory['interfaces'][0]['mac_address'],
task.node.uuid)
@mock.patch.object(port.Port, 'save', autospec=True)
@mock.patch.object(port.Port, 'get_by_address', autospec=True)
def test_port_local_link_connection_already_exists(self,
mock_get_port,
mock_port_save):
self.port['local_link_connection'] = {'switch_id': '11:11:11:11:11:11',
'port_id': 'Ether'}
mock_get_port.return_value = self.port
with task_manager.acquire(self.context, self.node.id) as task:
hook.LocalLinkConnectionHook().__call__(task, self.inventory,
self.plugin_data)
self.assertTrue(mock_port_save.called)
self.assertEqual(self.port.local_link_connection,
{'switch_id': '11:11:11:11:11:11',
'port_id': 'Ether'})
@mock.patch.object(port.Port, 'save', autospec=True)
@mock.patch.object(hook.LOG, 'warning', autospec=True)
@mock.patch.object(port.Port, 'get_by_address', autospec=True)
def test_invalid_tlv_value_hex_format(self, mock_get_port, mock_log,
mock_port_save):
self.inventory['interfaces'][0]['lldp'] = [(2, 'weee')]
mock_get_port.return_value = self.port
with task_manager.acquire(self.context, self.node.id) as task:
hook.LocalLinkConnectionHook().__call__(task, self.inventory,
self.plugin_data)
mock_log.assert_called_once_with(
'TLV value for TLV type %d is not in correct format. Ensure '
'that the TLV value is in hexidecimal format when sent to '
'ironic. Node: %s', 2, task.node.uuid)
self.assertFalse(mock_port_save.called)
self.assertEqual(self.port.local_link_connection, {})
@mock.patch.object(port.Port, 'save', autospec=True)
@mock.patch.object(port.Port, 'get_by_address', autospec=True)
def test_invalid_port_id_subtype(self, mock_get_port, mock_port_save):
# First byte of TLV value is processed to calculate the subtype for
# the port ID, Subtype 6 ('06...') isn't a subtype supported by this
# hook, so we expect it to skip this TLV.
self.inventory['interfaces'][0]['lldp'][2] = (
2, '0645746865726e6574312f3138')
mock_get_port.return_value = self.port
with task_manager.acquire(self.context, self.node.id) as task:
hook.LocalLinkConnectionHook().__call__(task, self.inventory,
self.plugin_data)
self.assertTrue(mock_port_save.called)
self.assertEqual(self.port.local_link_connection,
{'switch_id': '88:5a:92:ec:54:59'})
@mock.patch.object(port.Port, 'save', autospec=True)
@mock.patch.object(port.Port, 'get_by_address', autospec=True)
def test_port_id_subtype_mac(self, mock_get_port, mock_port_save):
self.inventory['interfaces'][0]['lldp'][2] = (
2, '03885a92ec5458')
mock_get_port.return_value = self.port
with task_manager.acquire(self.context, self.node.id) as task:
hook.LocalLinkConnectionHook().__call__(task, self.inventory,
self.plugin_data)
self.assertTrue(mock_port_save.called)
self.assertEqual(self.port.local_link_connection,
{'port_id': '88:5a:92:ec:54:58',
'switch_id': '88:5a:92:ec:54:59'})
@mock.patch.object(port.Port, 'save', autospec=True)
@mock.patch.object(port.Port, 'get_by_address', autospec=True)
def test_invalid_chassis_id_subtype(self, mock_get_port, mock_port_save):
# First byte of TLV value is processed to calculate the subtype for
# the chassis ID, Subtype 5 ('05...') isn't a subtype supported by
# this hook, so we expect it to skip this TLV.
self.inventory['interfaces'][0]['lldp'][1] = (1, '05885a92ec5459')
mock_get_port.return_value = self.port
with task_manager.acquire(self.context, self.node.id) as task:
hook.LocalLinkConnectionHook().__call__(task, self.inventory,
self.plugin_data)
self.assertTrue(mock_port_save.called)
self.assertEqual({'port_id': 'Ethernet1/18'},
self.port.local_link_connection)

View File

@ -0,0 +1,422 @@
# 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.
from unittest import mock
from ironic.conductor import task_manager
from ironic.conf import CONF
from ironic.drivers.modules.inspector.hooks import parse_lldp as hook
from ironic.drivers.modules.inspector import lldp_parsers as nv
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.objects import utils as obj_utils
class ParseLLDPTestCase(db_base.DbTestCase):
def setUp(self):
super().setUp()
CONF.set_override('enabled_inspect_interfaces',
['agent', 'no-inspect'])
self.node = obj_utils.create_test_node(self.context,
inspect_interface='agent')
self.inventory = {
'interfaces': [{
'name': 'em1',
}],
'cpu': 1,
'disks': 1,
'memory': 1
}
self.ip = '1.2.1.2'
self.mac = '11:22:33:44:55:66'
self.plugin_data = {'all_interfaces':
{'em1': {'mac': self.mac,
'ip': self.ip}}}
self.expected = {'em1': {'ip': self.ip, 'mac': self.mac}}
def test_all_valid_data(self):
self.plugin_data['lldp_raw'] = {
'em1': [
[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, ""],
]
}
self.inventory['interfaces'] = [{
'name': 'em1',
'lldp': [[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_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'
}
with task_manager.acquire(self.context, self.node.id) as task:
hook.ParseLLDPHook().__call__(task, self.inventory,
self.plugin_data)
actual = self.plugin_data.get('parsed_lldp').get('em1')
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_old_format(self):
self.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_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'
}
with task_manager.acquire(self.context, self.node.id) as task:
hook.ParseLLDPHook().__call__(task, self.inventory,
self.plugin_data)
actual = self.plugin_data['parsed_lldp']['em1']
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.inventory = {
# An artificial mix of old and new LLDP fields.
'interfaces': [
{
'name': 'em1'
},
{
'name': 'em2',
'lldp': [
[1, "04112233aabbdd"],
[2, "07373838"],
[3, "003c"]
]
},
{
'name': 'em3',
'lldp': [[3, "003c"]]
}
],
'cpu': 1,
'disks': 1,
'memory': 1
}
self.plugin_data = {
'all_interfaces': {
'em1': {'mac': self.mac, 'ip': self.ip},
'em2': {'mac': self.mac, 'ip': self.ip},
'em3': {'mac': self.mac, 'ip': self.ip}
},
'lldp_raw': {
'em1': [
[1, "04112233aabbcc"],
[2, "07373334"],
[3, "003c"]
],
'em3': [
[1, "04112233aabbee"],
[2, "07373939"],
[3, "003c"]
],
}
}
expected = {"em1": {nv.LLDP_CHASSIS_ID_NM: "11:22:33:aa:bb:cc",
nv.LLDP_PORT_ID_NM: "734"},
"em2": {nv.LLDP_CHASSIS_ID_NM: "11:22:33:aa:bb:dd",
nv.LLDP_PORT_ID_NM: "788"},
"em3": {nv.LLDP_CHASSIS_ID_NM: "11:22:33:aa:bb:ee",
nv.LLDP_PORT_ID_NM: "799"}}
with task_manager.acquire(self.context, self.node.id) as task:
hook.ParseLLDPHook().__call__(task, self.inventory,
self.plugin_data)
self.assertEqual(expected, self.plugin_data['parsed_lldp'])
def test_chassis_ids(self):
# Test IPv4 address
self.inventory['interfaces'] = [
{
'name': 'em1',
'lldp': [[1, '0501c000020f']]
},
{
'name': 'em2',
'lldp': [[1, '0773773031']]
}
]
self.expected = {
'em1': {nv.LLDP_CHASSIS_ID_NM: '192.0.2.15'},
'em2': {nv.LLDP_CHASSIS_ID_NM: "sw01"}
}
with task_manager.acquire(self.context, self.node.id) as task:
hook.ParseLLDPHook().__call__(task, self.inventory,
self.plugin_data)
self.assertEqual(self.expected, self.plugin_data['parsed_lldp'])
def test_duplicate_tlvs(self):
self.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': {
nv.LLDP_CHASSIS_ID_NM: "11:22:33:aa:bb:cc",
nv.LLDP_PORT_ID_NM: "734"
}}
with task_manager.acquire(self.context, self.node.id) as task:
hook.ParseLLDPHook().__call__(task, self.inventory,
self.plugin_data)
self.assertEqual(self.expected, self.plugin_data['parsed_lldp'])
def test_unhandled_tlvs(self):
self.inventory['interfaces'] = [{
'name': 'em1',
'lldp': [
[10, "04112233aabbcc"],
[12, "07373334"],
[128, "00120f080300010000"]]}]
# Nothing should be written to lldp_processed
with task_manager.acquire(self.context, self.node.id) as task:
hook.ParseLLDPHook().__call__(task, self.inventory,
self.plugin_data)
self.assertNotIn('parsed_lldp', self.plugin_data)
def test_unhandled_oui(self):
self.inventory['interfaces'] = [{
'name': 'em1',
'lldp': [
[127, "00906901425030323134323530393236"],
[127, "23ac0074657374"],
[127, "00120e010300010000"]]}]
# Nothing should be written to lldp_processed
with task_manager.acquire(self.context, self.node.id) as task:
hook.ParseLLDPHook().__call__(task, self.inventory,
self.plugin_data)
self.assertNotIn('parsed_lldp', self.plugin_data)
@mock.patch.object(nv.LOG, 'warning', autospec=True)
def test_null_strings(self, mock_log):
self.inventory['interfaces'] = [{
'name': 'em1',
'lldp': [
[1, "04"],
[4, ""], # PortDesc
[5, ""], # SysName
[6, ""], # SysDesc
[127, "0080c203006507"] # dot1 VlanName
]}]
self.expected = {'em1': {
nv.LLDP_PORT_DESC_NM: '',
nv.LLDP_SYS_DESC_NM: '',
nv.LLDP_SYS_NAME_NM: ''
}}
with task_manager.acquire(self.context, self.node.id) as task:
hook.ParseLLDPHook().__call__(task, self.inventory,
self.plugin_data)
self.assertEqual(self.expected, self.plugin_data['parsed_lldp'])
self.assertEqual(2, mock_log.call_count)
@mock.patch.object(nv.LOG, 'warning', autospec=True)
def test_truncated_int(self, mock_log):
self.inventory['interfaces'] = [{
'name': 'em1',
'lldp': [
[127, "00120f04"], # dot3 MTU
[127, "0080c201"], # dot1 PortVlan
[127, "0080c206"], # dot1 MgmtVID
]
}]
# Nothing should be written to lldp_processed
with task_manager.acquire(self.context, self.node.id) as task:
hook.ParseLLDPHook().__call__(task, self.inventory,
self.plugin_data)
self.assertNotIn('parsed_lldp', self.plugin_data)
self.assertEqual(3, mock_log.call_count)
@mock.patch.object(nv.LOG, 'warning', autospec=True)
def test_invalid_ip(self, mock_log):
self.inventory['interfaces'] = [{
'name': 'em1',
'lldp': [
[8, "0501"], # truncated
[8, "0507c000020f020000000000"]
] # invalid id
}]
with task_manager.acquire(self.context, self.node.id) as task:
hook.ParseLLDPHook().__call__(task, self.inventory,
self.plugin_data)
self.assertNotIn('parsed_lldp', self.plugin_data)
self.assertEqual(1, mock_log.call_count)
@mock.patch.object(nv.LOG, 'warning', autospec=True)
def test_truncated_mac(self, mock_log):
self.inventory['interfaces'] = [{
'name': 'em1',
'lldp': [[8, "0506"]]
}]
with task_manager.acquire(self.context, self.node.id) as task:
hook.ParseLLDPHook().__call__(task, self.inventory,
self.plugin_data)
self.assertNotIn('parsed_lldp', self.plugin_data)
self.assertEqual(1, mock_log.call_count)
@mock.patch.object(nv.LOG, 'warning', autospec=True)
def test_bad_value_macphy(self, mock_log):
self.inventory['interfaces'] = [{
'name': 'em1',
'lldp': [
[127, "00120f01036c11FFFF"], # invalid mau type
[127, "00120f01036c11"], # truncated
[127, "00120f01036c"] # truncated
]
}]
with task_manager.acquire(self.context, self.node.id) as task:
hook.ParseLLDPHook().__call__(task, self.inventory,
self.plugin_data)
self.assertNotIn('parsed_lldp', self.plugin_data)
self.assertEqual(3, mock_log.call_count)
@mock.patch.object(nv.LOG, 'warning', autospec=True)
def test_bad_value_linkagg(self, mock_log):
self.inventory['interfaces'] = [{
'name': 'em1',
'lldp': [
[127, "00120f0303"], # dot3 LinkAggregation
[127, "00120f03"] # truncated
]
}]
with task_manager.acquire(self.context, self.node.id) as task:
hook.ParseLLDPHook().__call__(task, self.inventory,
self.plugin_data)
self.assertNotIn('parsed_lldp', self.plugin_data)
self.assertEqual(2, mock_log.call_count)

View File

@ -48,3 +48,5 @@ futurist>=1.2.0 # Apache-2.0
tooz>=2.7.0 # Apache-2.0
openstacksdk>=0.48.0 # Apache-2.0
sushy>=4.3.0
construct>=2.9.39 # MIT
netaddr>=0.9.0 # BSD

View File

@ -211,6 +211,8 @@ ironic.inspection.hooks =
physical-network = ironic.drivers.modules.inspector.hooks.physical_network:PhysicalNetworkHook
raid-device = ironic.drivers.modules.inspector.hooks.raid_device:RaidDeviceHook
root-device = ironic.drivers.modules.inspector.hooks.root_device:RootDeviceHook
local-link-connection = ironic.drivers.modules.inspector.hooks.local_link_connection:LocalLinkConnectionHook
parse-lldp = ironic.drivers.modules.inspector.hooks.parse_lldp:ParseLLDPHook
[egg_info]
tag_build =