Introducing a dnsmasq PXE filter driver
A PXE filter driver is introduced that works by configuring and controlling the dnsmasq service. Closes-Bug: 1693813 Related-Bug: 1665666 Change-Id: I63fe91ee4f9ac3021bcfd9a4a378af56af800fac
This commit is contained in:
parent
8104e33366
commit
8ddfacdf34
123
doc/source/admin/dnsmasq-pxe-filter.rst
Normal file
123
doc/source/admin/dnsmasq-pxe-filter.rst
Normal file
@ -0,0 +1,123 @@
|
||||
.. _dnsmasq_pxe_filter:
|
||||
|
||||
**dnsmasq** PXE filter
|
||||
======================
|
||||
|
||||
Often an inspection PXE DHCP stack is implemented by the **dnsmasq** service.
|
||||
This PXE filter implementation relies on directly configuring the **dnsmasq**
|
||||
DHCP service to provide a caching PXE-traffic filter of node MAC addresses.
|
||||
|
||||
How it works
|
||||
------------
|
||||
|
||||
Using a configuration *file per MAC address* allows one to implement a
|
||||
filtering mechanism based on the ``ignore`` directive::
|
||||
|
||||
$ cat /etc/dnsmasq.d/de-ad-be-ef-de-ad
|
||||
de:ad:be:ef:de:ad,ignore
|
||||
$
|
||||
|
||||
The filename is used to keep track of all MAC addresses in the cache, avoiding
|
||||
file parsing. The content of the file determines the MAC address access policy.
|
||||
|
||||
Thanks to the ``inotify`` facility, **dnsmasq** is notified instantly once a
|
||||
new file is *created* or an existing file is *modified* in the
|
||||
DHCP hosts directory. Thus, to white-list a MAC address, one has to
|
||||
remove the ``ignore`` directive::
|
||||
|
||||
$ cat /etc/dnsmasq.d/de-ad-be-ef-de-ad
|
||||
de:ad:be:ef:de:ad
|
||||
$
|
||||
|
||||
The hosts directory content establishes a *cached* MAC addresses filter that is
|
||||
kept synchronized with the **ironic** port list.
|
||||
|
||||
.. note::
|
||||
|
||||
The **dnsmasq** inotify facility implementation doesn't react on a file being
|
||||
removed or truncated.
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
To enable the **dnsmasq** PXE filter, update the PXE filter driver name::
|
||||
|
||||
[pxe_filter]
|
||||
driver = dnsmasq
|
||||
|
||||
The DHCP hosts directory can be specified to override the default
|
||||
``/var/lib/ironic-inspector/dhcp-hostsdir``::
|
||||
|
||||
[dnsmasq_pxe_filter]
|
||||
dhcp_hostsdir = /etc/ironic-inspector/dhcp-hostsdir
|
||||
|
||||
The filter design relies on the hosts directory being in exclusive
|
||||
**inspector** control. The hosts directory should be considered a *private
|
||||
cache* directory of **inspector** that **dnsmasq** polls configuration updates
|
||||
from, through the ``inotify`` facility. The directory has to be writable by
|
||||
**inspector** and readable by **dnsmasq**.
|
||||
|
||||
One can also override the default start and stop commands to control the
|
||||
**dnsmasq** service::
|
||||
|
||||
[dnsmasq_pxe_filter]
|
||||
dnsmasq_start_command = dnsmasq --conf-file /etc/ironic-inspector/dnsmasq.conf
|
||||
dnsmasq_stop_command = kill $(cat /var/run/dnsmasq.pid)
|
||||
|
||||
.. note::
|
||||
|
||||
It is also possible to set an empty/none string or to use shell expansion in
|
||||
place of the commands. An empty start command means the **dnsmasq** service
|
||||
won't be started upon the filter initialization, an empty stop command means
|
||||
the service won't be stopped upon an (error) exit.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
These commands are executed through the ``rootwrap`` facility, so overriding
|
||||
may require a filter file to be created in the ``rootwrap.d`` directory. A
|
||||
sample configuration for **devstack** use might be:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
sudo cat > "$IRONIC_INSPECTOR_CONF_DIR/rootwrap.d/ironic-inspector-dnsmasq-systemctl.filters" <<EOF
|
||||
[Filters]
|
||||
# ironic_inspector/pxe_filter/dnsmasq.py
|
||||
systemctl: CommandFilter, systemctl, root, restart, devstack@ironic-inspector-dnsmasq
|
||||
systemctl: CommandFilter, systemctl, root, stop, devstack@ironic-inspector-dnsmasq
|
||||
EOF
|
||||
|
||||
Supported dnsmasq versions
|
||||
--------------------------
|
||||
|
||||
This filter driver has been checked by **inspector** CI with **dnsmasq**
|
||||
versions `>=2.76`. The ``inotify`` facility was introduced_ to **dnsmasq** in
|
||||
the version `2.73`.
|
||||
|
||||
.. _introduced: http://www.thekelleys.org.uk/dnsmasq/CHANGELOG
|
||||
|
||||
Caveats
|
||||
-------
|
||||
|
||||
The initial synchronization will put some load on the **dnsmasq** service
|
||||
starting based on the amount of ports **ironic** keeps. This can take up to a
|
||||
minute of full CPU load for huge amounts of MACs (tens of thousands).
|
||||
Subsequent filter synchronizations will only cause the **dnsmasq** to parse
|
||||
the modified files. Typically those are the bare metal nodes being added or
|
||||
phased out from the compute service, meaning dozens of file updates per sync
|
||||
call.
|
||||
|
||||
The **inspector** takes over the control of the DHCP hosts directory to
|
||||
implement its filter cache. Files are generated dynamically so should not be
|
||||
edited by hand. To minimize the interference between the deployment and
|
||||
introspection, **inspector** has to start the **dnsmasq** service only after
|
||||
the initial synchronization. Conversely, the **dnsmasq** service is stopped
|
||||
upon (unexpected) **inspector** exit.
|
||||
|
||||
To avoid accumulating stale DHCP host files over time, the driver cleans up
|
||||
the DHCP hosts directory during the ``init_filter`` call.
|
||||
|
||||
Although the filter driver tries its best to always stop the **dnsmasq**
|
||||
service, it is recommended that the operator configures the **dnsmasq**
|
||||
service in such a way that it terminates upon **inspector** (unexpected) exit
|
||||
to prevent a stale blacklist from being used by the **dnsmasq** service.
|
@ -8,3 +8,11 @@ How to upgrade Ironic Inspector
|
||||
:maxdepth: 2
|
||||
|
||||
upgrade
|
||||
|
||||
Dnsmasq PXE filter driver
|
||||
-------------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
dnsmasq-pxe-filter
|
||||
|
@ -76,6 +76,9 @@ Fill in these minimum configuration values:
|
||||
(defaults to ``br-ctlplane`` which is a sane default for **tripleo**-based
|
||||
installations but is unlikely to work for other cases).
|
||||
|
||||
* if you wish to use the ``dnsmasq`` PXE/DHCP filter driver rather than the
|
||||
default ``iptables`` driver, see the :ref:`dnsmasq_pxe_filter` description.
|
||||
|
||||
See comments inside `example.conf
|
||||
<https://github.com/openstack/ironic-inspector/blob/master/example.conf>`_
|
||||
for other possible configuration options.
|
||||
|
19
example.conf
19
example.conf
@ -340,6 +340,25 @@
|
||||
#enroll_node_driver = fake
|
||||
|
||||
|
||||
[dnsmasq_pxe_filter]
|
||||
|
||||
#
|
||||
# From ironic_inspector
|
||||
#
|
||||
|
||||
# The MAC address cache directory, exposed to dnsmasq.This directory
|
||||
# is expected to be in exclusive control of the driver. (string value)
|
||||
#dhcp_hostsdir = /var/lib/ironic-inspector/dhcp-hostsdir
|
||||
|
||||
# A (shell) command line to start the dnsmasq service upon filter
|
||||
# initialization. Default: don't start. (string value)
|
||||
#dnsmasq_start_command =
|
||||
|
||||
# A (shell) command line to stop the dnsmasq service upon inspector
|
||||
# (error) exit. Default: don't stop. (string value)
|
||||
#dnsmasq_stop_command =
|
||||
|
||||
|
||||
[iptables]
|
||||
|
||||
#
|
||||
|
@ -209,11 +209,26 @@ PXE_FILTER_OPTS = [
|
||||
'update of the filter.')),
|
||||
]
|
||||
|
||||
DNSMASQ_PXE_FILTER_OPTS = [
|
||||
cfg.StrOpt('dhcp_hostsdir',
|
||||
default='/var/lib/ironic-inspector/dhcp-hostsdir',
|
||||
help=_('The MAC address cache directory, exposed to dnsmasq.'
|
||||
'This directory is expected to be in exclusive control '
|
||||
'of the driver.')),
|
||||
cfg.StrOpt('dnsmasq_start_command', default='',
|
||||
help=_('A (shell) command line to start the dnsmasq service '
|
||||
'upon filter initialization. Default: don\'t start.')),
|
||||
cfg.StrOpt('dnsmasq_stop_command', default='',
|
||||
help=_('A (shell) command line to stop the dnsmasq service '
|
||||
'upon inspector (error) exit. Default: don\'t stop.')),
|
||||
]
|
||||
|
||||
|
||||
cfg.CONF.register_opts(SERVICE_OPTS)
|
||||
cfg.CONF.register_opts(IPTABLES_OPTS, group='iptables')
|
||||
cfg.CONF.register_opts(PROCESSING_OPTS, group='processing')
|
||||
cfg.CONF.register_opts(PXE_FILTER_OPTS, 'pxe_filter')
|
||||
cfg.CONF.register_opts(DNSMASQ_PXE_FILTER_OPTS, group='dnsmasq_pxe_filter')
|
||||
|
||||
|
||||
def list_opts():
|
||||
@ -222,6 +237,7 @@ def list_opts():
|
||||
('iptables', IPTABLES_OPTS),
|
||||
('processing', PROCESSING_OPTS),
|
||||
('pxe_filter', PXE_FILTER_OPTS),
|
||||
('dnsmasq_pxe_filter', DNSMASQ_PXE_FILTER_OPTS),
|
||||
]
|
||||
|
||||
|
||||
|
176
ironic_inspector/pxe_filter/dnsmasq.py
Normal file
176
ironic_inspector/pxe_filter/dnsmasq.py
Normal file
@ -0,0 +1,176 @@
|
||||
# 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.
|
||||
|
||||
# NOTE(milan) the filter design relies on the hostdir[1] being in exclusive
|
||||
# inspector control. The hostdir should be considered a private cache directory
|
||||
# of inspector that dnsmasq has read access to and polls updates from, through
|
||||
# the inotify facility.
|
||||
#
|
||||
# [1] see the --dhcp-hostsdir option description in
|
||||
# http://www.thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from oslo_concurrency import processutils
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
from oslo_utils import timeutils
|
||||
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector import node_cache
|
||||
from ironic_inspector.pxe_filter import base as pxe_filter
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
_ROOTWRAP_COMMAND = 'sudo ironic-inspector-rootwrap {rootwrap_config!s}'
|
||||
_MACBL_LEN = len('ff:ff:ff:ff:ff:ff,ignore\n')
|
||||
|
||||
|
||||
class DnsmasqFilter(pxe_filter.BaseFilter):
|
||||
"""The dnsmasq PXE filter driver.
|
||||
|
||||
A pxe filter driver implementation that controls access to dnsmasq
|
||||
through amending its configuration.
|
||||
"""
|
||||
|
||||
def reset(self):
|
||||
"""Stop dnsmasq and upcall reset."""
|
||||
_execute(CONF.dnsmasq_pxe_filter.dnsmasq_stop_command,
|
||||
ignore_errors=True)
|
||||
super(DnsmasqFilter, self).reset()
|
||||
|
||||
def _sync(self, ironic):
|
||||
"""Sync the inspector, ironic and dnsmasq state. Locked.
|
||||
|
||||
:raises: IOError, OSError.
|
||||
:returns: None.
|
||||
"""
|
||||
LOG.debug('Syncing the driver')
|
||||
timestamp_start = timeutils.utcnow()
|
||||
active_macs = node_cache.active_macs()
|
||||
ironic_macs = set(port.address for port in
|
||||
ironic.port.list(limit=0, fields=['address']))
|
||||
blacklist_macs = _get_blacklist()
|
||||
# NOTE(milan) whitelist MACs of ports not kept in ironic anymore
|
||||
# also whitelist active MACs that are still blacklisted in the
|
||||
# dnsmasq configuration but have just been asked to be introspected
|
||||
for mac in ((blacklist_macs - ironic_macs) |
|
||||
(blacklist_macs & active_macs)):
|
||||
_whitelist_mac(mac)
|
||||
# blacklist new ports that aren't being inspected
|
||||
for mac in ironic_macs - (blacklist_macs | active_macs):
|
||||
_blacklist_mac(mac)
|
||||
timestamp_end = timeutils.utcnow()
|
||||
LOG.debug('The dnsmasq PXE filter was synchronized (took %s)',
|
||||
timestamp_end - timestamp_start)
|
||||
|
||||
@pxe_filter.locked_driver_event(pxe_filter.Events.sync)
|
||||
def sync(self, ironic):
|
||||
"""Sync dnsmasq configuration with current Ironic&Inspector state.
|
||||
|
||||
Polls all ironic ports. Those being inspected, the active ones, are
|
||||
whitelisted while the rest are blacklisted in the dnsmasq
|
||||
configuration.
|
||||
|
||||
:param ironic: an ironic client instance.
|
||||
:raises: OSError, IOError.
|
||||
:returns: None.
|
||||
"""
|
||||
self._sync(ironic)
|
||||
|
||||
@pxe_filter.locked_driver_event(pxe_filter.Events.initialize)
|
||||
def init_filter(self):
|
||||
"""Performs an initial sync with ironic and starts dnsmasq.
|
||||
|
||||
The initial _sync() call reduces the chances dnsmasq might lose
|
||||
some inotify blacklist events by prefetching the blacklist before
|
||||
the dnsmasq is started.
|
||||
|
||||
:raises: OSError, IOError.
|
||||
:returns: None.
|
||||
"""
|
||||
_purge_dhcp_hostsdir()
|
||||
ironic = ir_utils.get_client()
|
||||
self._sync(ironic)
|
||||
_execute(CONF.dnsmasq_pxe_filter.dnsmasq_start_command)
|
||||
LOG.info('The dnsmasq PXE filter was initialized')
|
||||
|
||||
|
||||
def _purge_dhcp_hostsdir():
|
||||
"""Remove all the DHCP hosts files.
|
||||
|
||||
:raises: FileNotFoundError in case the dhcp_hostsdir is invalid.
|
||||
IOError in case of non-writable file or a record not being a file.
|
||||
:returns: None.
|
||||
"""
|
||||
dhcp_hostsdir = CONF.dnsmasq_pxe_filter.dhcp_hostsdir
|
||||
LOG.debug('Purging %s', dhcp_hostsdir)
|
||||
for mac in os.listdir(dhcp_hostsdir):
|
||||
path = os.path.join(dhcp_hostsdir, mac)
|
||||
# NOTE(milan) relying on a failure here aborting the init_filter() call
|
||||
os.remove(path)
|
||||
LOG.debug('Removed %s', path)
|
||||
|
||||
|
||||
def _get_blacklist():
|
||||
"""Get addresses currently blacklisted in dnsmasq.
|
||||
|
||||
:raises: FileNotFoundError in case the dhcp_hostsdir is invalid.
|
||||
:returns: a set of MACs currently blacklisted in dnsmasq.
|
||||
"""
|
||||
hostsdir = CONF.dnsmasq_pxe_filter.dhcp_hostsdir
|
||||
# whitelisted MACs lack the ,ignore directive
|
||||
return set(address for address in os.listdir(hostsdir)
|
||||
if os.stat(os.path.join(hostsdir, address)).st_size ==
|
||||
_MACBL_LEN)
|
||||
|
||||
|
||||
def _blacklist_mac(mac):
|
||||
"""Creates a dhcp_hostsdir ignore record for the MAC.
|
||||
|
||||
:raises: FileNotFoundError in case the dhcp_hostsdir is invalid,
|
||||
IOError in case the dhcp host MAC file isn't writable.
|
||||
:returns: None.
|
||||
"""
|
||||
path = os.path.join(CONF.dnsmasq_pxe_filter.dhcp_hostsdir, mac)
|
||||
# NOTE(milan) line-buffering enforced to ensure dnsmasq record update
|
||||
# through inotify, which reacts on f.close()
|
||||
with open(path, 'w', 1) as f:
|
||||
f.write('%s,ignore\n' % mac)
|
||||
LOG.debug('Blacklisted %s', mac)
|
||||
|
||||
|
||||
def _whitelist_mac(mac):
|
||||
"""Un-ignores the dhcp_hostsdir record for the MAC.
|
||||
|
||||
:raises: FileNotFoundError in case the dhcp_hostsdir is invalid,
|
||||
IOError in case the dhcp host MAC file isn't writable.
|
||||
:returns: None.
|
||||
"""
|
||||
path = os.path.join(CONF.dnsmasq_pxe_filter.dhcp_hostsdir, mac)
|
||||
with open(path, 'w', 1) as f:
|
||||
# remove the ,ignore directive
|
||||
f.write('%s\n' % mac)
|
||||
LOG.debug('Whitelisted %s', mac)
|
||||
|
||||
|
||||
def _execute(cmd=None, ignore_errors=False):
|
||||
# e.g: '/bin/kill $(cat /var/run/dnsmasq.pid)'
|
||||
if not cmd:
|
||||
return
|
||||
|
||||
helper = _ROOTWRAP_COMMAND.format(rootwrap_config=CONF.rootwrap_config)
|
||||
processutils.execute(cmd, run_as_root=True, root_helper=helper, shell=True,
|
||||
check_exit_code=not ignore_errors)
|
236
ironic_inspector/test/unit/test_dnsmasq_pxe_filter.py
Normal file
236
ironic_inspector/test/unit/test_dnsmasq_pxe_filter.py
Normal file
@ -0,0 +1,236 @@
|
||||
# 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 datetime
|
||||
import os
|
||||
|
||||
import fixtures
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
import six
|
||||
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector import node_cache
|
||||
from ironic_inspector.pxe_filter import dnsmasq
|
||||
from ironic_inspector.test import base as test_base
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class DnsmasqTestBase(test_base.BaseTest):
|
||||
def setUp(self):
|
||||
super(DnsmasqTestBase, self).setUp()
|
||||
self.driver = dnsmasq.DnsmasqFilter()
|
||||
|
||||
|
||||
class TestDnsmasqDriverAPI(DnsmasqTestBase):
|
||||
def setUp(self):
|
||||
super(TestDnsmasqDriverAPI, self).setUp()
|
||||
self.mock__execute = self.useFixture(
|
||||
fixtures.MockPatchObject(dnsmasq, '_execute')).mock
|
||||
self.driver._sync = mock.Mock()
|
||||
self.driver._tear_down = mock.Mock()
|
||||
self.mock__purge_dhcp_hostsdir = self.useFixture(
|
||||
fixtures.MockPatchObject(dnsmasq, '_purge_dhcp_hostsdir')).mock
|
||||
self.mock_ironic = mock.Mock()
|
||||
get_client_mock = self.useFixture(
|
||||
fixtures.MockPatchObject(ir_utils, 'get_client')).mock
|
||||
get_client_mock.return_value = self.mock_ironic
|
||||
self.start_command = '/far/boo buzz -V --ack 42'
|
||||
CONF.set_override('dnsmasq_start_command', self.start_command,
|
||||
'dnsmasq_pxe_filter')
|
||||
self.stop_command = '/what/ever'
|
||||
CONF.set_override('dnsmasq_stop_command', self.stop_command,
|
||||
'dnsmasq_pxe_filter')
|
||||
|
||||
def test_init_filter(self):
|
||||
self.driver.init_filter()
|
||||
|
||||
self.mock__purge_dhcp_hostsdir.assert_called_once_with()
|
||||
self.driver._sync.assert_called_once_with(self.mock_ironic)
|
||||
self.mock__execute.assert_called_once_with(self.start_command)
|
||||
|
||||
def test_sync(self):
|
||||
self.driver.init_filter()
|
||||
# NOTE(milan) init_filter performs an initial sync
|
||||
self.driver._sync.reset_mock()
|
||||
self.driver.sync(self.mock_ironic)
|
||||
|
||||
self.driver._sync.assert_called_once_with(self.mock_ironic)
|
||||
|
||||
def test_tear_down_filter(self):
|
||||
mock_reset = self.useFixture(
|
||||
fixtures.MockPatchObject(self.driver, 'reset')).mock
|
||||
self.driver.init_filter()
|
||||
self.driver.tear_down_filter()
|
||||
|
||||
mock_reset.assert_called_once_with()
|
||||
|
||||
def test_reset(self):
|
||||
self.driver.init_filter()
|
||||
# NOTE(milan) init_filter calls _base_cmd
|
||||
self.mock__execute.reset_mock()
|
||||
self.driver.reset()
|
||||
|
||||
self.mock__execute.assert_called_once_with(
|
||||
self.stop_command, ignore_errors=True)
|
||||
|
||||
|
||||
class TestMACHandlers(test_base.BaseTest):
|
||||
def setUp(self):
|
||||
super(TestMACHandlers, self).setUp()
|
||||
self.mock_listdir = self.useFixture(
|
||||
fixtures.MockPatchObject(os, 'listdir')).mock
|
||||
self.mock_stat = self.useFixture(
|
||||
fixtures.MockPatchObject(os, 'stat')).mock
|
||||
self.mock_remove = self.useFixture(
|
||||
fixtures.MockPatchObject(os, 'remove')).mock
|
||||
self.mac = 'ff:ff:ff:ff:ff:ff'
|
||||
self.dhcp_hostsdir = '/far'
|
||||
CONF.set_override('dhcp_hostsdir', self.dhcp_hostsdir,
|
||||
'dnsmasq_pxe_filter')
|
||||
self.mock_join = self.useFixture(
|
||||
fixtures.MockPatchObject(os.path, 'join')).mock
|
||||
self.mock_join.return_value = "%s/%s" % (self.dhcp_hostsdir, self.mac)
|
||||
|
||||
def test__whitelist_mac(self):
|
||||
with mock.patch.object(six.moves.builtins, 'open',
|
||||
new=mock.mock_open()) as mock_open:
|
||||
dnsmasq._whitelist_mac(self.mac)
|
||||
|
||||
mock_fd = mock_open.return_value
|
||||
self.mock_join.assert_called_once_with(self.dhcp_hostsdir, self.mac)
|
||||
mock_open.assert_called_once_with(self.mock_join.return_value, 'w', 1)
|
||||
mock_fd.write.assert_called_once_with('%s\n' % self.mac)
|
||||
|
||||
def test__blacklist_mac(self):
|
||||
with mock.patch.object(six.moves.builtins, 'open',
|
||||
new=mock.mock_open()) as mock_open:
|
||||
dnsmasq._blacklist_mac(self.mac)
|
||||
|
||||
mock_fd = mock_open.return_value
|
||||
self.mock_join.assert_called_once_with(self.dhcp_hostsdir, self.mac)
|
||||
mock_open.assert_called_once_with(self.mock_join.return_value, 'w', 1)
|
||||
mock_fd.write.assert_called_once_with('%s,ignore\n' % self.mac)
|
||||
|
||||
def test__get_blacklist(self):
|
||||
self.mock_listdir.return_value = [self.mac]
|
||||
self.mock_stat.return_value.st_size = len('%s,ignore\n' % self.mac)
|
||||
ret = dnsmasq._get_blacklist()
|
||||
|
||||
self.assertEqual({self.mac}, ret)
|
||||
self.mock_listdir.assert_called_once_with(self.dhcp_hostsdir)
|
||||
self.mock_join.assert_called_once_with(self.dhcp_hostsdir, self.mac)
|
||||
self.mock_stat.assert_called_once_with(self.mock_join.return_value)
|
||||
|
||||
def test__get_no_blacklist(self):
|
||||
self.mock_listdir.return_value = [self.mac]
|
||||
self.mock_stat.return_value.st_size = len('%s\n' % self.mac)
|
||||
ret = dnsmasq._get_blacklist()
|
||||
|
||||
self.assertEqual(set(), ret)
|
||||
self.mock_listdir.assert_called_once_with(self.dhcp_hostsdir)
|
||||
self.mock_join.assert_called_once_with(self.dhcp_hostsdir, self.mac)
|
||||
self.mock_stat.assert_called_once_with(self.mock_join.return_value)
|
||||
|
||||
def test__purge_dhcp_hostsdir(self):
|
||||
self.mock_listdir.return_value = [self.mac]
|
||||
dnsmasq._purge_dhcp_hostsdir()
|
||||
|
||||
self.mock_listdir.assert_called_once_with(self.dhcp_hostsdir)
|
||||
self.mock_join.assert_called_once_with(self.dhcp_hostsdir, self.mac)
|
||||
self.mock_remove.assert_called_once_with('%s/%s' % (self.dhcp_hostsdir,
|
||||
self.mac))
|
||||
|
||||
|
||||
class TestSync(DnsmasqTestBase):
|
||||
def setUp(self):
|
||||
super(TestSync, self).setUp()
|
||||
self.mock__get_blacklist = self.useFixture(
|
||||
fixtures.MockPatchObject(dnsmasq, '_get_blacklist')).mock
|
||||
self.mock__whitelist_mac = self.useFixture(
|
||||
fixtures.MockPatchObject(dnsmasq, '_whitelist_mac')).mock
|
||||
self.mock__blacklist_mac = self.useFixture(
|
||||
fixtures.MockPatchObject(dnsmasq, '_blacklist_mac')).mock
|
||||
self.mock_ironic = mock.Mock()
|
||||
self.mock_utcnow = self.useFixture(
|
||||
fixtures.MockPatchObject(dnsmasq.timeutils, 'utcnow')).mock
|
||||
self.timestamp_start = datetime.datetime.utcnow()
|
||||
self.timestamp_end = (self.timestamp_start +
|
||||
datetime.timedelta(seconds=42))
|
||||
self.mock_utcnow.side_effect = [self.timestamp_start,
|
||||
self.timestamp_end]
|
||||
self.mock_log = self.useFixture(
|
||||
fixtures.MockPatchObject(dnsmasq, 'LOG')).mock
|
||||
get_client_mock = self.useFixture(
|
||||
fixtures.MockPatchObject(ir_utils, 'get_client')).mock
|
||||
get_client_mock.return_value = self.mock_ironic
|
||||
self.mock_active_macs = self.useFixture(
|
||||
fixtures.MockPatchObject(node_cache, 'active_macs')).mock
|
||||
self.ironic_macs = {'new_mac', 'active_mac'}
|
||||
self.active_macs = {'active_mac'}
|
||||
self.blacklist_macs = {'gone_mac', 'active_mac'}
|
||||
self.mock__get_blacklist.return_value = self.blacklist_macs
|
||||
self.mock_ironic.port.list.return_value = [
|
||||
mock.Mock(address=address) for address in self.ironic_macs]
|
||||
self.mock_active_macs.return_value = self.active_macs
|
||||
|
||||
def test__sync(self):
|
||||
self.driver._sync(self.mock_ironic)
|
||||
|
||||
self.mock__whitelist_mac.assert_has_calls([mock.call('active_mac'),
|
||||
mock.call('gone_mac')],
|
||||
any_order=True)
|
||||
self.mock__blacklist_mac.assert_has_calls([mock.call('new_mac')],
|
||||
any_order=True)
|
||||
self.mock_ironic.port.list.assert_called_once_with(limit=0,
|
||||
fields=['address'])
|
||||
self.mock_active_macs.assert_called_once_with()
|
||||
self.mock__get_blacklist.assert_called_once_with()
|
||||
self.mock_log.debug.assert_has_calls([
|
||||
mock.call('Syncing the driver'),
|
||||
mock.call('The dnsmasq PXE filter was synchronized (took %s)',
|
||||
self.timestamp_end - self.timestamp_start)
|
||||
])
|
||||
|
||||
|
||||
class Test_Execute(test_base.BaseTest):
|
||||
def setUp(self):
|
||||
super(Test_Execute, self).setUp()
|
||||
self.mock_execute = self.useFixture(
|
||||
fixtures.MockPatchObject(dnsmasq.processutils, 'execute')
|
||||
).mock
|
||||
CONF.set_override('rootwrap_config', '/path/to/rootwrap.conf')
|
||||
self.rootwrap_cmd = dnsmasq._ROOTWRAP_COMMAND.format(
|
||||
rootwrap_config=CONF.rootwrap_config)
|
||||
self.useFixture(fixtures.MonkeyPatch(
|
||||
'ironic_inspector.pxe_filter.dnsmasq._ROOTWRAP_COMMAND',
|
||||
self.rootwrap_cmd))
|
||||
self.command = 'foobar baz'
|
||||
|
||||
def test__execute(self):
|
||||
dnsmasq._execute(self.command)
|
||||
self.mock_execute.assert_called_once_with(
|
||||
self.command, run_as_root=True, shell=True,
|
||||
check_exit_code=True, root_helper=self.rootwrap_cmd)
|
||||
|
||||
def test__execute_ignoring_errors(self):
|
||||
dnsmasq._execute(self.command, ignore_errors=True)
|
||||
self.mock_execute.assert_called_once_with(
|
||||
self.command, run_as_root=True, shell=True,
|
||||
check_exit_code=False, root_helper=self.rootwrap_cmd)
|
||||
|
||||
def test__execute_empty(self):
|
||||
dnsmasq._execute()
|
||||
|
||||
self.mock_execute.assert_not_called()
|
@ -0,0 +1,6 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Introduces the **dnsmasq** PXE filter driver. This driver takes advantage of
|
||||
the ``inotify`` facility to reconfigure the **dnsmasq** service in real time
|
||||
to implement a caching black-/white-list of port MAC addresses.
|
@ -58,6 +58,7 @@ ironic_inspector.rules.actions =
|
||||
set-capability = ironic_inspector.plugins.rules:SetCapabilityAction
|
||||
extend-attribute = ironic_inspector.plugins.rules:ExtendAttributeAction
|
||||
ironic_inspector.pxe_filter =
|
||||
dnsmasq = ironic_inspector.pxe_filter.dnsmasq:DnsmasqFilter
|
||||
iptables = ironic_inspector.pxe_filter.iptables:IptablesFilter
|
||||
noop = ironic_inspector.pxe_filter.base:NoopFilter
|
||||
oslo.config.opts =
|
||||
|
Loading…
Reference in New Issue
Block a user