diff --git a/doc/source/http-api.rst b/doc/source/http-api.rst index 6759ef2d8..f0e22098c 100644 --- a/doc/source/http-api.rst +++ b/doc/source/http-api.rst @@ -243,6 +243,8 @@ the ramdisk. Request body: JSON dictionary with at least these keys: * ``mac_address`` MAC (physical) address of the interface. + * ``client_id`` InfiniBand Client-ID, for Ethernet is None. + * ``root_disk`` default deployment root disk as calculated by the ironic-python-agent algorithm. diff --git a/doc/source/usage.rst b/doc/source/usage.rst index a0606c2c2..421a4801f 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -354,3 +354,33 @@ a CPU flag and a capability, for example:: cpu_flags = aes:cpu_aes,svm:cpu_vt,vmx:cpu_vt See the default value of this option for a more detail example. + +InfiniBand support +^^^^^^^^^^^^^^^^^^ +Starting with the Ocata release, **Ironic Inspector** supports detection of +InfiniBand network interfaces. A recent (Ocata or newer) IPA image is required +for that to work. When an InfiniBand network interface is discovered, the +**Ironic Inspector** adds a ``client-id`` attribute to the ``extra`` attribute +in the ironic port. The **Ironic Inspector** should be configured with +``firewall.ethoib_interfaces`` to indicate the Ethernet Over InfiniBand (EoIB) +which are used for physical access access to the DHCP network. +For example if **Ironic Inspector** DHCP server is using ``br-inspector`` and +the ``br-inspector`` has EoIB port e.g. ``eth0``, +the ``firewall.ethoib_interfaces`` should be set to ``eth0``. +The ``firewall.ethoib_interfaces`` allows to map the baremetal GUID to it's +EoIB MAC based on the neighs files. This is needed for blocking DHCP traffic +of the nodes (MACs) which are not part of the introspection. + +The format of the ``/sys/class/net//eth/neighs`` file:: + + # EMAC= IMAC= + # For example: + IMAC=97:fe:80:00:00:00:00:00:00:7c:fe:90:03:00:29:26:52 + qp number=97:fe + lid=80:00:00:00:00:00:00 + GUID=7c:fe:90:03:00:29:26:52 + +Example of content:: + + EMAC=02:00:02:97:00:01 IMAC=97:fe:80:00:00:00:00:00:00:7c:fe:90:03:00:29:26:52 + EMAC=02:00:00:61:00:02 IMAC=61:fe:80:00:00:00:00:00:00:7c:fe:90:03:00:29:24:4f diff --git a/example.conf b/example.conf index 2675f0710..15ec91ec5 100644 --- a/example.conf +++ b/example.conf @@ -400,6 +400,10 @@ # iptables chain name to use. (string value) #firewall_chain = ironic-inspector +# List of Etherent Over InfiniBand interfaces on the Ironic host which +# are used for Inspector DHCP (list value) +#ethoib_interfaces = + [ironic] diff --git a/ironic_inspector/conf.py b/ironic_inspector/conf.py index 4385a750c..64fde16cf 100644 --- a/ironic_inspector/conf.py +++ b/ironic_inspector/conf.py @@ -41,9 +41,18 @@ FIREWALL_OPTS = [ cfg.StrOpt('firewall_chain', default='ironic-inspector', help=_('iptables chain name to use.')), + cfg.ListOpt('ethoib_interfaces', + default=[], + help=_('List of Etherent Over InfiniBand interfaces ' + 'on the Inspector host which are used for physical ' + 'access to the DHCP network. Multiple interfaces would ' + 'be attached to a bond or bridge specified in ' + 'dnsmasq_interface. The MACs of the InfiniBand nodes ' + 'which are not in desired state are going to be ' + 'blacklisted based on the list of neighbor MACs ' + 'on these interfaces.')), ] - PROCESSING_OPTS = [ cfg.StrOpt('add_ports', default='pxe', diff --git a/ironic_inspector/firewall.py b/ironic_inspector/firewall.py index 75b689c51..62da7a5c6 100644 --- a/ironic_inspector/firewall.py +++ b/ironic_inspector/firewall.py @@ -13,6 +13,7 @@ import contextlib import os +import re import subprocess from eventlet import semaphore @@ -33,6 +34,7 @@ LOCK = semaphore.BoundedSemaphore() BASE_COMMAND = None BLACKLIST_CACHE = None ENABLED = True +EMAC_REGEX = 'EMAC=([0-9a-f]{2}(:[0-9a-f]{2}){5}) IMAC=.*' def _iptables(*args, **kwargs): @@ -177,15 +179,19 @@ def update_filters(ironic=None): assert INTERFACE is not None ironic = ir_utils.get_client() if ironic is None else ironic - with LOCK: if not _should_enable_dhcp(): _disable_dhcp() return - macs_active = set(p.address for p in ironic.port.list(limit=0)) + ports_active = ironic.port.list(limit=0, fields=['address', 'extra']) + macs_active = set(p.address for p in ports_active) to_blacklist = macs_active - node_cache.active_macs() - if BLACKLIST_CACHE is not None and to_blacklist == BLACKLIST_CACHE: + ib_mac_mapping = ( + _ib_mac_to_rmac_mapping(to_blacklist, ports_active)) + + if (BLACKLIST_CACHE is not None and + to_blacklist == BLACKLIST_CACHE and not ib_mac_mapping): LOG.debug('Not updating iptables - no changes in MAC list %s', to_blacklist) return @@ -197,6 +203,7 @@ def update_filters(ironic=None): with _temporary_chain(NEW_CHAIN, CHAIN): # - Blacklist active macs, so that nova can boot them for mac in to_blacklist: + mac = ib_mac_mapping.get(mac) or mac _iptables('-A', NEW_CHAIN, '-m', 'mac', '--mac-source', mac, '-j', 'DROP') # - Whitelist everything else @@ -205,3 +212,48 @@ def update_filters(ironic=None): # Cache result of successful iptables update ENABLED = True BLACKLIST_CACHE = to_blacklist + + +def _ib_mac_to_rmac_mapping(blacklist_macs, ports_active): + """Mapping between host InfiniBand MAC to EthernetOverInfiniBand MAC + + On InfiniBand deployment we need to map between the baremetal host + InfiniBand MAC to the EoIB MAC. The EoIB MAC addresses are learned + automatically by the EoIB interfaces and those MACs are recorded + to the /sys/class/net//eth/neighs file. + The InfiniBand GUID is taken from the ironic port client-id extra + attribute. The InfiniBand GUID is the last 8 bytes of the client-id. + The file format allows to map the GUID to EoIB MAC. The firewall + rules based on those MACs get applied to the dnsmasq_interface by the + update_filters function. + + :param blacklist_macs: List of InfiniBand baremetal hosts macs to + blacklist. + :param ports_active: list of active ironic ports + :return baremetal InfiniBand to remote mac on ironic node mapping + """ + ethoib_interfaces = CONF.firewall.ethoib_interfaces + ib_mac_to_remote_mac = {} + for interface in ethoib_interfaces: + neighs_file = ( + os.path.join('/sys/class/net', interface, 'eth/neighs')) + try: + with open(neighs_file, 'r') as fd: + data = fd.read() + except IOError: + LOG.error( + _LE('Interface %s is not Ethernet Over InfiniBand; ' + 'Skipping ...'), interface) + continue + for port in ports_active: + if port.address in blacklist_macs: + client_id = port.extra.get('client-id') + if client_id: + # Note(moshele): The last 8 bytes in the client-id is + # the baremetal node InfiniBand GUID + guid = client_id[-23:] + p = re.compile(EMAC_REGEX + guid) + match = p.search(data) + if match: + ib_mac_to_remote_mac[port.address] = match.group(1) + return ib_mac_to_remote_mac diff --git a/ironic_inspector/node_cache.py b/ironic_inspector/node_cache.py index 10a29a114..5fd6fde7e 100644 --- a/ironic_inspector/node_cache.py +++ b/ironic_inspector/node_cache.py @@ -341,17 +341,31 @@ class NodeInfo(object): self._node = ir_utils.get_node(self.uuid, ironic=ironic) return self._node - def create_ports(self, macs, ironic=None): + def create_ports(self, ports, ironic=None): """Create one or several ports for this node. + :param ports: List of ports with all their attributes + e.g [{'mac': xx, 'ip': xx, 'client_id': None}, + {'mac': xx, 'ip': None, 'client_id': None}] + It also support the old style of list of macs. A warning is issued if port already exists on a node. + :param ironic: Ironic client to use instead of self.ironic """ existing_macs = [] - for mac in macs: + for port in ports: + mac = port + extra = {} + if isinstance(port, dict): + mac = port['mac'] + client_id = port.get('client_id') + if client_id: + extra = {'client-id': client_id} + if mac not in self.ports(): - self._create_port(mac, ironic) + self._create_port(mac, ironic=ironic, extra=extra) else: existing_macs.append(mac) + if existing_macs: LOG.warning(_LW('Did not create ports %s as they already exist'), existing_macs, node_info=self) @@ -369,10 +383,11 @@ class NodeInfo(object): ironic.node.list_ports(self.uuid, limit=0)} return self._ports - def _create_port(self, mac, ironic=None): + def _create_port(self, mac, ironic=None, extra=None): ironic = ironic or self.ironic try: - port = ironic.port.create(node_uuid=self.uuid, address=mac) + port = ironic.port.create( + node_uuid=self.uuid, address=mac, extra=extra) except exceptions.Conflict: LOG.warning(_LW('Port %s already exists, skipping'), mac, node_info=self) diff --git a/ironic_inspector/plugins/standard.py b/ironic_inspector/plugins/standard.py index a62110a62..f6d9c7073 100644 --- a/ironic_inspector/plugins/standard.py +++ b/ironic_inspector/plugins/standard.py @@ -153,6 +153,7 @@ class ValidateInterfacesHook(base.ProcessingHook): name = iface.get('name') mac = iface.get('mac_address') ip = iface.get('ipv4_address') + client_id = iface.get('client_id') if not name: LOG.error(_LE('Malformed interface record: %s'), @@ -173,10 +174,11 @@ class ValidateInterfacesHook(base.ProcessingHook): mac = mac.lower() - LOG.debug('Found interface %(name)s with MAC "%(mac)s" and ' - 'IP address "%(ip)s"', - {'name': name, 'mac': mac, 'ip': ip}, data=data) - result[name] = {'ip': ip, 'mac': mac} + LOG.debug('Found interface %(name)s with MAC "%(mac)s", ' + 'IP address "%(ip)s" and client_id "%(client_id)s"', + {'name': name, 'mac': mac, 'ip': ip, + 'client_id': client_id}, data=data) + result[name] = {'ip': ip, 'mac': mac, 'client_id': client_id} return result @@ -199,6 +201,7 @@ class ValidateInterfacesHook(base.ProcessingHook): for name, iface in interfaces.items(): mac = iface.get('mac') ip = iface.get('ip') + client_id = iface.get('client_id') if name == 'lo' or (ip and netaddr.IPAddress(ip).is_loopback()): LOG.debug('Skipping local interface %s', name, data=data) @@ -215,7 +218,8 @@ class ValidateInterfacesHook(base.ProcessingHook): name, data=data) continue - result[name] = {'ip': ip, 'mac': mac.lower()} + result[name] = {'ip': ip, 'mac': mac.lower(), + 'client_id': client_id} if not result: raise utils.Error(_('No suitable interfaces found in %s') % diff --git a/ironic_inspector/process.py b/ironic_inspector/process.py index bf97903a7..e7bc57546 100644 --- a/ironic_inspector/process.py +++ b/ironic_inspector/process.py @@ -268,9 +268,8 @@ def _run_post_hooks(node_info, introspection_data): def _process_node(node_info, node, introspection_data): # NOTE(dtantsur): repeat the check in case something changed ir_utils.check_provision_state(node) - - node_info.create_ports(introspection_data.get('macs') or ()) - + interfaces = introspection_data.get('interfaces') + node_info.create_ports(list(interfaces.values())) _run_post_hooks(node_info, introspection_data) _store_data(node_info, introspection_data) @@ -434,7 +433,8 @@ def _reapply_with_data(node_info, introspection_data): 'introspection on stored data:\n%s') % '\n'.join(failures), node_info=node_info) - node_info.create_ports(introspection_data.get('macs') or ()) + interfaces = introspection_data.get('interfaces') + node_info.create_ports(list(interfaces.values())) _run_post_hooks(node_info, introspection_data) _store_data(node_info, introspection_data) node_info.invalidate_cache() diff --git a/ironic_inspector/test/base.py b/ironic_inspector/test/base.py index 11dabdd35..5457bfe1c 100644 --- a/ironic_inspector/test/base.py +++ b/ironic_inspector/test/base.py @@ -89,12 +89,21 @@ class InventoryTest(BaseTest): # Prepare some realistic inventory # https://github.com/openstack/ironic-inspector/blob/master/HTTP-API.rst # noqa self.bmc_address = '1.2.3.4' - self.macs = ['11:22:33:44:55:66', '66:55:44:33:22:11'] - self.ips = ['1.2.1.2', '1.2.1.1'] + self.macs = ( + ['11:22:33:44:55:66', '66:55:44:33:22:11', '7c:fe:90:29:26:52']) + self.ips = ['1.2.1.2', '1.2.1.1', '1.2.1.3'] self.inactive_mac = '12:12:21:12:21:12' self.pxe_mac = self.macs[0] self.all_macs = self.macs + [self.inactive_mac] self.pxe_iface_name = 'eth1' + self.client_id = ( + 'ff:00:00:00:00:00:02:00:00:02:c9:00:7c:fe:90:03:00:29:26:52') + self.valid_interfaces = { + self.pxe_iface_name: {'ip': self.ips[0], 'mac': self.macs[0], + 'client_id': None}, + 'ib0': {'ip': self.ips[2], 'mac': self.macs[2], + 'client_id': self.client_id} + } self.data = { 'boot_interface': '01-' + self.pxe_mac.replace(':', '-'), 'inventory': { @@ -104,6 +113,9 @@ class InventoryTest(BaseTest): {'name': 'eth2', 'mac_address': self.inactive_mac}, {'name': 'eth3', 'mac_address': self.macs[1], 'ipv4_address': self.ips[1]}, + {'name': 'ib0', 'mac_address': self.macs[2], + 'ipv4_address': self.ips[2], + 'client_id': self.client_id} ], 'disks': [ {'name': '/dev/sda', 'model': 'Big Data Disk', @@ -123,16 +135,25 @@ class InventoryTest(BaseTest): 'root_disk': {'name': '/dev/sda', 'model': 'Big Data Disk', 'size': 1000 * units.Gi, 'wwn': None}, + 'interfaces': self.valid_interfaces, } self.inventory = self.data['inventory'] self.all_interfaces = { - 'eth1': {'mac': self.macs[0], 'ip': self.ips[0]}, - 'eth2': {'mac': self.inactive_mac, 'ip': None}, - 'eth3': {'mac': self.macs[1], 'ip': self.ips[1]} + 'eth1': {'mac': self.macs[0], 'ip': self.ips[0], + 'client_id': None}, + 'eth2': {'mac': self.inactive_mac, 'ip': None, 'client_id': None}, + 'eth3': {'mac': self.macs[1], 'ip': self.ips[1], + 'client_id': None}, + 'ib0': {'mac': self.macs[2], 'ip': self.ips[2], + 'client_id': self.client_id} } self.active_interfaces = { - 'eth1': {'mac': self.macs[0], 'ip': self.ips[0]}, - 'eth3': {'mac': self.macs[1], 'ip': self.ips[1]} + 'eth1': {'mac': self.macs[0], 'ip': self.ips[0], + 'client_id': None}, + 'eth3': {'mac': self.macs[1], 'ip': self.ips[1], + 'client_id': None}, + 'ib0': {'mac': self.macs[2], 'ip': self.ips[2], + 'client_id': self.client_id} } self.pxe_interfaces = { self.pxe_iface_name: self.all_interfaces[self.pxe_iface_name] diff --git a/ironic_inspector/test/functional.py b/ironic_inspector/test/functional.py index 213a8c3d7..6cdd0ae0f 100644 --- a/ironic_inspector/test/functional.py +++ b/ironic_inspector/test/functional.py @@ -249,7 +249,35 @@ class Test(Base): self.cli.node.update.assert_called_once_with(self.uuid, mock.ANY) self.assertCalledWithPatch(self.patch, self.cli.node.update) self.cli.port.create.assert_called_once_with( - node_uuid=self.uuid, address='11:22:33:44:55:66') + node_uuid=self.uuid, address='11:22:33:44:55:66', extra={}) + + status = self.call_get_status(self.uuid) + self.check_status(status, finished=True) + + def test_bmc_with_client_id(self): + self.pxe_mac = self.macs[2] + self.data['boot_interface'] = ('20-' + self.pxe_mac.replace(':', '-')) + self.pxe_iface_name = 'ib0' + self.pxe_interfaces = { + self.pxe_iface_name: self.all_interfaces[self.pxe_iface_name] + } + self.call_introspect(self.uuid) + eventlet.greenthread.sleep(DEFAULT_SLEEP) + self.cli.node.set_power_state.assert_called_once_with(self.uuid, + 'reboot') + + status = self.call_get_status(self.uuid) + self.check_status(status, finished=False) + + res = self.call_continue(self.data) + self.assertEqual({'uuid': self.uuid}, res) + eventlet.greenthread.sleep(DEFAULT_SLEEP) + + self.cli.node.update.assert_called_once_with(self.uuid, mock.ANY) + self.assertCalledWithPatch(self.patch, self.cli.node.update) + self.cli.port.create.assert_called_once_with( + node_uuid=self.uuid, address=self.macs[2], + extra={'client-id': self.client_id}) status = self.call_get_status(self.uuid) self.check_status(status, finished=True) @@ -279,7 +307,7 @@ class Test(Base): self.assertCalledWithPatch(self.patch + patch_credentials, self.cli.node.update) self.cli.port.create.assert_called_once_with( - node_uuid=self.uuid, address='11:22:33:44:55:66') + node_uuid=self.uuid, address='11:22:33:44:55:66', extra={}) status = self.call_get_status(self.uuid) self.check_status(status, finished=True) @@ -482,7 +510,7 @@ class Test(Base): self.assertCalledWithPatch(self.patch_root_hints, self.cli.node.update) self.cli.port.create.assert_called_once_with( - node_uuid=self.uuid, address='11:22:33:44:55:66') + node_uuid=self.uuid, address='11:22:33:44:55:66', extra={}) status = self.call_get_status(self.uuid) self.check_status(status, finished=True) @@ -708,7 +736,7 @@ class Test(Base): self.cli.node.update.assert_called_once_with(self.uuid, mock.ANY) self.assertCalledWithPatch(self.patch, self.cli.node.update) self.cli.port.create.assert_called_once_with( - node_uuid=self.uuid, address='11:22:33:44:55:66') + node_uuid=self.uuid, extra={}, address='11:22:33:44:55:66') status = self.call_get_status(self.uuid) self.check_status(status, finished=True) diff --git a/ironic_inspector/test/unit/test_firewall.py b/ironic_inspector/test/unit/test_firewall.py index cd0ef1ea0..15ea42677 100644 --- a/ironic_inspector/test/unit/test_firewall.py +++ b/ironic_inspector/test/unit/test_firewall.py @@ -26,12 +26,18 @@ from ironic_inspector.test import base as test_base CONF = cfg.CONF +IB_DATA = """ +EMAC=02:00:02:97:00:01 IMAC=97:fe:80:00:00:00:00:00:00:7c:fe:90:03:00:29:26:52 +EMAC=02:00:00:61:00:02 IMAC=61:fe:80:00:00:00:00:00:00:7c:fe:90:03:00:29:24:4f +""" @mock.patch.object(firewall, '_iptables') @mock.patch.object(ir_utils, 'get_client') @mock.patch.object(subprocess, 'check_call') class TestFirewall(test_base.NodeTest): + CLIENT_ID = 'ff:00:00:00:00:00:02:00:00:02:c9:00:7c:fe:90:03:00:29:24:4f' + def test_update_filters_without_manage_firewall(self, mock_call, mock_get_client, mock_iptables): @@ -341,3 +347,99 @@ class TestFirewall(test_base.NodeTest): mock_iptables.assert_any_call('-A', firewall.NEW_CHAIN, '-j', 'ACCEPT') self.assertEqual({'foobar'}, firewall.BLACKLIST_CACHE) + + def test_update_filters_infiniband( + self, mock_call, mock_get_client, mock_iptables): + + CONF.set_override('ethoib_interfaces', ['eth0'], 'firewall') + active_macs = ['11:22:33:44:55:66', '66:55:44:33:22:11'] + expected_rmac = '02:00:00:61:00:02' + ports = [mock.Mock(address=m) for m in active_macs] + ports.append(mock.Mock(address='7c:fe:90:29:24:4f', + extra={'client-id': self.CLIENT_ID}, + spec=['address', 'extra'])) + mock_get_client.port.list.return_value = ports + node_cache.add_node(self.node.uuid, mac=active_macs, + state=istate.States.finished, + bmc_address='1.2.3.4', foo=None) + firewall.init() + + update_filters_expected_args = [ + ('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport', + '67', '-j', CONF.firewall.firewall_chain), + ('-F', CONF.firewall.firewall_chain), + ('-X', CONF.firewall.firewall_chain), + ('-N', CONF.firewall.firewall_chain), + ('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport', + '67', '-j', firewall.NEW_CHAIN), + ('-F', firewall.NEW_CHAIN), + ('-X', firewall.NEW_CHAIN), + ('-N', firewall.NEW_CHAIN), + # Blacklist + ('-A', firewall.NEW_CHAIN, '-m', 'mac', '--mac-source', + expected_rmac, '-j', 'DROP'), + ('-A', firewall.NEW_CHAIN, '-j', 'ACCEPT'), + ('-I', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport', + '67', '-j', firewall.NEW_CHAIN), + ('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport', + '67', '-j', CONF.firewall.firewall_chain), + ('-F', CONF.firewall.firewall_chain), + ('-X', CONF.firewall.firewall_chain), + ('-E', firewall.NEW_CHAIN, CONF.firewall.firewall_chain) + ] + + fileobj = mock.mock_open(read_data=IB_DATA) + with mock.patch('six.moves.builtins.open', fileobj, create=True): + firewall.update_filters(mock_get_client) + call_args_list = mock_iptables.call_args_list + + for (args, call) in zip(update_filters_expected_args, + call_args_list): + self.assertEqual(args, call[0]) + + def test_update_filters_infiniband_no_such_file( + self, mock_call, mock_get_client, mock_iptables): + + CONF.set_override('ethoib_interfaces', ['eth0'], 'firewall') + active_macs = ['11:22:33:44:55:66', '66:55:44:33:22:11'] + ports = [mock.Mock(address=m) for m in active_macs] + ports.append(mock.Mock(address='7c:fe:90:29:24:4f', + extra={'client-id': self.CLIENT_ID}, + spec=['address', 'extra'])) + mock_get_client.port.list.return_value = ports + node_cache.add_node(self.node.uuid, mac=active_macs, + state=istate.States.finished, + bmc_address='1.2.3.4', foo=None) + firewall.init() + + update_filters_expected_args = [ + ('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport', + '67', '-j', CONF.firewall.firewall_chain), + ('-F', CONF.firewall.firewall_chain), + ('-X', CONF.firewall.firewall_chain), + ('-N', CONF.firewall.firewall_chain), + ('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport', + '67', '-j', firewall.NEW_CHAIN), + ('-F', firewall.NEW_CHAIN), + ('-X', firewall.NEW_CHAIN), + ('-N', firewall.NEW_CHAIN), + # Blacklist + ('-A', firewall.NEW_CHAIN, '-m', 'mac', '--mac-source', + '7c:fe:90:29:24:4f', '-j', 'DROP'), + ('-A', firewall.NEW_CHAIN, '-j', 'ACCEPT'), + ('-I', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport', + '67', '-j', firewall.NEW_CHAIN), + ('-D', 'INPUT', '-i', 'br-ctlplane', '-p', 'udp', '--dport', + '67', '-j', CONF.firewall.firewall_chain), + ('-F', CONF.firewall.firewall_chain), + ('-X', CONF.firewall.firewall_chain), + ('-E', firewall.NEW_CHAIN, CONF.firewall.firewall_chain) + ] + + with mock.patch('six.moves.builtins.open', side_effect=IOError()): + firewall.update_filters(mock_get_client) + call_args_list = mock_iptables.call_args_list + + for (args, call) in zip(update_filters_expected_args, + call_args_list): + self.assertEqual(args, call[0]) diff --git a/ironic_inspector/test/unit/test_node_cache.py b/ironic_inspector/test/unit/test_node_cache.py index 605d372f3..4f1605023 100644 --- a/ironic_inspector/test/unit/test_node_cache.py +++ b/ironic_inspector/test/unit/test_node_cache.py @@ -70,7 +70,8 @@ class TestNodeCache(test_base.NodeTest): order_by(db.Attribute.name, db.Attribute.value).all()) self.assertEqual([('bmc_address', '1.2.3.4', self.uuid), ('mac', self.macs[0], self.uuid), - ('mac', self.macs[1], self.uuid)], + ('mac', self.macs[1], self.uuid), + ('mac', self.macs[2], self.uuid)], [(row.name, row.value, row.uuid) for row in res]) def test__delete_node(self): diff --git a/ironic_inspector/test/unit/test_process.py b/ironic_inspector/test/unit/test_process.py index ff2bf2936..6d38f2558 100644 --- a/ironic_inspector/test/unit/test_process.py +++ b/ironic_inspector/test/unit/test_process.py @@ -347,6 +347,10 @@ class TestProcessNode(BaseTest): 'processing') self.validate_attempts = 5 self.data['macs'] = self.macs # validate_interfaces hook + self.valid_interfaces['eth3'] = { + 'mac': self.macs[1], 'ip': self.ips[1], 'extra': {} + } + self.data['interfaces'] = self.valid_interfaces self.ports = self.all_ports self.new_creds = ('user', 'password') @@ -398,9 +402,11 @@ class TestProcessNode(BaseTest): process._process_node(self.node_info, self.node, self.data) self.cli.port.create.assert_any_call(node_uuid=self.uuid, - address=self.macs[0]) + address=self.macs[0], + extra={}) self.cli.port.create.assert_any_call(node_uuid=self.uuid, - address=self.macs[1]) + address=self.macs[1], + extra={}) self.cli.node.set_power_state.assert_called_once_with(self.uuid, 'off') self.assertFalse(self.cli.node.validate.called) @@ -414,9 +420,11 @@ class TestProcessNode(BaseTest): process._process_node(self.node_info, self.node, self.data) self.cli.port.create.assert_any_call(node_uuid=self.uuid, - address=self.macs[0]) + address=self.macs[0], + extra={}) self.cli.port.create.assert_any_call(node_uuid=self.uuid, - address=self.macs[1]) + address=self.macs[1], + extra={}) def test_set_ipmi_credentials(self): self.node_info.set_option('new_ipmi_credentials', self.new_creds) @@ -653,7 +661,8 @@ class TestReapplyNode(BaseTest): # behind validate_interfaces self.cli.port.create.assert_called_once_with( node_uuid=self.uuid, - address=swifted_data['macs'][0] + address=swifted_data['macs'][0], + extra={} ) @prepare_mocks @@ -707,7 +716,6 @@ class TestReapplyNode(BaseTest): swift_mock.get_object.return_value = json.dumps(self.data) exc = Exception('Oops') self.cli.port.create.side_effect = exc - self.call() finished_mock.assert_called_once_with(self.node_info, error=str(exc)) diff --git a/releasenotes/notes/infiniband-support-960d6846e326dec4.yaml b/releasenotes/notes/infiniband-support-960d6846e326dec4.yaml new file mode 100644 index 000000000..7d03c0425 --- /dev/null +++ b/releasenotes/notes/infiniband-support-960d6846e326dec4.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + InfiniBand interface discovery is supported in the introspection. + Therefore the ironic-inspector will add the client-id to the corresponding + ironic port that represent InfiniBand interface. The ironic-inspector + should be configured with ``firewall.ethoib_interfaces`` to indicate what are + Ethernet Over InfiniBand Interfaces that are used for DHCP.