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
|
.. _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
|
.. _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
|
.. _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
|
#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]
|
[swift]
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -199,10 +199,18 @@ SERVICE_OPTS = [
|
|||||||
help=_('Limit the number of elements an API list-call returns'))
|
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(SERVICE_OPTS)
|
||||||
cfg.CONF.register_opts(FIREWALL_OPTS, group='firewall')
|
cfg.CONF.register_opts(FIREWALL_OPTS, group='firewall')
|
||||||
cfg.CONF.register_opts(PROCESSING_OPTS, group='processing')
|
cfg.CONF.register_opts(PROCESSING_OPTS, group='processing')
|
||||||
|
cfg.CONF.register_opts(PXE_FILTER_OPTS, 'pxe_filter')
|
||||||
|
|
||||||
|
|
||||||
def list_opts():
|
def list_opts():
|
||||||
@ -210,6 +218,7 @@ def list_opts():
|
|||||||
('', SERVICE_OPTS),
|
('', SERVICE_OPTS),
|
||||||
('firewall', FIREWALL_OPTS),
|
('firewall', FIREWALL_OPTS),
|
||||||
('processing', PROCESSING_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-attribute = ironic_inspector.plugins.rules:SetAttributeAction
|
||||||
set-capability = ironic_inspector.plugins.rules:SetCapabilityAction
|
set-capability = ironic_inspector.plugins.rules:SetCapabilityAction
|
||||||
extend-attribute = ironic_inspector.plugins.rules:ExtendAttributeAction
|
extend-attribute = ironic_inspector.plugins.rules:ExtendAttributeAction
|
||||||
|
ironic_inspector.pxe_filter =
|
||||||
|
noop = ironic_inspector.pxe_filter.base:NoopFilter
|
||||||
oslo.config.opts =
|
oslo.config.opts =
|
||||||
ironic_inspector = ironic_inspector.conf:list_opts
|
ironic_inspector = ironic_inspector.conf:list_opts
|
||||||
ironic_inspector.common.ironic = ironic_inspector.common.ironic:list_opts
|
ironic_inspector.common.ironic = ironic_inspector.common.ironic:list_opts
|
||||||
|
Loading…
Reference in New Issue
Block a user