From 1bdcd4449f6dc3c39cffcefa8efba243b7c00456 Mon Sep 17 00:00:00 2001 From: Moshe Levi Date: Thu, 17 Nov 2016 02:11:04 +0200 Subject: [PATCH] Add a new Hardware Manager for Mellanox NICs This patch add Mellanox Manager to support Mellanox InfiniBand NICs. It adds client_id to the NetworkInterface for the InfiniBand network interface. The Mellanox Manager provides it own implementation of get_interface_info. The mlnx get_interface_info generate InfiniBand MAC and client-id from the InfiniBand network interface address. Closes-Bug: #1532534 Change-Id: I4e7f7649a1bdeaa3ee99b2748037b0f37fea486c --- ironic_python_agent/hardware.py | 9 +- .../hardware_managers/__init__.py | 0 ironic_python_agent/hardware_managers/mlnx.py | 111 +++++++++++++++ ironic_python_agent/netutils.py | 1 + .../tests/unit/hardware_managers/__init__.py | 0 .../tests/unit/hardware_managers/test_mlnx.py | 130 ++++++++++++++++++ ...nox_hardware_manager-edfae87964737df1.yaml | 6 + setup.cfg | 1 + 8 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 ironic_python_agent/hardware_managers/__init__.py create mode 100644 ironic_python_agent/hardware_managers/mlnx.py create mode 100755 ironic_python_agent/tests/unit/hardware_managers/__init__.py create mode 100755 ironic_python_agent/tests/unit/hardware_managers/test_mlnx.py create mode 100644 releasenotes/notes/add_mellanox_hardware_manager-edfae87964737df1.yaml diff --git a/ironic_python_agent/hardware.py b/ironic_python_agent/hardware.py index 7c8787753..f30594993 100644 --- a/ironic_python_agent/hardware.py +++ b/ironic_python_agent/hardware.py @@ -207,10 +207,11 @@ class BlockDevice(encoding.SerializableComparable): class NetworkInterface(encoding.SerializableComparable): serializable_fields = ('name', 'mac_address', 'switch_port_descr', 'switch_chassis_descr', 'ipv4_address', - 'has_carrier', 'lldp', 'vendor', 'product') + 'has_carrier', 'lldp', 'vendor', 'product', + 'client_id') def __init__(self, name, mac_addr, ipv4_address=None, has_carrier=True, - lldp=None, vendor=None, product=None): + lldp=None, vendor=None, product=None, client_id=None): self.name = name self.mac_address = mac_addr self.ipv4_address = ipv4_address @@ -218,6 +219,10 @@ class NetworkInterface(encoding.SerializableComparable): self.lldp = lldp self.vendor = vendor self.product = product + # client_id is used for InfiniBand only. we calculate the DHCP + # client identifier Option to allow DHCP to work over InfiniBand. + # see https://tools.ietf.org/html/rfc4390 + self.client_id = client_id # TODO(sambetts) Remove these fields in Ocata, they have been # superseded by self.lldp self.switch_port_descr = None diff --git a/ironic_python_agent/hardware_managers/__init__.py b/ironic_python_agent/hardware_managers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ironic_python_agent/hardware_managers/mlnx.py b/ironic_python_agent/hardware_managers/mlnx.py new file mode 100644 index 000000000..63ef5b32f --- /dev/null +++ b/ironic_python_agent/hardware_managers/mlnx.py @@ -0,0 +1,111 @@ +# Copyright 2016 Mellanox Technologies, Ltd +# +# 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 os + +from ironic_python_agent import errors +from ironic_python_agent import hardware +from ironic_python_agent import netutils +from oslo_log import log + +LOG = log.getLogger() +# Mellanox NIC Vendor ID +MLNX_VENDOR_ID = '0x15b3' +# Mellanox Prefix to generate InfiniBand CLient-ID +MLNX_INFINIBAND_CLIENT_ID_PREFIX = 'ff:00:00:00:00:00:02:00:00:02:c9:00:' + + +def _infiniband_address_to_mac(address): + """Convert InfiniBand address to MAC + + Convert InfiniBand address to MAC by Mellanox specific + translation. The InfiniBand address is 59 characters + composed from GID:GUID. The last 24 characters are the + GUID. The InfiniBand MAC is upper 10 characters and lower + 9 characters from the GUID + Example: + address - a0:00:00:27:fe:80:00:00:00:00:00:00:7c:fe:90:03:00:29:26:52 + GUID - 7c:fe:90:03:00:29:26:52 + InfiniBand MAC - 7c:fe:90:29:26:52 + + :param address: InfiniBand Address. + :returns: InfiniBand MAC. + """ + return address[36:-14] + address[51:] + + +def _generate_client_id(address): + """Generate client id from InfiniBand address + + :param address: InfiniBand address. + :returns: client id. + """ + return MLNX_INFINIBAND_CLIENT_ID_PREFIX + address[36:] + + +def _detect_hardware(): + """method for detection of Mellanox NICs + + :return True/False + """ + iface_names = os.listdir('/sys/class/net') + for ifname in iface_names: + if (hardware._get_device_info(ifname, 'net', 'vendor') == + MLNX_VENDOR_ID): + return True + return False + + +class MellanoxDeviceHardwareManager(hardware.HardwareManager): + """Mellanox hardware manager to support a single device""" + + HARDWARE_MANAGER_NAME = 'MellanoxDeviceHardwareManager' + HARDWARE_MANAGER_VERSION = '1' + + def evaluate_hardware_support(self): + """Declare level of hardware support provided.""" + + if _detect_hardware(): + LOG.debug('Found Mellanox device') + return hardware.HardwareSupport.MAINLINE + else: + LOG.debug('No Mellanox devices found') + return hardware.HardwareSupport.NONE + + def get_interface_info(self, interface_name): + """Return the interface information when its Mellanox and InfiniBand + + In case of Mellanox and InfiniBand interface we do the following: + 1. Calculate the "InfiniBand MAC" according to InfiniBand GUID + 2. Calculate the client-id according to InfiniBand GUID + """ + + addr_path = '/sys/class/net/{0}/address'.format(interface_name) + with open(addr_path) as addr_file: + address = addr_file.read().strip() + vendor = hardware._get_device_info(interface_name, 'net', 'vendor') + if (len(address) != netutils.INFINIBAND_ADDR_LEN or + vendor != MLNX_VENDOR_ID): + raise errors.IncompatibleHardwareMethodError() + + mac_addr = _infiniband_address_to_mac(address) + client_id = _generate_client_id(address) + + return hardware.NetworkInterface( + interface_name, mac_addr, + ipv4_address=netutils.get_ipv4_addr(interface_name), + has_carrier=netutils.interface_has_carrier(interface_name), + lldp=None, + vendor=vendor, + product=hardware._get_device_info(interface_name, 'net', 'device'), + client_id=client_id) diff --git a/ironic_python_agent/netutils.py b/ironic_python_agent/netutils.py index f1078fb8e..55c7a3eb3 100644 --- a/ironic_python_agent/netutils.py +++ b/ironic_python_agent/netutils.py @@ -30,6 +30,7 @@ LLDP_ETHERTYPE = 0x88cc IFF_PROMISC = 0x100 SIOCGIFFLAGS = 0x8913 SIOCSIFFLAGS = 0x8914 +INFINIBAND_ADDR_LEN = 59 class ifreq(ctypes.Structure): diff --git a/ironic_python_agent/tests/unit/hardware_managers/__init__.py b/ironic_python_agent/tests/unit/hardware_managers/__init__.py new file mode 100755 index 000000000..e69de29bb diff --git a/ironic_python_agent/tests/unit/hardware_managers/test_mlnx.py b/ironic_python_agent/tests/unit/hardware_managers/test_mlnx.py new file mode 100755 index 000000000..5341b46cb --- /dev/null +++ b/ironic_python_agent/tests/unit/hardware_managers/test_mlnx.py @@ -0,0 +1,130 @@ +# Copyright 2016 Mellanox Technologies, Ltd +# +# 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 os + +import mock +from oslotest import base as test_base + +from ironic_python_agent import errors +from ironic_python_agent import hardware +from ironic_python_agent.hardware_managers import mlnx + +IB_ADDRESS = 'a0:00:00:27:fe:80:00:00:00:00:00:00:7c:fe:90:03:00:29:26:52' +CLIENT_ID = 'ff:00:00:00:00:00:02:00:00:02:c9:00:7c:fe:90:03:00:29:26:52' + + +class MlnxHardwareManager(test_base.BaseTestCase): + def setUp(self): + super(MlnxHardwareManager, self).setUp() + self.hardware = mlnx.MellanoxDeviceHardwareManager() + self.node = {'uuid': 'dda135fb-732d-4742-8e72-df8f3199d244', + 'driver_internal_info': {}} + + def test_infiniband_address_to_mac(self): + self.assertEqual( + '7c:fe:90:29:26:52', + mlnx._infiniband_address_to_mac(IB_ADDRESS)) + + def test_generate_client_id(self): + self.assertEqual( + CLIENT_ID, + mlnx._generate_client_id(IB_ADDRESS)) + + @mock.patch.object(os, 'listdir') + @mock.patch('six.moves.builtins.open') + def test_detect_hardware(self, mocked_open, mock_listdir): + mock_listdir.return_value = ['eth0', 'ib0'] + mocked_open.return_value.__enter__ = lambda s: s + mocked_open.return_value.__exit__ = mock.Mock() + read_mock = mocked_open.return_value.read + read_mock.side_effect = ['0x8086\n', '0x15b3\n'] + self.assertTrue(mlnx._detect_hardware()) + + @mock.patch.object(os, 'listdir') + @mock.patch('six.moves.builtins.open') + def test_detect_hardware_no_mlnx(self, mocked_open, mock_listdir): + mock_listdir.return_value = ['eth0', 'eth1'] + mocked_open.return_value.__enter__ = lambda s: s + mocked_open.return_value.__exit__ = mock.Mock() + read_mock = mocked_open.return_value.read + read_mock.side_effect = ['0x8086\n', '0x8086\n'] + self.assertFalse(mlnx._detect_hardware()) + + @mock.patch.object(os, 'listdir') + @mock.patch('six.moves.builtins.open') + def test_detect_hardware_error(self, mocked_open, mock_listdir): + mock_listdir.return_value = ['eth0', 'ib0'] + mocked_open.return_value.__enter__ = lambda s: s + mocked_open.return_value.__exit__ = mock.Mock() + read_mock = mocked_open.return_value.read + read_mock.side_effect = ['0x8086\n', OSError('boom')] + self.assertFalse(mlnx._detect_hardware()) + + @mock.patch.object(os, 'listdir') + @mock.patch('six.moves.builtins.open') + def test_evaluate_hardware_support(self, mocked_open, mock_listdir): + mock_listdir.return_value = ['eth0', 'ib0'] + mocked_open.return_value.__enter__ = lambda s: s + mocked_open.return_value.__exit__ = mock.Mock() + read_mock = mocked_open.return_value.read + read_mock.side_effect = ['0x8086\n', '0x15b3\n'] + self.assertEqual( + hardware.HardwareSupport.MAINLINE, + self.hardware.evaluate_hardware_support()) + + @mock.patch.object(os, 'listdir') + @mock.patch('six.moves.builtins.open') + def test_evaluate_hardware_support_no_mlnx( + self, mocked_open, mock_listdir): + mock_listdir.return_value = ['eth0', 'eth1'] + mocked_open.return_value.__enter__ = lambda s: s + mocked_open.return_value.__exit__ = mock.Mock() + read_mock = mocked_open.return_value.read + read_mock.side_effect = ['0x8086\n', '0x8086\n'] + self.assertEqual( + hardware.HardwareSupport.NONE, + self.hardware.evaluate_hardware_support()) + + @mock.patch('six.moves.builtins.open') + def test_get_interface_info(self, mocked_open): + mocked_open.return_value.__enter__ = lambda s: s + mocked_open.return_value.__exit__ = mock.Mock() + read_mock = mocked_open.return_value.read + read_mock.side_effect = [IB_ADDRESS, '0x15b3\n'] + network_interface = self.hardware.get_interface_info('ib0') + self.assertEqual('ib0', network_interface.name) + self.assertEqual('7c:fe:90:29:26:52', network_interface.mac_address) + self.assertEqual('0x15b3', network_interface.vendor) + self.assertEqual(CLIENT_ID, network_interface.client_id) + + @mock.patch('six.moves.builtins.open') + def test_get_interface_info_no_ib_interface(self, mocked_open): + mocked_open.return_value.__enter__ = lambda s: s + mocked_open.return_value.__exit__ = mock.Mock() + read_mock = mocked_open.return_value.read + read_mock.side_effect = ['7c:fe:90:29:26:52', '0x15b3\n'] + self.assertRaises( + errors.IncompatibleHardwareMethodError, + self.hardware.get_interface_info, 'eth0') + + @mock.patch('six.moves.builtins.open') + def test_get_interface_info_no_mlnx_interface(self, mocked_open): + mocked_open.return_value.__enter__ = lambda s: s + mocked_open.return_value.__exit__ = mock.Mock() + read_mock = mocked_open.return_value.read + read_mock.side_effect = [IB_ADDRESS, '0x8086\n'] + self.assertRaises( + errors.IncompatibleHardwareMethodError, + self.hardware.get_interface_info, 'ib0') diff --git a/releasenotes/notes/add_mellanox_hardware_manager-edfae87964737df1.yaml b/releasenotes/notes/add_mellanox_hardware_manager-edfae87964737df1.yaml new file mode 100644 index 000000000..bddffb30d --- /dev/null +++ b/releasenotes/notes/add_mellanox_hardware_manager-edfae87964737df1.yaml @@ -0,0 +1,6 @@ +--- +features: + - Add support for Mellanox InfiniBand NIC in IPA. + Each Mellanox InfiniBand interface returned with + "InfiniBand MAC" and InfiniBand Client-ID according + to DHCP over InfiniBand https://tools.ietf.org/html/rfc4390. \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index fbdb6d511..c4326c550 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,7 @@ ironic_python_agent.extensions = ironic_python_agent.hardware_managers = generic = ironic_python_agent.hardware:GenericHardwareManager + mlnx = ironic_python_agent.hardware_managers.mlnx:MellanoxDeviceHardwareManager ironic_python_agent.inspector.collectors = default = ironic_python_agent.inspector:collect_default