Browse Source
Adopting the PXE filter interface/driver concept Related-Bug: 1665666 Change-Id: If83db978080b9c4e5d51ba50bbe8ed26e29abe83changes/31/471831/33
20 changed files with 730 additions and 831 deletions
@ -1,257 +0,0 @@
|
||||
# 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 contextlib |
||||
import os |
||||
import re |
||||
|
||||
from eventlet.green import subprocess |
||||
from eventlet import semaphore |
||||
from oslo_config import cfg |
||||
from oslo_log import log |
||||
|
||||
from ironic_inspector.common import ironic as ir_utils |
||||
from ironic_inspector import node_cache |
||||
|
||||
|
||||
CONF = cfg.CONF |
||||
LOG = log.getLogger("ironic_inspector.firewall") |
||||
NEW_CHAIN = None |
||||
CHAIN = None |
||||
INTERFACE = None |
||||
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): |
||||
# NOTE(dtantsur): -w flag makes it wait for xtables lock |
||||
cmd = BASE_COMMAND + args |
||||
ignore = kwargs.pop('ignore', False) |
||||
LOG.debug('Running iptables %s', args) |
||||
kwargs['stderr'] = subprocess.STDOUT |
||||
try: |
||||
subprocess.check_output(cmd, **kwargs) |
||||
except subprocess.CalledProcessError as exc: |
||||
output = exc.output.replace('\n', '. ') |
||||
if ignore: |
||||
LOG.debug('Ignoring failed iptables %(args)s: %(output)s', |
||||
{'args': args, 'output': output}) |
||||
else: |
||||
LOG.error('iptables %(iptables)s failed: %(exc)s', |
||||
{'iptables': args, 'exc': output}) |
||||
raise |
||||
|
||||
|
||||
def init(): |
||||
"""Initialize firewall management. |
||||
|
||||
Must be called one on start-up. |
||||
""" |
||||
if not CONF.firewall.manage_firewall: |
||||
return |
||||
|
||||
global INTERFACE, CHAIN, NEW_CHAIN, BASE_COMMAND, BLACKLIST_CACHE |
||||
BLACKLIST_CACHE = None |
||||
INTERFACE = CONF.firewall.dnsmasq_interface |
||||
CHAIN = CONF.firewall.firewall_chain |
||||
NEW_CHAIN = CHAIN + '_temp' |
||||
BASE_COMMAND = ('sudo', 'ironic-inspector-rootwrap', |
||||
CONF.rootwrap_config, 'iptables',) |
||||
|
||||
# -w flag makes iptables wait for xtables lock, but it's not supported |
||||
# everywhere yet |
||||
try: |
||||
with open(os.devnull, 'wb') as null: |
||||
subprocess.check_call(BASE_COMMAND + ('-w', '-h'), |
||||
stderr=null, stdout=null) |
||||
except subprocess.CalledProcessError: |
||||
LOG.warning('iptables does not support -w flag, please update ' |
||||
'it to at least version 1.4.21') |
||||
else: |
||||
BASE_COMMAND += ('-w',) |
||||
|
||||
_clean_up(CHAIN) |
||||
# Not really needed, but helps to validate that we have access to iptables |
||||
_iptables('-N', CHAIN) |
||||
|
||||
|
||||
def _clean_up(chain): |
||||
_iptables('-D', 'INPUT', '-i', INTERFACE, '-p', 'udp', |
||||
'--dport', '67', '-j', chain, |
||||
ignore=True) |
||||
_iptables('-F', chain, ignore=True) |
||||
_iptables('-X', chain, ignore=True) |
||||
|
||||
|
||||
def clean_up(): |
||||
"""Clean up everything before exiting.""" |
||||
if not CONF.firewall.manage_firewall: |
||||
return |
||||
|
||||
_clean_up(CHAIN) |
||||
_clean_up(NEW_CHAIN) |
||||
|
||||
|
||||
def _should_enable_dhcp(): |
||||
"""Check whether we should enable DHCP at all. |
||||
|
||||
We won't even open our DHCP if no nodes are on introspection and |
||||
node_not_found_hook is not set. |
||||
""" |
||||
return (node_cache.introspection_active() or |
||||
CONF.processing.node_not_found_hook) |
||||
|
||||
|
||||
@contextlib.contextmanager |
||||
def _temporary_chain(chain, main_chain): |
||||
"""Context manager to operate on a temporary chain.""" |
||||
# Clean up a bit to account for possible troubles on previous run |
||||
_clean_up(chain) |
||||
_iptables('-N', chain) |
||||
|
||||
yield |
||||
|
||||
# Swap chains |
||||
_iptables('-I', 'INPUT', '-i', INTERFACE, '-p', 'udp', |
||||
'--dport', '67', '-j', chain) |
||||
_iptables('-D', 'INPUT', '-i', INTERFACE, '-p', 'udp', |
||||
'--dport', '67', '-j', main_chain, |
||||
ignore=True) |
||||
_iptables('-F', main_chain, ignore=True) |
||||
_iptables('-X', main_chain, ignore=True) |
||||
_iptables('-E', chain, main_chain) |
||||
|
||||
|
||||
def _disable_dhcp(): |
||||
"""Disable DHCP completely.""" |
||||
global ENABLED, BLACKLIST_CACHE |
||||
|
||||
if not ENABLED: |
||||
LOG.debug('DHCP is already disabled, not updating') |
||||
return |
||||
|
||||
LOG.debug('No nodes on introspection and node_not_found_hook is ' |
||||
'not set - disabling DHCP') |
||||
BLACKLIST_CACHE = None |
||||
with _temporary_chain(NEW_CHAIN, CHAIN): |
||||
# Blacklist everything |
||||
_iptables('-A', NEW_CHAIN, '-j', 'REJECT') |
||||
|
||||
ENABLED = False |
||||
|
||||
|
||||
def update_filters(ironic=None): |
||||
"""Update firewall filter rules for introspection. |
||||
|
||||
Gives access to PXE boot port for any machine, except for those, |
||||
whose MAC is registered in Ironic and is not on introspection right now. |
||||
|
||||
This function is called from both introspection initialization code and |
||||
from periodic task. This function is supposed to be resistant to unexpected |
||||
iptables state. |
||||
|
||||
``init()`` function must be called once before any call to this function. |
||||
This function is using ``eventlet`` semaphore to serialize access from |
||||
different green threads. |
||||
|
||||
Does nothing, if firewall management is disabled in configuration. |
||||
|
||||
:param ironic: Ironic client instance, optional. |
||||
""" |
||||
global BLACKLIST_CACHE, ENABLED |
||||
|
||||
if not CONF.firewall.manage_firewall: |
||||
return |
||||
|
||||
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 |
||||
|
||||
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() |
||||
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 |
||||
|
||||
LOG.debug('Blacklisting active MAC\'s %s', to_blacklist) |
||||
# Force update on the next iteration if this attempt fails |
||||
BLACKLIST_CACHE = 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 |
||||
_iptables('-A', NEW_CHAIN, '-j', 'ACCEPT') |
||||
|
||||
# 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('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 |
@ -0,0 +1,232 @@
|
||||
# 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 contextlib |
||||
import os |
||||
import re |
||||
|
||||
from eventlet.green import subprocess |
||||
from oslo_config import cfg |
||||
from oslo_log import log |
||||
|
||||
from ironic_inspector import node_cache |
||||
from ironic_inspector.pxe_filter import base as pxe_filter |
||||
|
||||
|
||||
CONF = cfg.CONF |
||||
LOG = log.getLogger(__name__) |
||||
|
||||
_EMAC_REGEX = 'EMAC=([0-9a-f]{2}(:[0-9a-f]{2}){5}) IMAC=.*' |
||||
|
||||
|
||||
def _should_enable_dhcp(): |
||||
"""Check whether we should enable DHCP at all. |
||||
|
||||
We won't even open our DHCP if no nodes are on introspection and |
||||
node_not_found_hook is not set. |
||||
""" |
||||
return (node_cache.introspection_active() or |
||||
CONF.processing.node_not_found_hook is not None) |
||||
|
||||
|
||||
class IptablesFilter(pxe_filter.BaseFilter): |
||||
"""A PXE boot filtering interface implementation.""" |
||||
|
||||
def __init__(self): |
||||
super(IptablesFilter, self).__init__() |
||||
self.blacklist_cache = None |
||||
self.enabled = True |
||||
self.interface = CONF.iptables.dnsmasq_interface |
||||
self.chain = CONF.iptables.firewall_chain |
||||
self.new_chain = self.chain + '_temp' |
||||
self.base_command = ('sudo', 'ironic-inspector-rootwrap', |
||||
CONF.rootwrap_config, 'iptables') |
||||
|
||||
def reset(self): |
||||
self.enabled = True |
||||
self.blacklist_cache = None |
||||
for chain in (self.chain, self.new_chain): |
||||
try: |
||||
self._clean_up(chain) |
||||
except Exception as e: |
||||
LOG.exception('Encountered exception resetting filter: %s', e) |
||||
super(IptablesFilter, self).reset() |
||||
|
||||
@pxe_filter.locked_driver_event(pxe_filter.Events.initialize) |
||||
def init_filter(self): |
||||
# -w flag makes iptables wait for xtables lock, but it's not supported |
||||
# everywhere yet |
||||
try: |
||||
with open(os.devnull, 'wb') as null: |
||||
subprocess.check_call(self.base_command + ('-w', '-h'), |
||||
stderr=null, stdout=null) |
||||
except subprocess.CalledProcessError: |
||||
LOG.warning('iptables does not support -w flag, please update ' |
||||
'it to at least version 1.4.21') |
||||
else: |
||||
self.base_command += ('-w',) |
||||
|
||||
self._clean_up(self.chain) |
||||
# Not really needed, but helps to validate that we have access to |
||||
# iptables |
||||
self._iptables('-N', self.chain) |
||||
LOG.debug('The iptables filter was initialized') |
||||
|
||||
@pxe_filter.locked_driver_event(pxe_filter.Events.sync) |
||||
def sync(self, ironic): |
||||
"""Sync firewall filter rules for introspection. |
||||
|
||||
Gives access to PXE boot port for any machine, except for those, whose |
||||
MAC is registered in Ironic and is not on introspection right now. |
||||
|
||||
This function is called from both introspection initialization code and |
||||
from periodic task. This function is supposed to be resistant to |
||||
unexpected iptables state. |
||||
|
||||
``init()`` function must be called once before any call to this |
||||
function. This function is using ``eventlet`` semaphore to serialize |
||||
access from different green threads. |
||||
|
||||
:param ironic: an ironic client instance. |
||||
:returns: nothing. |
||||
""" |
||||
if not _should_enable_dhcp(): |
||||
self._disable_dhcp() |
||||
return |
||||
|
||||
to_blacklist = _get_blacklist(ironic) |
||||
if to_blacklist == self.blacklist_cache: |
||||
LOG.debug('Not updating iptables - no changes in MAC list %s', |
||||
to_blacklist) |
||||
return |
||||
|
||||
LOG.debug('Blacklisting active MAC\'s %s', to_blacklist) |
||||
with self._temporary_chain(self.new_chain, self.chain): |
||||
# Force update on the next iteration if this attempt fails |
||||
self.blacklist_cache = None |
||||
# - Blacklist active macs, so that nova can boot them |
||||
for mac in to_blacklist: |
||||
self._iptables('-A', self.new_chain, '-m', 'mac', |
||||
'--mac-source', mac, '-j', 'DROP') |
||||
# - Whitelist everything else |
||||
self._iptables('-A', self.new_chain, '-j', 'ACCEPT') |
||||
|
||||
# Cache result of successful iptables update |
||||
self.enabled = True |
||||
self.blacklist_cache = to_blacklist |
||||
LOG.debug('The iptables filter was synchronized') |
||||
|
||||
@contextlib.contextmanager |
||||
def _temporary_chain(self, chain, main_chain): |
||||
"""Context manager to operate on a temporary chain.""" |
||||
# Clean up a bit to account for possible troubles on previous run |
||||
self._clean_up(chain) |
||||
self._iptables('-N', chain) |
||||
|
||||
yield |
||||
|
||||
# Swap chains |
||||
self._iptables('-I', 'INPUT', '-i', self.interface, '-p', 'udp', |
||||
'--dport', '67', '-j', chain) |
||||
self._iptables('-D', 'INPUT', '-i', self.interface, '-p', 'udp', |
||||
'--dport', '67', '-j', main_chain, |
||||
ignore=True) |
||||
self._iptables('-F', main_chain, ignore=True) |
||||
self._iptables('-X', main_chain, ignore=True) |
||||
self._iptables('-E', chain, main_chain) |
||||
|
||||
def _iptables(self, *args, **kwargs): |
||||
# NOTE(dtantsur): -w flag makes it wait for xtables lock |
||||
cmd = self.base_command + args |
||||
ignore = kwargs.pop('ignore', False) |
||||
LOG.debug('Running iptables %s', args) |
||||
kwargs['stderr'] = subprocess.STDOUT |
||||
try: |
||||
subprocess.check_output(cmd, **kwargs) |
||||
except subprocess.CalledProcessError as exc: |
||||
output = exc.output.replace('\n', '. ') |
||||
if ignore: |
||||
LOG.debug('Ignoring failed iptables %(args)s: %(output)s', |
||||
{'args': args, 'output': output}) |
||||
else: |
||||
LOG.error('iptables %(iptables)s failed: %(exc)s', |
||||
{'iptables': args, 'exc': output}) |
||||
raise |
||||
|
||||
def _clean_up(self, chain): |
||||
self._iptables('-D', 'INPUT', '-i', self.interface, '-p', 'udp', |
||||
'--dport', '67', '-j', chain, |
||||
ignore=True) |
||||
self._iptables('-F', chain, ignore=True) |
||||
self._iptables('-X', chain, ignore=True) |
||||
|
||||
def _disable_dhcp(self): |
||||
"""Disable DHCP completely.""" |
||||
if not self.enabled: |
||||
LOG.debug('DHCP is already disabled, not updating') |
||||
return |
||||
|
||||
LOG.debug('No nodes on introspection and node_not_found_hook is ' |
||||
'not set - disabling DHCP') |
||||
self.blacklist_cache = None |
||||
with self._temporary_chain(self.new_chain, self.chain): |
||||
# Blacklist everything |
||||
self._iptables('-A', self.new_chain, '-j', 'REJECT') |
||||
self.enabled = False |
||||
|
||||
|
||||
def _ib_mac_to_rmac_mapping(ports): |
||||
"""Update port InfiniBand MAC address 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 filter rules based on those MACs get applied during a |
||||
driver.update() call |
||||
|
||||
:param ports: list of ironic ports |
||||
:returns: Nothing. |
||||
""" |
||||
ethoib_interfaces = CONF.iptables.ethoib_interfaces |
||||
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('Interface %s is not Ethernet Over InfiniBand; ' |
||||
'Skipping ...', interface) |
||||
continue |
||||
for port in ports: |
||||
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: |
||||
port.address = match.group(1) |
||||
|
||||
|
||||
def _get_blacklist(ironic): |
||||
ports = [ |
||||
port.address for port in ironic.port.list( |
||||
limit=0, fields=['address', 'extra']) |
||||
if port.address not in node_cache.active_macs()] |
||||
_ib_mac_to_rmac_mapping(ports) |
||||
return ports |
@ -1,444 +0,0 @@
|
||||
# Copyright 2015 NEC Corporation |
||||
# All Rights Reserved. |
||||
# |
||||
# 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.common import ironic as ir_utils |
||||
from ironic_inspector import firewall |
||||
from ironic_inspector import introspection_state as istate |
||||
from ironic_inspector import node_cache |
||||
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(firewall.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): |
||||
CONF.set_override('manage_firewall', False, 'firewall') |
||||
firewall.update_filters() |
||||
self.assertEqual(0, mock_iptables.call_count) |
||||
|
||||
def test_init_args(self, mock_call, mock_get_client, mock_iptables): |
||||
rootwrap_path = '/some/fake/path' |
||||
CONF.set_override('rootwrap_config', rootwrap_path) |
||||
firewall.init() |
||||
init_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)] |
||||
|
||||
call_args_list = mock_iptables.call_args_list |
||||
|
||||
for (args, call) in zip(init_expected_args, call_args_list): |
||||
self.assertEqual(args, call[0]) |
||||
|
||||
expected = ('sudo', 'ironic-inspector-rootwrap', rootwrap_path, |
||||
'iptables', '-w') |
||||
self.assertEqual(expected, firewall.BASE_COMMAND) |
||||
|
||||
def test_init_args_old_iptables(self, mock_call, mock_get_client, |
||||
mock_iptables): |
||||
rootwrap_path = '/some/fake/path' |
||||
CONF.set_override('rootwrap_config', rootwrap_path) |
||||
mock_call.side_effect = firewall.subprocess.CalledProcessError(2, '') |
||||
firewall.init() |
||||
init_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)] |
||||
|
||||
call_args_list = mock_iptables.call_args_list |
||||
|
||||
for (args, call) in zip(init_expected_args, call_args_list): |
||||
self.assertEqual(args, call[0]) |
||||
|
||||
expected = ('sudo', 'ironic-inspector-rootwrap', rootwrap_path, |
||||
'iptables',) |
||||
self.assertEqual(expected, firewall.BASE_COMMAND) |
||||
|
||||
def test_init_kwargs(self, mock_call, mock_get_client, mock_iptables): |
||||
firewall.init() |
||||
init_expected_kwargs = [ |
||||
{'ignore': True}, |
||||
{'ignore': True}, |
||||
{'ignore': True}] |
||||
|
||||
call_args_list = mock_iptables.call_args_list |
||||
|
||||
for (kwargs, call) in zip(init_expected_kwargs, call_args_list): |
||||
self.assertEqual(kwargs, call[1]) |
||||
|
||||
def test_update_filters_args(self, mock_call, mock_get_client, |
||||
mock_iptables): |
||||
# Pretend that we have nodes on introspection |
||||
node_cache.add_node(self.node.uuid, state=istate.States.waiting, |
||||
bmc_address='1.2.3.4') |
||||
|
||||
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), |
||||
('-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) |
||||
] |
||||
|
||||
firewall.update_filters() |
||||
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_kwargs(self, mock_call, mock_get_client, |
||||
mock_iptables): |
||||
firewall.init() |
||||
|
||||
update_filters_expected_kwargs = [ |
||||
{'ignore': True}, |
||||
{'ignore': True}, |
||||
{'ignore': True}, |
||||
{}, |
||||
{'ignore': True}, |
||||
{'ignore': True}, |
||||
{'ignore': True}, |
||||
{}, |
||||
{}, |
||||
{}, |
||||
{'ignore': True}, |
||||
{'ignore': True}, |
||||
{'ignore': True} |
||||
] |
||||
|
||||
firewall.update_filters() |
||||
call_args_list = mock_iptables.call_args_list |
||||
|
||||
for (kwargs, call) in zip(update_filters_expected_kwargs, |
||||
call_args_list): |
||||
self.assertEqual(kwargs, call[1]) |
||||
|
||||
def test_update_filters_with_blacklist(self, mock_call, mock_get_client, |
||||
mock_iptables): |
||||
active_macs = ['11:22:33:44:55:66', '66:55:44:33:22:11'] |
||||
inactive_mac = ['AA:BB:CC:DD:EE:FF'] |
||||
self.macs = active_macs + inactive_mac |
||||
self.ports = [mock.Mock(address=m) for m in self.macs] |
||||
mock_get_client.port.list.return_value = self.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', |
||||
inactive_mac[0], '-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) |
||||
] |
||||
|
||||
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]) |
||||
|
||||
# check caching |
||||
|
||||
mock_iptables.reset_mock() |
||||
firewall.update_filters(mock_get_client) |
||||
self.assertFalse(mock_iptables.called) |
||||
|
||||
def test_update_filters_clean_cache_on_error(self, mock_call, |
||||
mock_get_client, |
||||
mock_iptables): |
||||
active_macs = ['11:22:33:44:55:66', '66:55:44:33:22:11'] |
||||
inactive_mac = ['AA:BB:CC:DD:EE:FF'] |
||||
self.macs = active_macs + inactive_mac |
||||
self.ports = [mock.Mock(address=m) for m in self.macs] |
||||
mock_get_client.port.list.return_value = self.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', firewall.NEW_CHAIN), |
||||
('-F', firewall.NEW_CHAIN), |
||||
('-X', firewall.NEW_CHAIN), |
||||
('-N', firewall.NEW_CHAIN), |
||||
# Blacklist |
||||
('-A', firewall.NEW_CHAIN, '-m', 'mac', '--mac-source', |
||||
inactive_mac[0], '-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) |
||||
] |
||||
|
||||
mock_iptables.side_effect = [None, None, RuntimeError()] |
||||
self.assertRaises(RuntimeError, firewall.update_filters, |
||||
mock_get_client) |
||||
|
||||
# check caching |
||||
|
||||
mock_iptables.reset_mock() |
||||
mock_iptables.side_effect = None |
||||
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_args_node_not_found_hook(self, mock_call, |
||||
mock_get_client, |
||||
mock_iptables): |
||||
# DHCP should be always opened if node_not_found hook is set |
||||
CONF.set_override('node_not_found_hook', 'enroll', 'processing') |
||||
|
||||
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), |
||||
('-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) |
||||
] |
||||
|
||||
firewall.update_filters() |
||||
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_args_no_introspection(self, mock_call, |
||||
mock_get_client, |
||||
mock_iptables): |
||||
firewall.init() |
||||
firewall.BLACKLIST_CACHE = ['foo'] |
||||
mock_get_client.return_value.port.list.return_value = [ |
||||
mock.Mock(address='foobar')] |
||||
|
||||
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), |
||||
('-A', firewall.NEW_CHAIN, '-j', 'REJECT'), |
||||
('-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) |
||||
] |
||||
|
||||
firewall.update_filters() |
||||
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]) |
||||
|
||||
self.assertIsNone(firewall.BLACKLIST_CACHE) |
||||
|
||||
# Check caching enabled flag |
||||
|
||||
mock_iptables.reset_mock() |
||||
firewall.update_filters() |
||||
self.assertFalse(mock_iptables.called) |
||||
|
||||
# Adding a node changes it back |
||||
|
||||
node_cache.add_node(self.node.uuid, state=istate.States.starting, |
||||
bmc_address='1.2.3.4') |
||||
mock_iptables.reset_mock() |
||||
firewall.update_filters() |
||||
|
||||
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]) |