PXE boot filtering drivers
Introduce a driver concept for PXE filtering Change-Id: I73297771c4118f368b80a5f1021a0d5c3fc8b96e Closes-Bug: 1665666
This commit is contained in:
parent
c172b2eaf0
commit
e02bc755a6
@ -316,3 +316,52 @@ the database::
|
||||
.. _Create a Migration Script: http://alembic.zzzcomputing.com/en/latest/tutorial.html#create-a-migration-script
|
||||
.. _ironic_inspector.db: http://docs.openstack.org/developer/ironic-inspector/api/ironic_inspector.db.html
|
||||
.. _What does Autogenerate Detect (and what does it not detect?): http://alembic.zzzcomputing.com/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect
|
||||
|
||||
Implementing PXE Filter Drivers
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Background
|
||||
----------
|
||||
|
||||
**inspector** in-band introspection PXE-boots the Ironic Python Agent "live"
|
||||
image, to inspect the baremetal server. **ironic** also PXE-boots IPA to
|
||||
perform tasks on a node, such as deploying an image. **ironic** uses
|
||||
**neutron** to provide DHCP, however **neutron** does not provide DHCP for
|
||||
unknown MAC addresses so **inspector** has to use its own DHCP/TFTP stack for
|
||||
discovery and inspection.
|
||||
|
||||
When **ironic** and **inspector** are operating in the same L2 network, there
|
||||
is a potential for the two DHCPs to race, which could result in a node being
|
||||
deployed by **ironic** being PXE booted by **inspector**.
|
||||
|
||||
To prevent DHCP races between the **inspector** DHCP and **ironic** DHCP,
|
||||
**inspector** has to be able to filter which nodes can get a DHCP lease from
|
||||
the **inspector** DHCP server. These filters can then be used to prevent
|
||||
node's enrolled in **ironic** inventory from being PXE-booted unless they are
|
||||
explicitly moved into the ``inspected`` state.
|
||||
|
||||
Filter Interface
|
||||
----------------
|
||||
|
||||
.. py:currentmodule:: ironic_inspector.pxe_filter.interface
|
||||
|
||||
The contract between **inspector** and a PXE filter driver is described in the
|
||||
:class:`FilterDriver` interface. The methods a driver has to implement are:
|
||||
|
||||
* :meth:`~FilterDriver.init_filter` called on the service start to initialize
|
||||
internal driver state
|
||||
|
||||
* :meth:`~FilterDriver.sync` called both periodically and when a node starts or
|
||||
finishes introspection to white or blacklist its ports MAC addresses in the
|
||||
driver
|
||||
|
||||
* :meth:`~FilterDriver.tear_down_filter` called on service exit to reset the
|
||||
internal driver state
|
||||
|
||||
.. py:currentmodule:: ironic_inspector.pxe_filter.base
|
||||
|
||||
The driver-specific configuration is suggested to be parsed during
|
||||
instantiation. There's also a convenience generic interface implementation
|
||||
:class:`BaseFilter` that provides base locking and initialization
|
||||
implementation. If required, a driver can opt-out from the periodic
|
||||
synchronization by overriding the :meth:`~BaseFilter.get_periodic_sync_task`.
|
||||
|
15
example.conf
15
example.conf
@ -795,6 +795,21 @@
|
||||
#power_off = true
|
||||
|
||||
|
||||
[pxe_filter]
|
||||
|
||||
#
|
||||
# From ironic_inspector
|
||||
#
|
||||
|
||||
# PXE boot filter driver to use, such as iptables (string value)
|
||||
#driver = noop
|
||||
|
||||
# Amount of time in seconds, after which repeat periodic update of the
|
||||
# filter. (integer value)
|
||||
# Minimum value: 0
|
||||
#sync_period = 15
|
||||
|
||||
|
||||
[swift]
|
||||
|
||||
#
|
||||
|
@ -199,10 +199,18 @@ SERVICE_OPTS = [
|
||||
help=_('Limit the number of elements an API list-call returns'))
|
||||
]
|
||||
|
||||
PXE_FILTER_OPTS = [
|
||||
cfg.StrOpt('driver', default='noop',
|
||||
help=_('PXE boot filter driver to use, such as iptables')),
|
||||
cfg.IntOpt('sync_period', default=15, min=0,
|
||||
help=_('Amount of time in seconds, after which repeat periodic '
|
||||
'update of the filter.')),
|
||||
]
|
||||
|
||||
cfg.CONF.register_opts(SERVICE_OPTS)
|
||||
cfg.CONF.register_opts(FIREWALL_OPTS, group='firewall')
|
||||
cfg.CONF.register_opts(PROCESSING_OPTS, group='processing')
|
||||
cfg.CONF.register_opts(PXE_FILTER_OPTS, 'pxe_filter')
|
||||
|
||||
|
||||
def list_opts():
|
||||
@ -210,6 +218,7 @@ def list_opts():
|
||||
('', SERVICE_OPTS),
|
||||
('firewall', FIREWALL_OPTS),
|
||||
('processing', PROCESSING_OPTS),
|
||||
('pxe_filter', PXE_FILTER_OPTS),
|
||||
]
|
||||
|
||||
|
||||
|
0
ironic_inspector/pxe_filter/__init__.py
Normal file
0
ironic_inspector/pxe_filter/__init__.py
Normal file
224
ironic_inspector/pxe_filter/base.py
Normal file
224
ironic_inspector/pxe_filter/base.py
Normal file
@ -0,0 +1,224 @@
|
||||
# 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.
|
||||
|
||||
"""Base code for PXE boot filtering."""
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
|
||||
from automaton import exceptions as automaton_errors
|
||||
from automaton import machines
|
||||
from eventlet import semaphore
|
||||
from futurist import periodics
|
||||
from oslo_concurrency import lockutils
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
import stevedore
|
||||
|
||||
from ironic_inspector.common.i18n import _
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector.pxe_filter import interface
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
_STEVEDORE_DRIVER_NAMESPACE = 'ironic_inspector.pxe_filter'
|
||||
|
||||
|
||||
class InvalidFilterDriverState(RuntimeError):
|
||||
"""The fsm of the filter driver raised an error."""
|
||||
|
||||
|
||||
class States(object):
|
||||
"""PXE filter driver states."""
|
||||
uninitialized = 'uninitialized'
|
||||
initialized = 'initialized'
|
||||
|
||||
|
||||
class Events(object):
|
||||
"""PXE filter driver transitions."""
|
||||
initialize = 'initialize'
|
||||
sync = 'sync'
|
||||
reset = 'reset'
|
||||
|
||||
|
||||
# a reset is always possible
|
||||
State_space = [
|
||||
{
|
||||
'name': States.uninitialized,
|
||||
'next_states': {
|
||||
Events.initialize: States.initialized,
|
||||
Events.reset: States.uninitialized,
|
||||
},
|
||||
},
|
||||
{
|
||||
'name': States.initialized,
|
||||
'next_states': {
|
||||
Events.sync: States.initialized,
|
||||
Events.reset: States.uninitialized,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def locked_driver_event(event):
|
||||
"""Call driver method having processed the fsm event."""
|
||||
def outer(method):
|
||||
@functools.wraps(method)
|
||||
def inner(self, *args, **kwargs):
|
||||
with self.lock, self.fsm_reset_on_error() as fsm:
|
||||
fsm.process_event(event)
|
||||
return method(self, *args, **kwargs)
|
||||
return inner
|
||||
return outer
|
||||
|
||||
|
||||
class BaseFilter(interface.FilterDriver):
|
||||
"""The generic PXE boot filtering interface implementation.
|
||||
|
||||
This driver doesn't do anything but provides a basic synchronization and
|
||||
initialization logic for some drivers to reuse. Subclasses have to provide
|
||||
a custom sync() method.
|
||||
"""
|
||||
|
||||
fsm = machines.FiniteMachine.build(State_space)
|
||||
fsm.default_start_state = States.uninitialized
|
||||
|
||||
def __init__(self):
|
||||
super(BaseFilter, self).__init__()
|
||||
self.lock = semaphore.BoundedSemaphore()
|
||||
self.fsm.initialize(start_state=States.uninitialized)
|
||||
|
||||
def __str__(self):
|
||||
return '%(driver)s, state=%(state)s' % {
|
||||
'driver': type(self).__name__, 'state': self.state}
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Current driver state."""
|
||||
return self.fsm.current_state
|
||||
|
||||
def reset(self):
|
||||
"""Reset internal driver state.
|
||||
|
||||
This method is called by the fsm_context manager upon exception as well
|
||||
as by the tear_down_filter method. A subclass might wish to override as
|
||||
necessary, though must not lock the driver. The overriding subclass
|
||||
should up-call.
|
||||
|
||||
:returns: nothing.
|
||||
"""
|
||||
LOG.debug('Resetting the PXE filter driver %s', self)
|
||||
# a reset event is always possible
|
||||
self.fsm.process_event(Events.reset)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def fsm_reset_on_error(self):
|
||||
"""Reset the filter driver upon generic exception.
|
||||
|
||||
The context is self.fsm. The automaton.exceptions.NotFound error is
|
||||
cast to the InvalidFilterDriverState error. Other exceptions trigger
|
||||
self.reset()
|
||||
|
||||
:raises: InvalidFilterDriverState
|
||||
:returns: nothing.
|
||||
"""
|
||||
LOG.debug('The PXE filter driver %s enters the fsm_reset_on_error '
|
||||
'context', self)
|
||||
try:
|
||||
yield self.fsm
|
||||
except automaton_errors.NotFound as e:
|
||||
raise InvalidFilterDriverState(_('The PXE filter driver %(driver)s'
|
||||
': my fsm encountered an '
|
||||
'exception: %(error)s') % {
|
||||
'driver': self, 'error': e})
|
||||
except Exception as e:
|
||||
LOG.exception('The PXE filter %(filter)s encountered an '
|
||||
'exception: %(error)s; resetting the filter',
|
||||
{'filter': self, 'error': e})
|
||||
self.reset()
|
||||
raise
|
||||
finally:
|
||||
LOG.debug('The PXE filter driver %s left the fsm_reset_on_error '
|
||||
'context', self)
|
||||
|
||||
@locked_driver_event(Events.initialize)
|
||||
def init_filter(self):
|
||||
"""Base driver initialization logic. Locked.
|
||||
|
||||
:raises: InvalidFilterDriverState
|
||||
:returns: nothing.
|
||||
"""
|
||||
LOG.debug('Initializing the PXE filter driver %s', self)
|
||||
|
||||
def tear_down_filter(self):
|
||||
"""Base driver tear down logic. Locked.
|
||||
|
||||
:returns: nothing.
|
||||
"""
|
||||
LOG.debug('Tearing down the PXE filter driver %s', self)
|
||||
with self.lock:
|
||||
self.reset()
|
||||
|
||||
@locked_driver_event(Events.sync)
|
||||
def sync(self, ironic):
|
||||
"""Base driver sync logic. Locked.
|
||||
|
||||
:param ironic: obligatory ironic client instance
|
||||
:returns: nothing.
|
||||
"""
|
||||
LOG.debug('Syncing the PXE filter driver %s', self)
|
||||
|
||||
def get_periodic_sync_task(self):
|
||||
"""Get periodic sync task for the filter.
|
||||
|
||||
:returns: a periodic task to be run in the background.
|
||||
"""
|
||||
ironic = ir_utils.get_client()
|
||||
return periodics.periodic(
|
||||
# NOTE(milan): the periodic decorator doesn't support 0 as
|
||||
# a spacing value of (a switched off) periodic
|
||||
spacing=CONF.pxe_filter.sync_period or float('inf'),
|
||||
enabled=bool(CONF.pxe_filter.sync_period))(
|
||||
lambda: self.sync(ironic))
|
||||
|
||||
|
||||
class NoopFilter(BaseFilter):
|
||||
"""A trivial PXE boot filter."""
|
||||
|
||||
|
||||
_DRIVER_MANAGER = None
|
||||
|
||||
|
||||
@lockutils.synchronized(__name__)
|
||||
def _driver_manager():
|
||||
"""Create a Stevedore driver manager for filtering drivers. Locked."""
|
||||
global _DRIVER_MANAGER
|
||||
|
||||
name = CONF.pxe_filter.driver
|
||||
if _DRIVER_MANAGER is None:
|
||||
_DRIVER_MANAGER = stevedore.driver.DriverManager(
|
||||
_STEVEDORE_DRIVER_NAMESPACE,
|
||||
name=name,
|
||||
invoke_on_load=True
|
||||
)
|
||||
|
||||
return _DRIVER_MANAGER
|
||||
|
||||
|
||||
def driver():
|
||||
"""Get the driver for the PXE filter.
|
||||
|
||||
:returns: the singleton PXE filter driver object.
|
||||
"""
|
||||
return _driver_manager().driver
|
64
ironic_inspector/pxe_filter/interface.py
Normal file
64
ironic_inspector/pxe_filter/interface.py
Normal file
@ -0,0 +1,64 @@
|
||||
# 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.
|
||||
|
||||
"""The code of the PXE boot filtering interface."""
|
||||
|
||||
import abc
|
||||
|
||||
import six
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class FilterDriver(object):
|
||||
"""The PXE boot filtering interface."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def init_filter(self):
|
||||
"""Initialize the internal driver state.
|
||||
|
||||
This method should be idempotent and may perform system-wide filter
|
||||
state changes. Can be synchronous.
|
||||
|
||||
:returns: nothing.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def sync(self, ironic):
|
||||
"""Synchronize the filter with ironic and inspector.
|
||||
|
||||
To be called both periodically and as needed by inspector. The filter
|
||||
should tear down its internal state if the sync method raises in order
|
||||
to "propagate" filtering exception between periodic and on-demand sync
|
||||
call. To this end, a driver should raise from the sync call if its
|
||||
internal state isn't properly initialized.
|
||||
|
||||
:param ironic: an ironic client instance.
|
||||
:returns: nothing.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def tear_down_filter(self):
|
||||
"""Reset the filter.
|
||||
|
||||
This method should be idempotent and may perform system-wide filter
|
||||
state changes. Can be synchronous.
|
||||
|
||||
:returns: nothing.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_periodic_sync_task(self):
|
||||
"""Get periodic sync task for the filter.
|
||||
|
||||
:returns: a periodic task to be run in the background.
|
||||
"""
|
272
ironic_inspector/test/unit/test_pxe_filter.py
Normal file
272
ironic_inspector/test/unit/test_pxe_filter.py
Normal file
@ -0,0 +1,272 @@
|
||||
# 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 fixtures
|
||||
import mock
|
||||
import six
|
||||
import stevedore
|
||||
|
||||
from automaton import exceptions as automaton_errors
|
||||
from eventlet import semaphore
|
||||
from futurist import periodics
|
||||
from oslo_config import cfg
|
||||
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector.pxe_filter import base as pxe_filter
|
||||
from ironic_inspector.pxe_filter import interface
|
||||
from ironic_inspector.test import base as test_base
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class TestDriverManager(test_base.BaseTest):
|
||||
def setUp(self):
|
||||
super(TestDriverManager, self).setUp()
|
||||
pxe_filter._DRIVER_MANAGER = None
|
||||
stevedore_driver_fixture = self.useFixture(fixtures.MockPatchObject(
|
||||
stevedore.driver, 'DriverManager', autospec=True))
|
||||
self.stevedore_driver_mock = stevedore_driver_fixture.mock
|
||||
|
||||
def test_default(self):
|
||||
driver_manager = pxe_filter._driver_manager()
|
||||
self.stevedore_driver_mock.assert_called_once_with(
|
||||
pxe_filter._STEVEDORE_DRIVER_NAMESPACE,
|
||||
name='noop',
|
||||
invoke_on_load=True
|
||||
)
|
||||
self.assertIsNotNone(driver_manager)
|
||||
self.assertIs(pxe_filter._DRIVER_MANAGER, driver_manager)
|
||||
|
||||
def test_pxe_filter_name(self):
|
||||
CONF.set_override('driver', 'foo', 'pxe_filter')
|
||||
driver_manager = pxe_filter._driver_manager()
|
||||
self.stevedore_driver_mock.assert_called_once_with(
|
||||
pxe_filter._STEVEDORE_DRIVER_NAMESPACE,
|
||||
'foo',
|
||||
invoke_on_load=True
|
||||
)
|
||||
self.assertIsNotNone(driver_manager)
|
||||
self.assertIs(pxe_filter._DRIVER_MANAGER, driver_manager)
|
||||
|
||||
def test_default_existing_driver_manager(self):
|
||||
pxe_filter._DRIVER_MANAGER = True
|
||||
driver_manager = pxe_filter._driver_manager()
|
||||
self.stevedore_driver_mock.assert_not_called()
|
||||
self.assertIs(pxe_filter._DRIVER_MANAGER, driver_manager)
|
||||
|
||||
|
||||
class TestDriverManagerLoading(test_base.BaseTest):
|
||||
def setUp(self):
|
||||
super(TestDriverManagerLoading, self).setUp()
|
||||
pxe_filter._DRIVER_MANAGER = None
|
||||
|
||||
@mock.patch.object(pxe_filter, 'NoopFilter', autospec=True)
|
||||
def test_pxe_filter_driver_loads(self, noop_driver_cls):
|
||||
CONF.set_override('driver', 'noop', 'pxe_filter')
|
||||
driver_manager = pxe_filter._driver_manager()
|
||||
noop_driver_cls.assert_called_once_with()
|
||||
self.assertIs(noop_driver_cls.return_value, driver_manager.driver)
|
||||
|
||||
def test_invalid_filter_driver(self):
|
||||
CONF.set_override('driver', 'foo', 'pxe_filter')
|
||||
six.assertRaisesRegex(self, stevedore.exception.NoMatches, 'foo',
|
||||
pxe_filter._driver_manager)
|
||||
self.assertIsNone(pxe_filter._DRIVER_MANAGER)
|
||||
|
||||
|
||||
class BaseFilterBaseTest(test_base.BaseTest):
|
||||
def setUp(self):
|
||||
super(BaseFilterBaseTest, self).setUp()
|
||||
self.mock_lock = mock.MagicMock(spec=semaphore.BoundedSemaphore)
|
||||
self.mock_bounded_semaphore = self.useFixture(
|
||||
fixtures.MockPatchObject(semaphore, 'BoundedSemaphore')).mock
|
||||
self.mock_bounded_semaphore.return_value = self.mock_lock
|
||||
self.driver = pxe_filter.NoopFilter()
|
||||
|
||||
def assert_driver_is_locked(self):
|
||||
"""Assert the driver is currently locked and wasn't locked before."""
|
||||
self.driver.lock.__enter__.assert_called_once_with()
|
||||
self.driver.lock.__exit__.assert_not_called()
|
||||
|
||||
def assert_driver_was_locked_once(self):
|
||||
"""Assert the driver was locked exactly once before."""
|
||||
self.driver.lock.__enter__.assert_called_once_with()
|
||||
self.driver.lock.__exit__.assert_called_once_with(None, None, None)
|
||||
|
||||
def assert_driver_was_not_locked(self):
|
||||
"""Assert the driver was not locked"""
|
||||
self.mock_lock.__enter__.assert_not_called()
|
||||
self.mock_lock.__exit__.assert_not_called()
|
||||
|
||||
|
||||
class TestLockedDriverEvent(BaseFilterBaseTest):
|
||||
def setUp(self):
|
||||
super(TestLockedDriverEvent, self).setUp()
|
||||
self.mock_fsm_reset_on_error = self.useFixture(
|
||||
fixtures.MockPatchObject(self.driver, 'fsm_reset_on_error')).mock
|
||||
self.expected_args = (None,)
|
||||
self.expected_kwargs = {'foo': None}
|
||||
self.mock_fsm = self.useFixture(
|
||||
fixtures.MockPatchObject(self.driver, 'fsm')).mock
|
||||
(self.driver.fsm_reset_on_error.return_value.
|
||||
__enter__.return_value) = self.mock_fsm
|
||||
|
||||
def test_locked_driver_event(self):
|
||||
event = 'foo'
|
||||
|
||||
@pxe_filter.locked_driver_event(event)
|
||||
def fun(driver, *args, **kwargs):
|
||||
self.assertIs(self.driver, driver)
|
||||
self.assertEqual(self.expected_args, args)
|
||||
self.assertEqual(self.expected_kwargs, kwargs)
|
||||
self.assert_driver_is_locked()
|
||||
|
||||
self.assert_driver_was_not_locked()
|
||||
fun(self.driver, *self.expected_args, **self.expected_kwargs)
|
||||
|
||||
self.mock_fsm_reset_on_error.assert_called_once_with()
|
||||
self.mock_fsm.process_event.assert_called_once_with(event)
|
||||
self.assert_driver_was_locked_once()
|
||||
|
||||
|
||||
class TestBaseFilterFsmPrecautions(BaseFilterBaseTest):
|
||||
def setUp(self):
|
||||
super(TestBaseFilterFsmPrecautions, self).setUp()
|
||||
self.mock_fsm = self.useFixture(
|
||||
fixtures.MockPatchObject(pxe_filter.NoopFilter, 'fsm')).mock
|
||||
# NOTE(milan): overriding driver so that the patch ^ is applied
|
||||
self.mock_bounded_semaphore.reset_mock()
|
||||
self.driver = pxe_filter.NoopFilter()
|
||||
self.mock_reset = self.useFixture(
|
||||
fixtures.MockPatchObject(self.driver, 'reset')).mock
|
||||
|
||||
def test___init__(self):
|
||||
self.assertIs(self.mock_lock, self.driver.lock)
|
||||
self.mock_bounded_semaphore.assert_called_once_with()
|
||||
self.assertIs(self.mock_fsm, self.driver.fsm)
|
||||
self.mock_fsm.initialize.assert_called_once_with(
|
||||
start_state=pxe_filter.States.uninitialized)
|
||||
|
||||
def test_fsm_reset_on_error(self):
|
||||
with self.driver.fsm_reset_on_error() as fsm:
|
||||
self.assertIs(self.mock_fsm, fsm)
|
||||
|
||||
self.mock_reset.assert_not_called()
|
||||
|
||||
def test_fsm_automaton_error(self):
|
||||
|
||||
def fun():
|
||||
with self.driver.fsm_reset_on_error():
|
||||
raise automaton_errors.NotFound('Oops!')
|
||||
|
||||
self.assertRaisesRegex(pxe_filter.InvalidFilterDriverState,
|
||||
'.*NoopFilter.*Oops!', fun)
|
||||
self.mock_reset.assert_not_called()
|
||||
|
||||
def test_fsm_reset_on_error_ctx_custom_error(self):
|
||||
|
||||
class MyError(Exception):
|
||||
pass
|
||||
|
||||
def fun():
|
||||
with self.driver.fsm_reset_on_error():
|
||||
raise MyError('Oops!')
|
||||
|
||||
self.assertRaisesRegex(MyError, 'Oops!', fun)
|
||||
self.mock_reset.assert_called_once_with()
|
||||
|
||||
|
||||
class TestBaseFilterInterface(BaseFilterBaseTest):
|
||||
def setUp(self):
|
||||
super(TestBaseFilterInterface, self).setUp()
|
||||
self.mock_get_client = self.useFixture(
|
||||
fixtures.MockPatchObject(ir_utils, 'get_client')).mock
|
||||
self.mock_ironic = mock.Mock()
|
||||
self.mock_get_client.return_value = self.mock_ironic
|
||||
self.mock_periodic = self.useFixture(
|
||||
fixtures.MockPatchObject(periodics, 'periodic')).mock
|
||||
self.mock_reset = self.useFixture(
|
||||
fixtures.MockPatchObject(self.driver, 'reset')).mock
|
||||
self.mock_log = self.useFixture(
|
||||
fixtures.MockPatchObject(pxe_filter, 'LOG')).mock
|
||||
self.driver.fsm_reset_on_error = self.useFixture(
|
||||
fixtures.MockPatchObject(self.driver, 'fsm_reset_on_error')).mock
|
||||
|
||||
def test_init_filter(self):
|
||||
self.driver.init_filter()
|
||||
|
||||
self.mock_log.debug.assert_called_once_with(
|
||||
'Initializing the PXE filter driver %s', self.driver)
|
||||
self.mock_reset.assert_not_called()
|
||||
|
||||
def test_sync(self):
|
||||
self.driver.sync(self.mock_ironic)
|
||||
|
||||
self.mock_log.debug.assert_called_once_with(
|
||||
'Syncing the PXE filter driver %s', self.driver)
|
||||
self.mock_reset.assert_not_called()
|
||||
|
||||
def test_tear_down_filter(self):
|
||||
self.assert_driver_was_not_locked()
|
||||
self.driver.tear_down_filter()
|
||||
|
||||
self.assert_driver_was_locked_once()
|
||||
self.mock_reset.assert_called_once_with()
|
||||
|
||||
def test_get_periodic_sync_task(self):
|
||||
sync_mock = self.useFixture(
|
||||
fixtures.MockPatchObject(self.driver, 'sync')).mock
|
||||
self.driver.get_periodic_sync_task()
|
||||
self.mock_periodic.assert_called_once_with(spacing=15, enabled=True)
|
||||
self.mock_periodic.return_value.call_args[0][0]()
|
||||
sync_mock.assert_called_once_with(self.mock_get_client.return_value)
|
||||
|
||||
def test_get_periodic_sync_task_disabled(self):
|
||||
CONF.set_override('sync_period', 0, 'pxe_filter')
|
||||
self.driver.get_periodic_sync_task()
|
||||
self.mock_periodic.assert_called_once_with(spacing=float('inf'),
|
||||
enabled=False)
|
||||
|
||||
def test_get_periodic_sync_task_custom_spacing(self):
|
||||
CONF.set_override('sync_period', 4224, 'pxe_filter')
|
||||
self.driver.get_periodic_sync_task()
|
||||
self.mock_periodic.assert_called_once_with(spacing=4224, enabled=True)
|
||||
|
||||
|
||||
class TestDriverReset(BaseFilterBaseTest):
|
||||
def setUp(self):
|
||||
super(TestDriverReset, self).setUp()
|
||||
self.mock_fsm = self.useFixture(
|
||||
fixtures.MockPatchObject(self.driver, 'fsm')).mock
|
||||
|
||||
def test_reset(self):
|
||||
self.driver.reset()
|
||||
|
||||
self.assert_driver_was_not_locked()
|
||||
self.mock_fsm.process_event.assert_called_once_with(
|
||||
pxe_filter.Events.reset)
|
||||
|
||||
|
||||
class TestDriver(test_base.BaseTest):
|
||||
def setUp(self):
|
||||
super(TestDriver, self).setUp()
|
||||
self.mock_driver = mock.Mock(spec=interface.FilterDriver)
|
||||
self.mock__driver_manager = self.useFixture(
|
||||
fixtures.MockPatchObject(pxe_filter, '_driver_manager')).mock
|
||||
self.mock__driver_manager.return_value.driver = self.mock_driver
|
||||
|
||||
def test_driver(self):
|
||||
ret = pxe_filter.driver()
|
||||
|
||||
self.assertIs(self.mock_driver, ret)
|
||||
self.mock__driver_manager.assert_called_once_with()
|
@ -57,6 +57,8 @@ ironic_inspector.rules.actions =
|
||||
set-attribute = ironic_inspector.plugins.rules:SetAttributeAction
|
||||
set-capability = ironic_inspector.plugins.rules:SetCapabilityAction
|
||||
extend-attribute = ironic_inspector.plugins.rules:ExtendAttributeAction
|
||||
ironic_inspector.pxe_filter =
|
||||
noop = ironic_inspector.pxe_filter.base:NoopFilter
|
||||
oslo.config.opts =
|
||||
ironic_inspector = ironic_inspector.conf:list_opts
|
||||
ironic_inspector.common.ironic = ironic_inspector.common.ironic:list_opts
|
||||
|
Loading…
Reference in New Issue
Block a user