Merge "Add inspection hooks"
This commit is contained in:
commit
341f80e24d
123
ironic/drivers/modules/inspector/hooks/local_link_connection.py
Normal file
123
ironic/drivers/modules/inspector/hooks/local_link_connection.py
Normal 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)
|
87
ironic/drivers/modules/inspector/hooks/parse_lldp.py
Normal file
87
ironic/drivers/modules/inspector/hooks/parse_lldp.py
Normal 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}
|
364
ironic/drivers/modules/inspector/lldp_parsers.py
Normal file
364
ironic/drivers/modules/inspector/lldp_parsers.py
Normal 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)
|
365
ironic/drivers/modules/inspector/lldp_tlvs.py
Normal file
365
ironic/drivers/modules/inspector/lldp_tlvs.py
Normal 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)
|
@ -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):
|
||||
|
@ -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)
|
@ -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)
|
@ -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
|
||||
|
@ -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 =
|
||||
|
Loading…
Reference in New Issue
Block a user