Add GenericLocalLinkConnectionHook processing hook
For Ironic multi-tenant networking support, we need to be able to discover the nodes local connectivity. To do this IPA can try to pull LLDP packets for every NIC. This patch adds a processing hook to handle the data from these packets in ironic-inspector so that we can populate the correct fields fields in Ironic. The generic lldp hook only handles the mandatory fields port id and chassis id, set on port_id and switch_id in local_link_connection. Further LLDP fields should be handled by additional vendor specific LLDP processing hooks, that populate the switch_info field in a non-generic way. Change-Id: I884eaaa9cc54cd08c21147da438b1dabc10d3a40 Related-Bug: #1526403 Depends-On: Ie655fd59b06de7b84fba3b438d5e4c2ecd8075c3 Depends-On: Idae9b1ede1797029da1bd521501b121957ca1f1a
This commit is contained in:
parent
823f6d26a2
commit
4af672b849
@ -214,6 +214,12 @@ Here are some plugins that can be additionally enabled:
|
|||||||
then the new format will be stored in the 'extra' key. The 'data' key is
|
then the new format will be stored in the 'extra' key. The 'data' key is
|
||||||
then deleted from the introspection data, as unless converted it's assumed
|
then deleted from the introspection data, as unless converted it's assumed
|
||||||
unusable by introspection rules.
|
unusable by introspection rules.
|
||||||
|
``local_link_connection``
|
||||||
|
Processes LLDP data returned from inspection specifically looking for the
|
||||||
|
port ID and chassis ID, if found it configures the local link connection
|
||||||
|
information on the nodes Ironic ports with that data. To enable LLDP in the
|
||||||
|
inventory from IPA ``ipa-collect-lldp=1`` should be passed as a kernel
|
||||||
|
parameter to the IPA ramdisk.
|
||||||
|
|
||||||
Refer to :ref:`contributing_link` for information on how to write your
|
Refer to :ref:`contributing_link` for information on how to write your
|
||||||
own plugin.
|
own plugin.
|
||||||
|
@ -216,25 +216,26 @@ class NodeInfo(object):
|
|||||||
self._attributes = None
|
self._attributes = None
|
||||||
self._ironic = None
|
self._ironic = None
|
||||||
|
|
||||||
def node(self):
|
def node(self, ironic=None):
|
||||||
"""Get Ironic node object associated with the cached node record."""
|
"""Get Ironic node object associated with the cached node record."""
|
||||||
if self._node is None:
|
if self._node is None:
|
||||||
self._node = ir_utils.get_node(self.uuid, ironic=self.ironic)
|
ironic = ironic or self.ironic
|
||||||
|
self._node = ir_utils.get_node(self.uuid, ironic=ironic)
|
||||||
return self._node
|
return self._node
|
||||||
|
|
||||||
def create_ports(self, macs):
|
def create_ports(self, macs, ironic=None):
|
||||||
"""Create one or several ports for this node.
|
"""Create one or several ports for this node.
|
||||||
|
|
||||||
A warning is issued if port already exists on a node.
|
A warning is issued if port already exists on a node.
|
||||||
"""
|
"""
|
||||||
for mac in macs:
|
for mac in macs:
|
||||||
if mac not in self.ports():
|
if mac not in self.ports():
|
||||||
self._create_port(mac)
|
self._create_port(mac, ironic)
|
||||||
else:
|
else:
|
||||||
LOG.warning(_LW('Port %s already exists, skipping'),
|
LOG.warning(_LW('Port %s already exists, skipping'),
|
||||||
mac, node_info=self)
|
mac, node_info=self)
|
||||||
|
|
||||||
def ports(self):
|
def ports(self, ironic=None):
|
||||||
"""Get Ironic port objects associated with the cached node record.
|
"""Get Ironic port objects associated with the cached node record.
|
||||||
|
|
||||||
This value is cached as well, use invalidate_cache() to clean.
|
This value is cached as well, use invalidate_cache() to clean.
|
||||||
@ -242,13 +243,15 @@ class NodeInfo(object):
|
|||||||
:return: dict MAC -> port object
|
:return: dict MAC -> port object
|
||||||
"""
|
"""
|
||||||
if self._ports is None:
|
if self._ports is None:
|
||||||
|
ironic = ironic or self.ironic
|
||||||
self._ports = {p.address: p for p in
|
self._ports = {p.address: p for p in
|
||||||
self.ironic.node.list_ports(self.uuid, limit=0)}
|
ironic.node.list_ports(self.uuid, limit=0)}
|
||||||
return self._ports
|
return self._ports
|
||||||
|
|
||||||
def _create_port(self, mac):
|
def _create_port(self, mac, ironic=None):
|
||||||
|
ironic = ironic or self.ironic
|
||||||
try:
|
try:
|
||||||
port = self.ironic.port.create(node_uuid=self.uuid, address=mac)
|
port = ironic.port.create(node_uuid=self.uuid, address=mac)
|
||||||
except exceptions.Conflict:
|
except exceptions.Conflict:
|
||||||
LOG.warning(_LW('Port %s already exists, skipping'),
|
LOG.warning(_LW('Port %s already exists, skipping'),
|
||||||
mac, node_info=self)
|
mac, node_info=self)
|
||||||
@ -258,14 +261,16 @@ class NodeInfo(object):
|
|||||||
else:
|
else:
|
||||||
self._ports[mac] = port
|
self._ports[mac] = port
|
||||||
|
|
||||||
def patch(self, patches):
|
def patch(self, patches, ironic=None):
|
||||||
"""Apply JSON patches to a node.
|
"""Apply JSON patches to a node.
|
||||||
|
|
||||||
Refreshes cached node instance.
|
Refreshes cached node instance.
|
||||||
|
|
||||||
:param patches: JSON patches to apply
|
:param patches: JSON patches to apply
|
||||||
|
:param ironic: Ironic client to use instead of self.ironic
|
||||||
:raises: ironicclient exceptions
|
:raises: ironicclient exceptions
|
||||||
"""
|
"""
|
||||||
|
ironic = ironic or self.ironic
|
||||||
# NOTE(aarefiev): support path w/o ahead forward slash
|
# NOTE(aarefiev): support path w/o ahead forward slash
|
||||||
# as Ironic cli does
|
# as Ironic cli does
|
||||||
for patch in patches:
|
for patch in patches:
|
||||||
@ -273,14 +278,16 @@ class NodeInfo(object):
|
|||||||
patch['path'] = '/' + patch['path']
|
patch['path'] = '/' + patch['path']
|
||||||
|
|
||||||
LOG.debug('Updating node with patches %s', patches, node_info=self)
|
LOG.debug('Updating node with patches %s', patches, node_info=self)
|
||||||
self._node = self.ironic.node.update(self.uuid, patches)
|
self._node = ironic.node.update(self.uuid, patches)
|
||||||
|
|
||||||
def patch_port(self, port, patches):
|
def patch_port(self, port, patches, ironic=None):
|
||||||
"""Apply JSON patches to a port.
|
"""Apply JSON patches to a port.
|
||||||
|
|
||||||
:param port: port object or its MAC
|
:param port: port object or its MAC
|
||||||
:param patches: JSON patches to apply
|
:param patches: JSON patches to apply
|
||||||
|
:param ironic: Ironic client to use instead of self.ironic
|
||||||
"""
|
"""
|
||||||
|
ironic = ironic or self.ironic
|
||||||
ports = self.ports()
|
ports = self.ports()
|
||||||
if isinstance(port, str):
|
if isinstance(port, str):
|
||||||
port = ports[port]
|
port = ports[port]
|
||||||
@ -288,39 +295,45 @@ class NodeInfo(object):
|
|||||||
LOG.debug('Updating port %(mac)s with patches %(patches)s',
|
LOG.debug('Updating port %(mac)s with patches %(patches)s',
|
||||||
{'mac': port.address, 'patches': patches},
|
{'mac': port.address, 'patches': patches},
|
||||||
node_info=self)
|
node_info=self)
|
||||||
new_port = self.ironic.port.update(port.uuid, patches)
|
new_port = ironic.port.update(port.uuid, patches)
|
||||||
ports[port.address] = new_port
|
ports[port.address] = new_port
|
||||||
|
|
||||||
def update_properties(self, **props):
|
def update_properties(self, ironic=None, **props):
|
||||||
"""Update properties on a node.
|
"""Update properties on a node.
|
||||||
|
|
||||||
:param props: properties to update
|
:param props: properties to update
|
||||||
|
:param ironic: Ironic client to use instead of self.ironic
|
||||||
"""
|
"""
|
||||||
|
ironic = ironic or self.ironic
|
||||||
patches = [{'op': 'add', 'path': '/properties/%s' % k, 'value': v}
|
patches = [{'op': 'add', 'path': '/properties/%s' % k, 'value': v}
|
||||||
for k, v in props.items()]
|
for k, v in props.items()]
|
||||||
self.patch(patches)
|
self.patch(patches, ironic)
|
||||||
|
|
||||||
def update_capabilities(self, **caps):
|
def update_capabilities(self, ironic=None, **caps):
|
||||||
"""Update capabilities on a node.
|
"""Update capabilities on a node.
|
||||||
|
|
||||||
:param props: capabilities to update
|
:param caps: capabilities to update
|
||||||
|
:param ironic: Ironic client to use instead of self.ironic
|
||||||
"""
|
"""
|
||||||
existing = ir_utils.capabilities_to_dict(
|
existing = ir_utils.capabilities_to_dict(
|
||||||
self.node().properties.get('capabilities'))
|
self.node().properties.get('capabilities'))
|
||||||
existing.update(caps)
|
existing.update(caps)
|
||||||
self.update_properties(
|
self.update_properties(
|
||||||
|
ironic=ironic,
|
||||||
capabilities=ir_utils.dict_to_capabilities(existing))
|
capabilities=ir_utils.dict_to_capabilities(existing))
|
||||||
|
|
||||||
def delete_port(self, port):
|
def delete_port(self, port, ironic=None):
|
||||||
"""Delete port.
|
"""Delete port.
|
||||||
|
|
||||||
:param port: port object or its MAC
|
:param port: port object or its MAC
|
||||||
|
:param ironic: Ironic client to use instead of self.ironic
|
||||||
"""
|
"""
|
||||||
|
ironic = ironic or self.ironic
|
||||||
ports = self.ports()
|
ports = self.ports()
|
||||||
if isinstance(port, str):
|
if isinstance(port, str):
|
||||||
port = ports[port]
|
port = ports[port]
|
||||||
|
|
||||||
self.ironic.port.delete(port.uuid)
|
ironic.port.delete(port.uuid)
|
||||||
del ports[port.address]
|
del ports[port.address]
|
||||||
|
|
||||||
def get_by_path(self, path):
|
def get_by_path(self, path):
|
||||||
@ -350,6 +363,7 @@ class NodeInfo(object):
|
|||||||
:raises: KeyError if value is not found and default is not set
|
:raises: KeyError if value is not found and default is not set
|
||||||
:raises: everything that patch() may raise
|
:raises: everything that patch() may raise
|
||||||
"""
|
"""
|
||||||
|
ironic = kwargs.pop("ironic", None) or self.ironic
|
||||||
try:
|
try:
|
||||||
value = self.get_by_path(path)
|
value = self.get_by_path(path)
|
||||||
op = 'replace'
|
op = 'replace'
|
||||||
@ -363,7 +377,7 @@ class NodeInfo(object):
|
|||||||
ref_value = copy.deepcopy(value)
|
ref_value = copy.deepcopy(value)
|
||||||
value = func(value)
|
value = func(value)
|
||||||
if value != ref_value:
|
if value != ref_value:
|
||||||
self.patch([{'op': op, 'path': path, 'value': value}])
|
self.patch([{'op': op, 'path': path, 'value': value}], ironic)
|
||||||
|
|
||||||
|
|
||||||
def add_node(uuid, **attributes):
|
def add_node(uuid, **attributes):
|
||||||
|
122
ironic_inspector/plugins/local_link_connection.py
Normal file
122
ironic_inspector/plugins/local_link_connection.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Generic LLDP Processing Hook"""
|
||||||
|
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
from ironicclient import exc as client_exc
|
||||||
|
import netaddr
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from ironic_inspector.common.i18n import _LW, _LE
|
||||||
|
from ironic_inspector.common import ironic
|
||||||
|
from ironic_inspector.plugins import base
|
||||||
|
from ironic_inspector import utils
|
||||||
|
|
||||||
|
LOG = utils.getProcessingLogger(__name__)
|
||||||
|
|
||||||
|
# NOTE(sambetts) Constants defined according to IEEE standard for LLDP
|
||||||
|
# http://standards.ieee.org/getieee802/download/802.1AB-2009.pdf
|
||||||
|
LLDP_TLV_TYPE_CHASSIS_ID = 1
|
||||||
|
LLDP_TLV_TYPE_PORT_ID = 2
|
||||||
|
PORT_ID_SUBTYPE_MAC = 3
|
||||||
|
PORT_ID_SUBTYPE_IFNAME = 5
|
||||||
|
PORT_ID_SUBTYPE_LOCAL = 7
|
||||||
|
STRING_PORT_SUBTYPES = [PORT_ID_SUBTYPE_IFNAME, PORT_ID_SUBTYPE_LOCAL]
|
||||||
|
CHASSIS_ID_SUBTYPE_MAC = 4
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
REQUIRED_IRONIC_VERSION = '1.19'
|
||||||
|
|
||||||
|
|
||||||
|
class GenericLocalLinkConnectionHook(base.ProcessingHook):
|
||||||
|
"""Process mandatory LLDP packet fields
|
||||||
|
|
||||||
|
Non-vendor specific LLDP packet fields processed 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 info port id and switch id
|
||||||
|
fields on the Ironic port that represents that NIC.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _get_local_link_patch(self, tlv_type, tlv_value, port):
|
||||||
|
try:
|
||||||
|
data = bytearray(binascii.unhexlify(tlv_value))
|
||||||
|
except TypeError:
|
||||||
|
LOG.warning(_LW("TLV value for TLV type %d not in correct"
|
||||||
|
"format, ensure TLV value is in "
|
||||||
|
"hexidecimal format when sent to "
|
||||||
|
"inspector"), tlv_type)
|
||||||
|
return
|
||||||
|
|
||||||
|
item = value = None
|
||||||
|
if tlv_type == LLDP_TLV_TYPE_PORT_ID:
|
||||||
|
# Check to ensure the port id is an allowed type
|
||||||
|
item = "port_id"
|
||||||
|
if data[0] in STRING_PORT_SUBTYPES:
|
||||||
|
value = data[1:].decode()
|
||||||
|
if data[0] == PORT_ID_SUBTYPE_MAC:
|
||||||
|
value = str(netaddr.EUI(
|
||||||
|
binascii.hexlify(data[1:]).decode()))
|
||||||
|
elif tlv_type == LLDP_TLV_TYPE_CHASSIS_ID:
|
||||||
|
# Check to ensure the chassis id is the allowed type
|
||||||
|
if data[0] == CHASSIS_ID_SUBTYPE_MAC:
|
||||||
|
item = "switch_id"
|
||||||
|
value = str(netaddr.EUI(
|
||||||
|
binascii.hexlify(data[1:]).decode()))
|
||||||
|
|
||||||
|
if item and value:
|
||||||
|
if (not CONF.processing.overwrite_existing and
|
||||||
|
item in port.local_link_connection):
|
||||||
|
return
|
||||||
|
return {'op': 'add',
|
||||||
|
'path': '/local_link_connection/%s' % item,
|
||||||
|
'value': value}
|
||||||
|
|
||||||
|
def before_update(self, introspection_data, node_info, **kwargs):
|
||||||
|
"""Process LLDP data and patch Ironic port local link connection"""
|
||||||
|
inventory = utils.get_inventory(introspection_data)
|
||||||
|
|
||||||
|
ironic_ports = node_info.ports()
|
||||||
|
|
||||||
|
for iface in inventory['interfaces']:
|
||||||
|
if iface['name'] not in introspection_data['all_interfaces']:
|
||||||
|
continue
|
||||||
|
port = ironic_ports[iface['mac_address']]
|
||||||
|
|
||||||
|
lldp_data = iface.get('lldp')
|
||||||
|
if lldp_data is None:
|
||||||
|
LOG.warning(_LW("No LLDP Data found for interface %s"), iface)
|
||||||
|
continue
|
||||||
|
|
||||||
|
patches = []
|
||||||
|
for tlv_type, tlv_value in lldp_data:
|
||||||
|
patch = self._get_local_link_patch(tlv_type, tlv_value, port)
|
||||||
|
if patch is not None:
|
||||||
|
patches.append(patch)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# NOTE(sambetts) We need a newer version of Ironic API for this
|
||||||
|
# transaction, so create a new ironic client and explicitly
|
||||||
|
# pass it into the function.
|
||||||
|
cli = ironic.get_client(api_version=REQUIRED_IRONIC_VERSION)
|
||||||
|
node_info.patch_port(iface['mac_address'], patches, ironic=cli)
|
||||||
|
except client_exc.NotAcceptable:
|
||||||
|
LOG.error(_LE("Unable to set Ironic port local link "
|
||||||
|
"connection information because Ironic does not "
|
||||||
|
"support the required version"))
|
||||||
|
# NOTE(sambetts) May as well break out out of the loop here
|
||||||
|
# because Ironic version is not going to change for the other
|
||||||
|
# interfaces.
|
||||||
|
break
|
@ -71,9 +71,11 @@ class BaseTest(fixtures.TestWithFixtures):
|
|||||||
def assertCalledWithPatch(self, expected, mock_call):
|
def assertCalledWithPatch(self, expected, mock_call):
|
||||||
def _get_patch_param(call):
|
def _get_patch_param(call):
|
||||||
try:
|
try:
|
||||||
return call[0][1]
|
if isinstance(call[0][1], list):
|
||||||
|
return call[0][1]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return call[0][0]
|
pass
|
||||||
|
return call[0][0]
|
||||||
|
|
||||||
actual = sum(map(_get_patch_param, mock_call.call_args_list), [])
|
actual = sum(map(_get_patch_param, mock_call.call_args_list), [])
|
||||||
self.assertPatchEqual(actual, expected)
|
self.assertPatchEqual(actual, expected)
|
||||||
|
138
ironic_inspector/test/unit/test_plugins_local_link_connection.py
Normal file
138
ironic_inspector/test/unit/test_plugins_local_link_connection.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from ironic_inspector import node_cache
|
||||||
|
from ironic_inspector.plugins import local_link_connection
|
||||||
|
from ironic_inspector.test import base as test_base
|
||||||
|
from ironic_inspector import utils
|
||||||
|
|
||||||
|
|
||||||
|
class TestGenericLocalLinkConnectionHook(test_base.NodeTest):
|
||||||
|
hook = local_link_connection.GenericLocalLinkConnectionHook()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestGenericLocalLinkConnectionHook, self).setUp()
|
||||||
|
self.data = {
|
||||||
|
'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')]
|
||||||
|
}],
|
||||||
|
'cpu': 1,
|
||||||
|
'disks': 1,
|
||||||
|
'memory': 1
|
||||||
|
},
|
||||||
|
'all_interfaces': {
|
||||||
|
'em1': {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
llc = {
|
||||||
|
'port_id': '56'
|
||||||
|
}
|
||||||
|
|
||||||
|
ports = [mock.Mock(spec=['address', 'uuid', 'local_link_connection'],
|
||||||
|
address=a, local_link_connection=llc)
|
||||||
|
for a in ('11:11:11:11:11:11',)]
|
||||||
|
self.node_info = node_cache.NodeInfo(uuid=self.uuid, started_at=0,
|
||||||
|
node=self.node, ports=ports)
|
||||||
|
|
||||||
|
@mock.patch.object(node_cache.NodeInfo, 'patch_port')
|
||||||
|
def test_expected_data(self, mock_patch):
|
||||||
|
patches = [
|
||||||
|
{'path': '/local_link_connection/port_id',
|
||||||
|
'value': 'Ethernet1/18', 'op': 'add'},
|
||||||
|
{'path': '/local_link_connection/switch_id',
|
||||||
|
'value': '88-5A-92-EC-54-59', 'op': 'add'},
|
||||||
|
]
|
||||||
|
self.hook.before_update(self.data, self.node_info)
|
||||||
|
self.assertCalledWithPatch(patches, mock_patch)
|
||||||
|
|
||||||
|
@mock.patch.object(node_cache.NodeInfo, 'patch_port')
|
||||||
|
def test_invalid_chassis_id_subtype(self, mock_patch):
|
||||||
|
# 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
|
||||||
|
# plugin, so we expect it to skip this TLV.
|
||||||
|
self.data['inventory']['interfaces'][0]['lldp'][1] = (
|
||||||
|
1, '05885a92ec5459')
|
||||||
|
patches = [
|
||||||
|
{'path': '/local_link_connection/port_id',
|
||||||
|
'value': 'Ethernet1/18', 'op': 'add'},
|
||||||
|
]
|
||||||
|
self.hook.before_update(self.data, self.node_info)
|
||||||
|
self.assertCalledWithPatch(patches, mock_patch)
|
||||||
|
|
||||||
|
@mock.patch.object(node_cache.NodeInfo, 'patch_port')
|
||||||
|
def test_invalid_port_id_subtype(self, mock_patch):
|
||||||
|
# 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
|
||||||
|
# plugin, so we expect it to skip this TLV.
|
||||||
|
self.data['inventory']['interfaces'][0]['lldp'][2] = (
|
||||||
|
2, '0645746865726e6574312f3138')
|
||||||
|
patches = [
|
||||||
|
{'path': '/local_link_connection/switch_id',
|
||||||
|
'value': '88-5A-92-EC-54-59', 'op': 'add'}
|
||||||
|
]
|
||||||
|
self.hook.before_update(self.data, self.node_info)
|
||||||
|
self.assertCalledWithPatch(patches, mock_patch)
|
||||||
|
|
||||||
|
@mock.patch.object(node_cache.NodeInfo, 'patch_port')
|
||||||
|
def test_port_id_subtype_mac(self, mock_patch):
|
||||||
|
self.data['inventory']['interfaces'][0]['lldp'][2] = (
|
||||||
|
2, '03885a92ec5458')
|
||||||
|
patches = [
|
||||||
|
{'path': '/local_link_connection/port_id',
|
||||||
|
'value': '88-5A-92-EC-54-58', 'op': 'add'},
|
||||||
|
{'path': '/local_link_connection/switch_id',
|
||||||
|
'value': '88-5A-92-EC-54-59', 'op': 'add'}
|
||||||
|
]
|
||||||
|
self.hook.before_update(self.data, self.node_info)
|
||||||
|
self.assertCalledWithPatch(patches, mock_patch)
|
||||||
|
|
||||||
|
@mock.patch.object(node_cache.NodeInfo, 'patch_port')
|
||||||
|
def test_lldp_none(self, mock_patch):
|
||||||
|
self.data['inventory']['interfaces'][0]['lldp'] = None
|
||||||
|
patches = []
|
||||||
|
self.hook.before_update(self.data, self.node_info)
|
||||||
|
self.assertCalledWithPatch(patches, mock_patch)
|
||||||
|
|
||||||
|
@mock.patch.object(node_cache.NodeInfo, 'patch_port')
|
||||||
|
def test_interface_not_in_all_interfaces(self, mock_patch):
|
||||||
|
self.data['all_interfaces'] = {}
|
||||||
|
patches = []
|
||||||
|
self.hook.before_update(self.data, self.node_info)
|
||||||
|
self.assertCalledWithPatch(patches, mock_patch)
|
||||||
|
|
||||||
|
def test_no_inventory(self):
|
||||||
|
del self.data['inventory']
|
||||||
|
self.assertRaises(utils.Error, self.hook.before_update,
|
||||||
|
self.data, self.node_info)
|
||||||
|
|
||||||
|
@mock.patch.object(node_cache.NodeInfo, 'patch_port')
|
||||||
|
def test_no_overwrite(self, mock_patch):
|
||||||
|
cfg.CONF.set_override('overwrite_existing', False, group='processing')
|
||||||
|
patches = [
|
||||||
|
{'path': '/local_link_connection/switch_id',
|
||||||
|
'value': '88-5A-92-EC-54-59', 'op': 'add'}
|
||||||
|
]
|
||||||
|
self.hook.before_update(self.data, self.node_info)
|
||||||
|
self.assertCalledWithPatch(patches, mock_patch)
|
@ -179,7 +179,7 @@ class TestSetCapabilityAction(test_base.NodeTest):
|
|||||||
self.act.apply(self.node_info, self.params)
|
self.act.apply(self.node_info, self.params)
|
||||||
mock_patch.assert_called_once_with(
|
mock_patch.assert_called_once_with(
|
||||||
[{'op': 'add', 'path': '/properties/capabilities',
|
[{'op': 'add', 'path': '/properties/capabilities',
|
||||||
'value': 'cap1:val'}])
|
'value': 'cap1:val'}], mock.ANY)
|
||||||
|
|
||||||
@mock.patch.object(node_cache.NodeInfo, 'patch')
|
@mock.patch.object(node_cache.NodeInfo, 'patch')
|
||||||
def test_apply_with_existing(self, mock_patch):
|
def test_apply_with_existing(self, mock_patch):
|
||||||
@ -203,7 +203,7 @@ class TestExtendAttributeAction(test_base.NodeTest):
|
|||||||
def test_apply(self, mock_patch):
|
def test_apply(self, mock_patch):
|
||||||
self.act.apply(self.node_info, self.params)
|
self.act.apply(self.node_info, self.params)
|
||||||
mock_patch.assert_called_once_with(
|
mock_patch.assert_called_once_with(
|
||||||
[{'op': 'add', 'path': '/extra/value', 'value': [42]}])
|
[{'op': 'add', 'path': '/extra/value', 'value': [42]}], mock.ANY)
|
||||||
|
|
||||||
@mock.patch.object(node_cache.NodeInfo, 'patch')
|
@mock.patch.object(node_cache.NodeInfo, 'patch')
|
||||||
def test_apply_non_empty(self, mock_patch):
|
def test_apply_non_empty(self, mock_patch):
|
||||||
@ -211,7 +211,8 @@ class TestExtendAttributeAction(test_base.NodeTest):
|
|||||||
self.act.apply(self.node_info, self.params)
|
self.act.apply(self.node_info, self.params)
|
||||||
|
|
||||||
mock_patch.assert_called_once_with(
|
mock_patch.assert_called_once_with(
|
||||||
[{'op': 'replace', 'path': '/extra/value', 'value': [0, 42]}])
|
[{'op': 'replace', 'path': '/extra/value', 'value': [0, 42]}],
|
||||||
|
mock.ANY)
|
||||||
|
|
||||||
@mock.patch.object(node_cache.NodeInfo, 'patch')
|
@mock.patch.object(node_cache.NodeInfo, 'patch')
|
||||||
def test_apply_unique_with_existing(self, mock_patch):
|
def test_apply_unique_with_existing(self, mock_patch):
|
||||||
|
5
releasenotes/notes/add-lldp-plugin-4645596cb8b39fd3.yaml
Normal file
5
releasenotes/notes/add-lldp-plugin-4645596cb8b39fd3.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Added GenericLocalLinkConnectionHook processing plugin to process LLDP data
|
||||||
|
returned during inspection and set port ID and switch ID in an Ironic
|
||||||
|
node's port local link connection information using that data.
|
@ -32,6 +32,7 @@ ironic_inspector.hooks.processing =
|
|||||||
extra_hardware = ironic_inspector.plugins.extra_hardware:ExtraHardwareHook
|
extra_hardware = ironic_inspector.plugins.extra_hardware:ExtraHardwareHook
|
||||||
raid_device = ironic_inspector.plugins.raid_device:RaidDeviceDetection
|
raid_device = ironic_inspector.plugins.raid_device:RaidDeviceDetection
|
||||||
capabilities = ironic_inspector.plugins.capabilities:CapabilitiesHook
|
capabilities = ironic_inspector.plugins.capabilities:CapabilitiesHook
|
||||||
|
local_link_connection = ironic_inspector.plugins.local_link_connection:GenericLocalLinkConnectionHook
|
||||||
ironic_inspector.hooks.node_not_found =
|
ironic_inspector.hooks.node_not_found =
|
||||||
example = ironic_inspector.plugins.example:example_not_found_hook
|
example = ironic_inspector.plugins.example:example_not_found_hook
|
||||||
enroll = ironic_inspector.plugins.discovery:enroll_node_not_found_hook
|
enroll = ironic_inspector.plugins.discovery:enroll_node_not_found_hook
|
||||||
|
Loading…
Reference in New Issue
Block a user