Merge "Adding InfiniBand Support"

This commit is contained in:
Jenkins 2017-02-02 16:29:28 +00:00 committed by Gerrit Code Review
commit 516ddc424f
14 changed files with 320 additions and 36 deletions

View File

@ -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.

View File

@ -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/<ethoib>/eth/neighs`` file::
# EMAC=<ethernet mac of the ethoib> IMAC=<qp number:lid:GUID>
# 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

View File

@ -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]

View File

@ -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',

View File

@ -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/<ethoib_interface>/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

View File

@ -343,17 +343,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)
@ -371,10 +385,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)

View File

@ -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') %

View File

@ -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()

View File

@ -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]

View File

@ -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)

View File

@ -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])

View File

@ -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):

View File

@ -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))

View File

@ -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.