diff --git a/ironic/conductor/base_manager.py b/ironic/conductor/base_manager.py new file mode 100644 index 0000000000..b80b41c66a --- /dev/null +++ b/ironic/conductor/base_manager.py @@ -0,0 +1,318 @@ +# 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 conductor manager functionality.""" + +import inspect +import threading + +from eventlet import greenpool +from oslo_concurrency import lockutils +from oslo_config import cfg +from oslo_context import context as ironic_context +from oslo_db import exception as db_exception +from oslo_log import log +from oslo_service import periodic_task +from oslo_utils import excutils + +from ironic.common import driver_factory +from ironic.common import exception +from ironic.common import hash_ring as hash +from ironic.common.i18n import _ +from ironic.common.i18n import _LC +from ironic.common.i18n import _LE +from ironic.common.i18n import _LI +from ironic.common.i18n import _LW +from ironic.common import rpc +from ironic.common import states +from ironic.conductor import task_manager +from ironic.db import api as dbapi + + +conductor_opts = [ + cfg.IntOpt('workers_pool_size', + default=100, + help=_('The size of the workers greenthread pool.')), + cfg.IntOpt('heartbeat_interval', + default=10, + help=_('Seconds between conductor heart beats.')), +] + + +CONF = cfg.CONF +CONF.register_opts(conductor_opts, 'conductor') +LOG = log.getLogger(__name__) +WORKER_SPAWN_lOCK = "conductor_worker_spawn" + + +class BaseConductorManager(periodic_task.PeriodicTasks): + + def __init__(self, host, topic): + super(BaseConductorManager, self).__init__(CONF) + if not host: + host = CONF.host + self.host = host + self.topic = topic + self.notifier = rpc.get_notifier() + + def _get_driver(self, driver_name): + """Get the driver. + + :param driver_name: name of the driver. + :returns: the driver; an instance of a class which implements + :class:`ironic.drivers.base.BaseDriver`. + :raises: DriverNotFound if the driver is not loaded. + + """ + try: + return self._driver_factory[driver_name].obj + except KeyError: + raise exception.DriverNotFound(driver_name=driver_name) + + def init_host(self): + self.dbapi = dbapi.get_instance() + + self._keepalive_evt = threading.Event() + """Event for the keepalive thread.""" + + self._worker_pool = greenpool.GreenPool( + size=CONF.conductor.workers_pool_size) + """GreenPool of background workers for performing tasks async.""" + + self.ring_manager = hash.HashRingManager() + """Consistent hash ring which maps drivers to conductors.""" + + # NOTE(deva): instantiating DriverFactory may raise DriverLoadError + # or DriverNotFound + self._driver_factory = driver_factory.DriverFactory() + """Driver factory loads all enabled drivers.""" + + self.drivers = self._driver_factory.names + """List of driver names which this conductor supports.""" + + if not self.drivers: + msg = _LE("Conductor %s cannot be started because no drivers " + "were loaded. This could be because no drivers were " + "specified in 'enabled_drivers' config option.") + LOG.error(msg, self.host) + raise exception.NoDriversLoaded(conductor=self.host) + + # Collect driver-specific periodic tasks + for driver_obj in driver_factory.drivers().values(): + self._collect_periodic_tasks(driver_obj) + for iface_name in (driver_obj.core_interfaces + + driver_obj.standard_interfaces + + ['vendor']): + iface = getattr(driver_obj, iface_name, None) + if iface: + self._collect_periodic_tasks(iface) + + # clear all locks held by this conductor before registering + self.dbapi.clear_node_reservations_for_conductor(self.host) + try: + # Register this conductor with the cluster + cdr = self.dbapi.register_conductor({'hostname': self.host, + 'drivers': self.drivers}) + except exception.ConductorAlreadyRegistered: + # This conductor was already registered and did not shut down + # properly, so log a warning and update the record. + LOG.warning( + _LW("A conductor with hostname %(hostname)s " + "was previously registered. Updating registration"), + {'hostname': self.host}) + cdr = self.dbapi.register_conductor({'hostname': self.host, + 'drivers': self.drivers}, + update_existing=True) + self.conductor = cdr + + # NOTE(lucasagomes): If the conductor server dies abruptly + # mid deployment (OMM Killer, power outage, etc...) we + # can not resume the deployment even if the conductor + # comes back online. Cleaning the reservation of the nodes + # (dbapi.clear_node_reservations_for_conductor) is not enough to + # unstick it, so let's gracefully fail the deployment so the node + # can go through the steps (deleting & cleaning) to make itself + # available again. + filters = {'reserved': False, + 'provision_state': states.DEPLOYING} + last_error = (_("The deployment can't be resumed by conductor " + "%s. Moving to fail state.") % self.host) + self._fail_if_in_state(ironic_context.get_admin_context(), filters, + states.DEPLOYING, 'provision_updated_at', + last_error=last_error) + + # Spawn a dedicated greenthread for the keepalive + try: + self._spawn_worker(self._conductor_service_record_keepalive) + LOG.info(_LI('Successfully started conductor with hostname ' + '%(hostname)s.'), + {'hostname': self.host}) + except exception.NoFreeConductorWorker: + with excutils.save_and_reraise_exception(): + LOG.critical(_LC('Failed to start keepalive')) + self.del_host() + + def _collect_periodic_tasks(self, obj): + for n, method in inspect.getmembers(obj, inspect.ismethod): + if getattr(method, '_periodic_enabled', False): + self.add_periodic_task(method) + + def del_host(self, deregister=True): + # Conductor deregistration fails if called on non-initialized + # conductor (e.g. when rpc server is unreachable). + if not hasattr(self, 'conductor'): + return + self._keepalive_evt.set() + if deregister: + try: + # Inform the cluster that this conductor is shutting down. + # Note that rebalancing will not occur immediately, but when + # the periodic sync takes place. + self.dbapi.unregister_conductor(self.host) + LOG.info(_LI('Successfully stopped conductor with hostname ' + '%(hostname)s.'), + {'hostname': self.host}) + except exception.ConductorNotFound: + pass + else: + LOG.info(_LI('Not deregistering conductor with hostname ' + '%(hostname)s.'), + {'hostname': self.host}) + # Waiting here to give workers the chance to finish. This has the + # benefit of releasing locks workers placed on nodes, as well as + # having work complete normally. + self._worker_pool.waitall() + + def periodic_tasks(self, context, raise_on_error=False): + """Periodic tasks are run at pre-specified interval.""" + return self.run_periodic_tasks(context, raise_on_error=raise_on_error) + + def iter_nodes(self, fields=None, **kwargs): + """Iterate over nodes mapped to this conductor. + + Requests node set from and filters out nodes that are not + mapped to this conductor. + + Yields tuples (node_uuid, driver, ...) where ... is derived from + fields argument, e.g.: fields=None means yielding ('uuid', 'driver'), + fields=['foo'] means yielding ('uuid', 'driver', 'foo'). + + :param fields: list of fields to fetch in addition to uuid and driver + :param kwargs: additional arguments to pass to dbapi when looking for + nodes + :return: generator yielding tuples of requested fields + """ + columns = ['uuid', 'driver'] + list(fields or ()) + node_list = self.dbapi.get_nodeinfo_list(columns=columns, **kwargs) + for result in node_list: + if self._mapped_to_this_conductor(*result[:2]): + yield result + + @lockutils.synchronized(WORKER_SPAWN_lOCK, 'ironic-') + def _spawn_worker(self, func, *args, **kwargs): + + """Create a greenthread to run func(*args, **kwargs). + + Spawns a greenthread if there are free slots in pool, otherwise raises + exception. Execution control returns immediately to the caller. + + :returns: GreenThread object. + :raises: NoFreeConductorWorker if worker pool is currently full. + + """ + if self._worker_pool.free(): + return self._worker_pool.spawn(func, *args, **kwargs) + else: + raise exception.NoFreeConductorWorker() + + def _conductor_service_record_keepalive(self): + while not self._keepalive_evt.is_set(): + try: + self.dbapi.touch_conductor(self.host) + except db_exception.DBConnectionError: + LOG.warning(_LW('Conductor could not connect to database ' + 'while heartbeating.')) + self._keepalive_evt.wait(CONF.conductor.heartbeat_interval) + + def _mapped_to_this_conductor(self, node_uuid, driver): + """Check that node is mapped to this conductor. + + Note that because mappings are eventually consistent, it is possible + for two conductors to simultaneously believe that a node is mapped to + them. Any operation that depends on exclusive control of a node should + take out a lock. + """ + try: + ring = self.ring_manager[driver] + except exception.DriverNotFound: + return False + + return self.host in ring.get_hosts(node_uuid) + + def _fail_if_in_state(self, context, filters, provision_state, + sort_key, callback_method=None, + err_handler=None, last_error=None): + """Fail nodes that are in specified state. + + Retrieves nodes that satisfy the criteria in 'filters'. + If any of these nodes is in 'provision_state', it has failed + in whatever provisioning activity it was currently doing. + That failure is processed here. + + :param: context: request context + :param: filters: criteria (as a dictionary) to get the desired + list of nodes that satisfy the filter constraints. + For example, if filters['provisioned_before'] = 60, + this would process nodes whose provision_updated_at + field value was 60 or more seconds before 'now'. + :param: provision_state: provision_state that the node is in, + for the provisioning activity to have failed. + :param: sort_key: the nodes are sorted based on this key. + :param: callback_method: the callback method to be invoked in a + spawned thread, for a failed node. This + method must take a :class:`TaskManager` as + the first (and only required) parameter. + :param: err_handler: for a failed node, the error handler to invoke + if an error occurs trying to spawn an thread + to do the callback_method. + :param: last_error: the error message to be updated in node.last_error + + """ + node_iter = self.iter_nodes(filters=filters, + sort_key=sort_key, + sort_dir='asc') + + workers_count = 0 + for node_uuid, driver in node_iter: + try: + with task_manager.acquire(context, node_uuid, + purpose='node state check') as task: + if (task.node.maintenance or + task.node.provision_state != provision_state): + continue + + # timeout has been reached - process the event 'fail' + if callback_method: + task.process_event('fail', + callback=self._spawn_worker, + call_args=(callback_method, task), + err_handler=err_handler) + else: + task.node.last_error = last_error + task.process_event('fail') + except exception.NoFreeConductorWorker: + break + except (exception.NodeLocked, exception.NodeNotFound): + continue + workers_count += 1 + if workers_count >= CONF.conductor.periodic_max_workers: + break diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py index 3f75ac5acb..af8e6615fd 100644 --- a/ironic/conductor/manager.py +++ b/ironic/conductor/manager.py @@ -43,16 +43,10 @@ a change, etc. import collections import datetime -import inspect import tempfile -import threading import eventlet -from eventlet import greenpool -from oslo_concurrency import lockutils from oslo_config import cfg -from oslo_context import context as ironic_context -from oslo_db import exception as db_exception from oslo_log import log import oslo_messaging as messaging from oslo_service import periodic_task @@ -60,27 +54,22 @@ from oslo_utils import excutils from oslo_utils import uuidutils from ironic.common import dhcp_factory -from ironic.common import driver_factory from ironic.common import exception from ironic.common.glance_service import service_utils as glance_utils -from ironic.common import hash_ring as hash from ironic.common.i18n import _ -from ironic.common.i18n import _LC from ironic.common.i18n import _LE from ironic.common.i18n import _LI from ironic.common.i18n import _LW from ironic.common import images -from ironic.common import rpc from ironic.common import states from ironic.common import swift +from ironic.conductor import base_manager from ironic.conductor import task_manager from ironic.conductor import utils -from ironic.db import api as dbapi from ironic import objects from ironic.objects import base as objects_base MANAGER_TOPIC = 'ironic.conductor_manager' -WORKER_SPAWN_lOCK = "conductor_worker_spawn" LOG = log.getLogger(__name__) @@ -89,9 +78,6 @@ conductor_opts = [ help=_('URL of Ironic API service. If not set ironic can ' 'get the current value from the keystone service ' 'catalog.')), - cfg.IntOpt('heartbeat_interval', - default=10, - help=_('Seconds between conductor heart beats.')), cfg.IntOpt('heartbeat_timeout', default=60, help=_('Maximum time (in seconds) since the last check-in ' @@ -126,9 +112,6 @@ conductor_opts = [ help=_('Maximum number of worker threads that can be started ' 'simultaneously by a periodic task. Should be less ' 'than RPC thread pool size.')), - cfg.IntOpt('workers_pool_size', - default=100, - help=_('The size of the workers greenthread pool.')), cfg.IntOpt('node_locked_retry_attempts', default=3, help=_('Number of attempts to grab a node lock.')), @@ -197,7 +180,7 @@ CONF.register_opts(conductor_opts, 'conductor') SYNC_EXCLUDED_STATES = (states.DEPLOYWAIT, states.CLEANWAIT, states.ENROLL) -class ConductorManager(periodic_task.PeriodicTasks): +class ConductorManager(base_manager.BaseConductorManager): """Ironic Conductor manager main class.""" # NOTE(rloo): This must be in sync with rpcapi.ConductorAPI's. @@ -206,171 +189,8 @@ class ConductorManager(periodic_task.PeriodicTasks): target = messaging.Target(version=RPC_API_VERSION) def __init__(self, host, topic): - super(ConductorManager, self).__init__(CONF) - if not host: - host = CONF.host - self.host = host - self.topic = topic + super(ConductorManager, self).__init__(host, topic) self.power_state_sync_count = collections.defaultdict(int) - self.notifier = rpc.get_notifier() - - def _get_driver(self, driver_name): - """Get the driver. - - :param driver_name: name of the driver. - :returns: the driver; an instance of a class which implements - :class:`ironic.drivers.base.BaseDriver`. - :raises: DriverNotFound if the driver is not loaded. - - """ - try: - return self._driver_factory[driver_name].obj - except KeyError: - raise exception.DriverNotFound(driver_name=driver_name) - - def init_host(self): - self.dbapi = dbapi.get_instance() - - self._keepalive_evt = threading.Event() - """Event for the keepalive thread.""" - - self._worker_pool = greenpool.GreenPool( - size=CONF.conductor.workers_pool_size) - """GreenPool of background workers for performing tasks async.""" - - self.ring_manager = hash.HashRingManager() - """Consistent hash ring which maps drivers to conductors.""" - - # NOTE(deva): instantiating DriverFactory may raise DriverLoadError - # or DriverNotFound - self._driver_factory = driver_factory.DriverFactory() - """Driver factory loads all enabled drivers.""" - - self.drivers = self._driver_factory.names - """List of driver names which this conductor supports.""" - - if not self.drivers: - msg = _LE("Conductor %s cannot be started because no drivers " - "were loaded. This could be because no drivers were " - "specified in 'enabled_drivers' config option.") - LOG.error(msg, self.host) - raise exception.NoDriversLoaded(conductor=self.host) - - # Collect driver-specific periodic tasks - for driver_obj in driver_factory.drivers().values(): - self._collect_periodic_tasks(driver_obj) - for iface_name in (driver_obj.core_interfaces + - driver_obj.standard_interfaces + - ['vendor']): - iface = getattr(driver_obj, iface_name, None) - if iface: - self._collect_periodic_tasks(iface) - - # clear all locks held by this conductor before registering - self.dbapi.clear_node_reservations_for_conductor(self.host) - try: - # Register this conductor with the cluster - cdr = self.dbapi.register_conductor({'hostname': self.host, - 'drivers': self.drivers}) - except exception.ConductorAlreadyRegistered: - # This conductor was already registered and did not shut down - # properly, so log a warning and update the record. - LOG.warning( - _LW("A conductor with hostname %(hostname)s " - "was previously registered. Updating registration"), - {'hostname': self.host}) - cdr = self.dbapi.register_conductor({'hostname': self.host, - 'drivers': self.drivers}, - update_existing=True) - self.conductor = cdr - - # NOTE(lucasagomes): If the conductor server dies abruptly - # mid deployment (OMM Killer, power outage, etc...) we - # can not resume the deployment even if the conductor - # comes back online. Cleaning the reservation of the nodes - # (dbapi.clear_node_reservations_for_conductor) is not enough to - # unstick it, so let's gracefully fail the deployment so the node - # can go through the steps (deleting & cleaning) to make itself - # available again. - filters = {'reserved': False, - 'provision_state': states.DEPLOYING} - last_error = (_("The deployment can't be resumed by conductor " - "%s. Moving to fail state.") % self.host) - self._fail_if_in_state(ironic_context.get_admin_context(), filters, - states.DEPLOYING, 'provision_updated_at', - last_error=last_error) - - # Spawn a dedicated greenthread for the keepalive - try: - self._spawn_worker(self._conductor_service_record_keepalive) - LOG.info(_LI('Successfully started conductor with hostname ' - '%(hostname)s.'), - {'hostname': self.host}) - except exception.NoFreeConductorWorker: - with excutils.save_and_reraise_exception(): - LOG.critical(_LC('Failed to start keepalive')) - self.del_host() - - def _collect_periodic_tasks(self, obj): - for n, method in inspect.getmembers(obj, inspect.ismethod): - if getattr(method, '_periodic_enabled', False): - self.add_periodic_task(method) - - def del_host(self, deregister=True): - # Conductor deregistration fails if called on non-initialized - # conductor (e.g. when rpc server is unreachable). - if not hasattr(self, 'conductor'): - return - self._keepalive_evt.set() - if deregister: - try: - # Inform the cluster that this conductor is shutting down. - # Note that rebalancing will not occur immediately, but when - # the periodic sync takes place. - self.dbapi.unregister_conductor(self.host) - LOG.info(_LI('Successfully stopped conductor with hostname ' - '%(hostname)s.'), - {'hostname': self.host}) - except exception.ConductorNotFound: - pass - else: - LOG.info(_LI('Not deregistering conductor with hostname ' - '%(hostname)s.'), - {'hostname': self.host}) - # Waiting here to give workers the chance to finish. This has the - # benefit of releasing locks workers placed on nodes, as well as - # having work complete normally. - self._worker_pool.waitall() - - def periodic_tasks(self, context, raise_on_error=False): - """Periodic tasks are run at pre-specified interval.""" - return self.run_periodic_tasks(context, raise_on_error=raise_on_error) - - @lockutils.synchronized(WORKER_SPAWN_lOCK, 'ironic-') - def _spawn_worker(self, func, *args, **kwargs): - - """Create a greenthread to run func(*args, **kwargs). - - Spawns a greenthread if there are free slots in pool, otherwise raises - exception. Execution control returns immediately to the caller. - - :returns: GreenThread object. - :raises: NoFreeConductorWorker if worker pool is currently full. - - """ - if self._worker_pool.free(): - return self._worker_pool.spawn(func, *args, **kwargs) - else: - raise exception.NoFreeConductorWorker() - - def _conductor_service_record_keepalive(self): - while not self._keepalive_evt.is_set(): - try: - self.dbapi.touch_conductor(self.host) - except db_exception.DBConnectionError: - LOG.warning(_LW('Conductor could not connect to database ' - 'while heartbeating.')) - self._keepalive_evt.wait(CONF.conductor.heartbeat_interval) @messaging.expected_exceptions(exception.InvalidParameterValue, exception.MissingParameterValue, @@ -1452,42 +1272,6 @@ class ConductorManager(periodic_task.PeriodicTasks): if workers_count == CONF.conductor.periodic_max_workers: break - def _mapped_to_this_conductor(self, node_uuid, driver): - """Check that node is mapped to this conductor. - - Note that because mappings are eventually consistent, it is possible - for two conductors to simultaneously believe that a node is mapped to - them. Any operation that depends on exclusive control of a node should - take out a lock. - """ - try: - ring = self.ring_manager[driver] - except exception.DriverNotFound: - return False - - return self.host in ring.get_hosts(node_uuid) - - def iter_nodes(self, fields=None, **kwargs): - """Iterate over nodes mapped to this conductor. - - Requests node set from and filters out nodes that are not - mapped to this conductor. - - Yields tuples (node_uuid, driver, ...) where ... is derived from - fields argument, e.g.: fields=None means yielding ('uuid', 'driver'), - fields=['foo'] means yielding ('uuid', 'driver', 'foo'). - - :param fields: list of fields to fetch in addition to uuid and driver - :param kwargs: additional arguments to pass to dbapi when looking for - nodes - :return: generator yielding tuples of requested fields - """ - columns = ['uuid', 'driver'] + list(fields or ()) - node_list = self.dbapi.get_nodeinfo_list(columns=columns, **kwargs) - for result in node_list: - if self._mapped_to_this_conductor(*result[:2]): - yield result - @messaging.expected_exceptions(exception.NodeLocked) def validate_driver_interfaces(self, context, node_id): """Validate the `core` and `standardized` interfaces for drivers. @@ -2024,65 +1808,6 @@ class ConductorManager(periodic_task.PeriodicTasks): self._fail_if_in_state(context, filters, states.INSPECTING, sort_key, last_error=last_error) - def _fail_if_in_state(self, context, filters, provision_state, - sort_key, callback_method=None, - err_handler=None, last_error=None): - """Fail nodes that are in specified state. - - Retrieves nodes that satisfy the criteria in 'filters'. - If any of these nodes is in 'provision_state', it has failed - in whatever provisioning activity it was currently doing. - That failure is processed here. - - :param: context: request context - :param: filters: criteria (as a dictionary) to get the desired - list of nodes that satisfy the filter constraints. - For example, if filters['provisioned_before'] = 60, - this would process nodes whose provision_updated_at - field value was 60 or more seconds before 'now'. - :param: provision_state: provision_state that the node is in, - for the provisioning activity to have failed. - :param: sort_key: the nodes are sorted based on this key. - :param: callback_method: the callback method to be invoked in a - spawned thread, for a failed node. This - method must take a :class:`TaskManager` as - the first (and only required) parameter. - :param: err_handler: for a failed node, the error handler to invoke - if an error occurs trying to spawn an thread - to do the callback_method. - :param: last_error: the error message to be updated in node.last_error - - """ - node_iter = self.iter_nodes(filters=filters, - sort_key=sort_key, - sort_dir='asc') - - workers_count = 0 - for node_uuid, driver in node_iter: - try: - with task_manager.acquire(context, node_uuid, - purpose='node state check') as task: - if (task.node.maintenance or - task.node.provision_state != provision_state): - continue - - # timeout has been reached - process the event 'fail' - if callback_method: - task.process_event('fail', - callback=self._spawn_worker, - call_args=(callback_method, task), - err_handler=err_handler) - else: - task.node.last_error = last_error - task.process_event('fail') - except exception.NoFreeConductorWorker: - break - except (exception.NodeLocked, exception.NodeNotFound): - continue - workers_count += 1 - if workers_count >= CONF.conductor.periodic_max_workers: - break - @messaging.expected_exceptions(exception.NodeLocked, exception.UnsupportedDriverExtension, exception.InvalidParameterValue, diff --git a/ironic/tests/unit/conductor/mgr_utils.py b/ironic/tests/unit/conductor/mgr_utils.py index 84b3f68d3f..4403efa59a 100644 --- a/ironic/tests/unit/conductor/mgr_utils.py +++ b/ironic/tests/unit/conductor/mgr_utils.py @@ -17,10 +17,17 @@ """Test utils for Ironic Managers.""" +import mock +from oslo_utils import strutils +from oslo_utils import uuidutils import pkg_resources from stevedore import dispatch from ironic.common import driver_factory +from ironic.common import exception +from ironic.common import states +from ironic.conductor import manager +from ironic import objects def mock_the_extension_manager(driver="fake", namespace="ironic.drivers"): @@ -55,3 +62,126 @@ def mock_the_extension_manager(driver="fake", namespace="ironic.drivers"): for e in [mock_ext]) return (mock_ext_mgr, mock_ext) + + +class CommonMixIn(object): + @staticmethod + def _create_node(**kwargs): + attrs = {'id': 1, + 'uuid': uuidutils.generate_uuid(), + 'power_state': states.POWER_OFF, + 'target_power_state': None, + 'maintenance': False, + 'reservation': None} + attrs.update(kwargs) + node = mock.Mock(spec_set=objects.Node) + for attr in attrs: + setattr(node, attr, attrs[attr]) + return node + + def _create_task(self, node=None, node_attrs=None): + if node_attrs is None: + node_attrs = {} + if node is None: + node = self._create_node(**node_attrs) + task = mock.Mock(spec_set=['node', 'release_resources', + 'spawn_after', 'process_event']) + task.node = node + return task + + def _get_nodeinfo_list_response(self, nodes=None): + if nodes is None: + nodes = [self.node] + elif not isinstance(nodes, (list, tuple)): + nodes = [nodes] + return [tuple(getattr(n, c) for c in self.columns) for n in nodes] + + def _get_acquire_side_effect(self, task_infos): + """Helper method to generate a task_manager.acquire() side effect. + + This accepts a list of information about task mocks to return. + task_infos can be a single entity or a list. + + Each task_info can be a single entity, the task to return, or it + can be a tuple of (task, exception_to_raise_on_exit). 'task' can + be an exception to raise on __enter__. + + Examples: _get_acquire_side_effect(self, task): Yield task + _get_acquire_side_effect(self, [task, enter_exception(), + (task2, exit_exception())]) + Yield task on first call to acquire() + raise enter_exception() in __enter__ on 2nd call to + acquire() + Yield task2 on 3rd call to acquire(), but raise + exit_exception() on __exit__() + """ + tasks = [] + exit_exceptions = [] + if not isinstance(task_infos, list): + task_infos = [task_infos] + for task_info in task_infos: + if isinstance(task_info, tuple): + task, exc = task_info + else: + task = task_info + exc = None + tasks.append(task) + exit_exceptions.append(exc) + + class FakeAcquire(object): + def __init__(fa_self, context, node_id, *args, **kwargs): + # We actually verify these arguments via + # acquire_mock.call_args_list(). However, this stores the + # node_id so we can assert we're returning the correct node + # in __enter__(). + fa_self.node_id = node_id + + def __enter__(fa_self): + task = tasks.pop(0) + if isinstance(task, Exception): + raise task + # NOTE(comstud): Not ideal to throw this into + # a helper, however it's the cleanest way + # to verify we're dealing with the correct task/node. + if strutils.is_int_like(fa_self.node_id): + self.assertEqual(fa_self.node_id, task.node.id) + else: + self.assertEqual(fa_self.node_id, task.node.uuid) + return task + + def __exit__(fa_self, exc_typ, exc_val, exc_tb): + exc = exit_exceptions.pop(0) + if exc_typ is None and exc is not None: + raise exc + + return FakeAcquire + + +class ServiceSetUpMixin(object): + def setUp(self): + super(ServiceSetUpMixin, self).setUp() + self.hostname = 'test-host' + self.config(enabled_drivers=['fake']) + self.config(node_locked_retry_attempts=1, group='conductor') + self.config(node_locked_retry_interval=0, group='conductor') + self.service = manager.ConductorManager(self.hostname, 'test-topic') + mock_the_extension_manager() + self.driver = driver_factory.get_driver("fake") + + def _stop_service(self): + try: + objects.Conductor.get_by_hostname(self.context, self.hostname) + except exception.ConductorNotFound: + return + self.service.del_host() + + def _start_service(self): + self.service.init_host() + self.addCleanup(self._stop_service) + + +def mock_record_keepalive(func_or_class): + return mock.patch.object( + manager.ConductorManager, + '_conductor_service_record_keepalive', + lambda _: None)(func_or_class) diff --git a/ironic/tests/unit/conductor/test_base_manager.py b/ironic/tests/unit/conductor/test_base_manager.py new file mode 100644 index 0000000000..5ad86a5056 --- /dev/null +++ b/ironic/tests/unit/conductor/test_base_manager.py @@ -0,0 +1,187 @@ +# 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. + +"""Test class for Ironic BaseConductorManager.""" + +import eventlet +import mock +from oslo_config import cfg +from oslo_db import exception as db_exception + +from ironic.common import driver_factory +from ironic.common import exception +from ironic.conductor import base_manager +from ironic.conductor import manager +from ironic.drivers import base as drivers_base +from ironic import objects +from ironic.tests.unit.conductor import mgr_utils +from ironic.tests.unit.db import base as tests_db_base +from ironic.tests.unit.objects import utils as obj_utils + + +CONF = cfg.CONF + + +@mgr_utils.mock_record_keepalive +class StartStopTestCase(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase): + def test_start_registers_conductor(self): + self.assertRaises(exception.ConductorNotFound, + objects.Conductor.get_by_hostname, + self.context, self.hostname) + self._start_service() + res = objects.Conductor.get_by_hostname(self.context, self.hostname) + self.assertEqual(self.hostname, res['hostname']) + + def test_start_clears_conductor_locks(self): + node = obj_utils.create_test_node(self.context, + reservation=self.hostname) + node.save() + self._start_service() + node.refresh() + self.assertIsNone(node.reservation) + + def test_stop_unregisters_conductor(self): + self._start_service() + res = objects.Conductor.get_by_hostname(self.context, self.hostname) + self.assertEqual(self.hostname, res['hostname']) + self.service.del_host() + self.assertRaises(exception.ConductorNotFound, + objects.Conductor.get_by_hostname, + self.context, self.hostname) + + def test_stop_doesnt_unregister_conductor(self): + self._start_service() + res = objects.Conductor.get_by_hostname(self.context, self.hostname) + self.assertEqual(self.hostname, res['hostname']) + self.service.del_host(deregister=False) + res = objects.Conductor.get_by_hostname(self.context, self.hostname) + self.assertEqual(self.hostname, res['hostname']) + + @mock.patch.object(manager.ConductorManager, 'init_host') + def test_stop_uninitialized_conductor(self, mock_init): + self._start_service() + self.service.del_host() + + @mock.patch.object(driver_factory.DriverFactory, '__getitem__', + lambda *args: mock.MagicMock()) + def test_start_registers_driver_names(self): + init_names = ['fake1', 'fake2'] + restart_names = ['fake3', 'fake4'] + + df = driver_factory.DriverFactory() + with mock.patch.object(df._extension_manager, 'names') as mock_names: + # verify driver names are registered + self.config(enabled_drivers=init_names) + mock_names.return_value = init_names + self._start_service() + res = objects.Conductor.get_by_hostname(self.context, + self.hostname) + self.assertEqual(init_names, res['drivers']) + + # verify that restart registers new driver names + self.config(enabled_drivers=restart_names) + mock_names.return_value = restart_names + self._start_service() + res = objects.Conductor.get_by_hostname(self.context, + self.hostname) + self.assertEqual(restart_names, res['drivers']) + + @mock.patch.object(driver_factory.DriverFactory, '__getitem__') + def test_start_registers_driver_specific_tasks(self, get_mock): + init_names = ['fake1'] + expected_name = 'ironic.tests.unit.conductor.test_base_manager.task' + expected_name2 = 'ironic.tests.unit.conductor.test_base_manager.iface' + self.config(enabled_drivers=init_names) + + class TestInterface(object): + @drivers_base.driver_periodic_task(spacing=100500) + def iface(self): + pass + + class Driver(object): + core_interfaces = [] + standard_interfaces = ['iface'] + + iface = TestInterface() + + @drivers_base.driver_periodic_task(spacing=42) + def task(self, context): + pass + + obj = Driver() + self.assertTrue(obj.task._periodic_enabled) + get_mock.return_value = mock.Mock(obj=obj) + + with mock.patch.object( + driver_factory.DriverFactory()._extension_manager, + 'names') as mock_names: + mock_names.return_value = init_names + self._start_service() + tasks = dict(self.service._periodic_tasks) + self.assertEqual(obj.task, tasks[expected_name]) + self.assertEqual(obj.iface.iface, tasks[expected_name2]) + self.assertEqual(42, + self.service._periodic_spacing[expected_name]) + self.assertEqual(100500, + self.service._periodic_spacing[expected_name2]) + self.assertIn(expected_name, self.service._periodic_last_run) + self.assertIn(expected_name2, self.service._periodic_last_run) + + @mock.patch.object(driver_factory.DriverFactory, '__init__') + def test_start_fails_on_missing_driver(self, mock_df): + mock_df.side_effect = exception.DriverNotFound('test') + with mock.patch.object(self.dbapi, 'register_conductor') as mock_reg: + self.assertRaises(exception.DriverNotFound, + self.service.init_host) + self.assertTrue(mock_df.called) + self.assertFalse(mock_reg.called) + + @mock.patch.object(base_manager, 'LOG') + @mock.patch.object(driver_factory, 'DriverFactory') + def test_start_fails_on_no_driver(self, df_mock, log_mock): + driver_factory_mock = mock.MagicMock(names=[]) + df_mock.return_value = driver_factory_mock + self.assertRaises(exception.NoDriversLoaded, + self.service.init_host) + self.assertTrue(log_mock.error.called) + + @mock.patch.object(eventlet.greenpool.GreenPool, 'waitall') + def test_del_host_waits_on_workerpool(self, wait_mock): + self._start_service() + self.service.del_host() + self.assertTrue(wait_mock.called) + + +class KeepAliveTestCase(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase): + def test__conductor_service_record_keepalive(self): + self._start_service() + # avoid wasting time at the event.wait() + CONF.set_override('heartbeat_interval', 0, 'conductor') + with mock.patch.object(self.dbapi, 'touch_conductor') as mock_touch: + with mock.patch.object(self.service._keepalive_evt, + 'is_set') as mock_is_set: + mock_is_set.side_effect = [False, True] + self.service._conductor_service_record_keepalive() + mock_touch.assert_called_once_with(self.hostname) + + def test__conductor_service_record_keepalive_failed_db_conn(self): + self._start_service() + # avoid wasting time at the event.wait() + CONF.set_override('heartbeat_interval', 0, 'conductor') + with mock.patch.object(self.dbapi, 'touch_conductor') as mock_touch: + mock_touch.side_effect = [None, db_exception.DBConnectionError(), + None] + with mock.patch.object(self.service._keepalive_evt, + 'is_set') as mock_is_set: + mock_is_set.side_effect = [False, False, False, True] + self.service._conductor_service_record_keepalive() + self.assertEqual(3, mock_touch.call_count) diff --git a/ironic/tests/unit/conductor/test_manager.py b/ironic/tests/unit/conductor/test_manager.py index 6fe7542100..5644e20e20 100644 --- a/ironic/tests/unit/conductor/test_manager.py +++ b/ironic/tests/unit/conductor/test_manager.py @@ -23,9 +23,7 @@ import datetime import eventlet import mock from oslo_config import cfg -from oslo_db import exception as db_exception import oslo_messaging as messaging -from oslo_utils import strutils from oslo_utils import uuidutils from oslo_versionedobjects import base as ovo_base from oslo_versionedobjects import fields @@ -53,287 +51,8 @@ from ironic.tests.unit.objects import utils as obj_utils CONF = cfg.CONF -class _CommonMixIn(object): - @staticmethod - def _create_node(**kwargs): - attrs = {'id': 1, - 'uuid': uuidutils.generate_uuid(), - 'power_state': states.POWER_OFF, - 'target_power_state': None, - 'maintenance': False, - 'reservation': None} - attrs.update(kwargs) - node = mock.Mock(spec_set=objects.Node) - for attr in attrs: - setattr(node, attr, attrs[attr]) - return node - - def _create_task(self, node=None, node_attrs=None): - if node_attrs is None: - node_attrs = {} - if node is None: - node = self._create_node(**node_attrs) - task = mock.Mock(spec_set=['node', 'release_resources', - 'spawn_after', 'process_event']) - task.node = node - return task - - def _get_nodeinfo_list_response(self, nodes=None): - if nodes is None: - nodes = [self.node] - elif not isinstance(nodes, (list, tuple)): - nodes = [nodes] - return [tuple(getattr(n, c) for c in self.columns) for n in nodes] - - def _get_acquire_side_effect(self, task_infos): - """Helper method to generate a task_manager.acquire() side effect. - - This accepts a list of information about task mocks to return. - task_infos can be a single entity or a list. - - Each task_info can be a single entity, the task to return, or it - can be a tuple of (task, exception_to_raise_on_exit). 'task' can - be an exception to raise on __enter__. - - Examples: _get_acquire_side_effect(self, task): Yield task - _get_acquire_side_effect(self, [task, enter_exception(), - (task2, exit_exception())]) - Yield task on first call to acquire() - raise enter_exception() in __enter__ on 2nd call to - acquire() - Yield task2 on 3rd call to acquire(), but raise - exit_exception() on __exit__() - """ - tasks = [] - exit_exceptions = [] - if not isinstance(task_infos, list): - task_infos = [task_infos] - for task_info in task_infos: - if isinstance(task_info, tuple): - task, exc = task_info - else: - task = task_info - exc = None - tasks.append(task) - exit_exceptions.append(exc) - - class FakeAcquire(object): - def __init__(fa_self, context, node_id, *args, **kwargs): - # We actually verify these arguments via - # acquire_mock.call_args_list(). However, this stores the - # node_id so we can assert we're returning the correct node - # in __enter__(). - fa_self.node_id = node_id - - def __enter__(fa_self): - task = tasks.pop(0) - if isinstance(task, Exception): - raise task - # NOTE(comstud): Not ideal to throw this into - # a helper, however it's the cleanest way - # to verify we're dealing with the correct task/node. - if strutils.is_int_like(fa_self.node_id): - self.assertEqual(fa_self.node_id, task.node.id) - else: - self.assertEqual(fa_self.node_id, task.node.uuid) - return task - - def __exit__(fa_self, exc_typ, exc_val, exc_tb): - exc = exit_exceptions.pop(0) - if exc_typ is None and exc is not None: - raise exc - - return FakeAcquire - - -class _ServiceSetUpMixin(object): - def setUp(self): - super(_ServiceSetUpMixin, self).setUp() - self.hostname = 'test-host' - self.config(enabled_drivers=['fake']) - self.config(node_locked_retry_attempts=1, group='conductor') - self.config(node_locked_retry_interval=0, group='conductor') - self.service = manager.ConductorManager(self.hostname, 'test-topic') - mgr_utils.mock_the_extension_manager() - self.driver = driver_factory.get_driver("fake") - - def _stop_service(self): - try: - objects.Conductor.get_by_hostname(self.context, self.hostname) - except exception.ConductorNotFound: - return - self.service.del_host() - - def _start_service(self): - self.service.init_host() - self.addCleanup(self._stop_service) - - -def _mock_record_keepalive(func_or_class): - return mock.patch.object( - manager.ConductorManager, - '_conductor_service_record_keepalive', - lambda _: None)(func_or_class) - - -@_mock_record_keepalive -class StartStopTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): - def test_start_registers_conductor(self): - self.assertRaises(exception.ConductorNotFound, - objects.Conductor.get_by_hostname, - self.context, self.hostname) - self._start_service() - res = objects.Conductor.get_by_hostname(self.context, self.hostname) - self.assertEqual(self.hostname, res['hostname']) - - def test_start_clears_conductor_locks(self): - node = obj_utils.create_test_node(self.context, - reservation=self.hostname) - node.save() - self._start_service() - node.refresh() - self.assertIsNone(node.reservation) - - def test_stop_unregisters_conductor(self): - self._start_service() - res = objects.Conductor.get_by_hostname(self.context, self.hostname) - self.assertEqual(self.hostname, res['hostname']) - self.service.del_host() - self.assertRaises(exception.ConductorNotFound, - objects.Conductor.get_by_hostname, - self.context, self.hostname) - - def test_stop_doesnt_unregister_conductor(self): - self._start_service() - res = objects.Conductor.get_by_hostname(self.context, self.hostname) - self.assertEqual(self.hostname, res['hostname']) - self.service.del_host(deregister=False) - res = objects.Conductor.get_by_hostname(self.context, self.hostname) - self.assertEqual(self.hostname, res['hostname']) - - @mock.patch.object(manager.ConductorManager, 'init_host') - def test_stop_uninitialized_conductor(self, mock_init): - self._start_service() - self.service.del_host() - - @mock.patch.object(driver_factory.DriverFactory, '__getitem__', - lambda *args: mock.MagicMock()) - def test_start_registers_driver_names(self): - init_names = ['fake1', 'fake2'] - restart_names = ['fake3', 'fake4'] - - df = driver_factory.DriverFactory() - with mock.patch.object(df._extension_manager, 'names') as mock_names: - # verify driver names are registered - self.config(enabled_drivers=init_names) - mock_names.return_value = init_names - self._start_service() - res = objects.Conductor.get_by_hostname(self.context, - self.hostname) - self.assertEqual(init_names, res['drivers']) - - # verify that restart registers new driver names - self.config(enabled_drivers=restart_names) - mock_names.return_value = restart_names - self._start_service() - res = objects.Conductor.get_by_hostname(self.context, - self.hostname) - self.assertEqual(restart_names, res['drivers']) - - @mock.patch.object(driver_factory.DriverFactory, '__getitem__') - def test_start_registers_driver_specific_tasks(self, get_mock): - init_names = ['fake1'] - expected_task_name = 'ironic.tests.unit.conductor.test_manager.task' - expected_task_name2 = 'ironic.tests.unit.conductor.test_manager.iface' - self.config(enabled_drivers=init_names) - - class TestInterface(object): - @drivers_base.driver_periodic_task(spacing=100500) - def iface(self): - pass - - class Driver(object): - core_interfaces = [] - standard_interfaces = ['iface'] - - iface = TestInterface() - - @drivers_base.driver_periodic_task(spacing=42) - def task(self, context): - pass - - obj = Driver() - self.assertTrue(obj.task._periodic_enabled) - get_mock.return_value = mock.Mock(obj=obj) - - with mock.patch.object( - driver_factory.DriverFactory()._extension_manager, - 'names') as mock_names: - mock_names.return_value = init_names - self._start_service() - tasks = dict(self.service._periodic_tasks) - self.assertEqual(obj.task, tasks[expected_task_name]) - self.assertEqual(obj.iface.iface, tasks[expected_task_name2]) - self.assertEqual(42, - self.service._periodic_spacing[expected_task_name]) - self.assertEqual(100500, - self.service._periodic_spacing[expected_task_name2]) - self.assertIn(expected_task_name, self.service._periodic_last_run) - self.assertIn(expected_task_name2, self.service._periodic_last_run) - - @mock.patch.object(driver_factory.DriverFactory, '__init__') - def test_start_fails_on_missing_driver(self, mock_df): - mock_df.side_effect = exception.DriverNotFound('test') - with mock.patch.object(self.dbapi, 'register_conductor') as mock_reg: - self.assertRaises(exception.DriverNotFound, - self.service.init_host) - self.assertTrue(mock_df.called) - self.assertFalse(mock_reg.called) - - @mock.patch.object(manager, 'LOG') - @mock.patch.object(driver_factory, 'DriverFactory') - def test_start_fails_on_no_driver(self, df_mock, log_mock): - driver_factory_mock = mock.MagicMock(names=[]) - df_mock.return_value = driver_factory_mock - self.assertRaises(exception.NoDriversLoaded, - self.service.init_host) - self.assertTrue(log_mock.error.called) - - @mock.patch.object(eventlet.greenpool.GreenPool, 'waitall') - def test_del_host_waits_on_workerpool(self, wait_mock): - self._start_service() - self.service.del_host() - self.assertTrue(wait_mock.called) - - -class KeepAliveTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): - def test__conductor_service_record_keepalive(self): - self._start_service() - # avoid wasting time at the event.wait() - CONF.set_override('heartbeat_interval', 0, 'conductor') - with mock.patch.object(self.dbapi, 'touch_conductor') as mock_touch: - with mock.patch.object(self.service._keepalive_evt, - 'is_set') as mock_is_set: - mock_is_set.side_effect = [False, True] - self.service._conductor_service_record_keepalive() - mock_touch.assert_called_once_with(self.hostname) - - def test__conductor_service_record_keepalive_failed_db_conn(self): - self._start_service() - # avoid wasting time at the event.wait() - CONF.set_override('heartbeat_interval', 0, 'conductor') - with mock.patch.object(self.dbapi, 'touch_conductor') as mock_touch: - mock_touch.side_effect = [None, db_exception.DBConnectionError(), - None] - with mock.patch.object(self.service._keepalive_evt, - 'is_set') as mock_is_set: - mock_is_set.side_effect = [False, False, False, True] - self.service._conductor_service_record_keepalive() - self.assertEqual(3, mock_touch.call_count) - - -@_mock_record_keepalive -class ChangeNodePowerStateTestCase(_ServiceSetUpMixin, +@mgr_utils.mock_record_keepalive +class ChangeNodePowerStateTestCase(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase): def test_change_node_power_state_power_on(self): @@ -483,8 +202,9 @@ class ChangeNodePowerStateTestCase(_ServiceSetUpMixin, self.assertIsNone(node.last_error) -@_mock_record_keepalive -class UpdateNodeTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): +@mgr_utils.mock_record_keepalive +class UpdateNodeTestCase(mgr_utils.ServiceSetUpMixin, + tests_db_base.DbTestCase): def test_update_node(self): node = obj_utils.create_test_node(self.context, driver='fake', extra={'test': 'one'}) @@ -564,8 +284,9 @@ class UpdateNodeTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): self.assertEqual(existing_driver, node.driver) -@_mock_record_keepalive -class VendorPassthruTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): +@mgr_utils.mock_record_keepalive +class VendorPassthruTestCase(mgr_utils.ServiceSetUpMixin, + tests_db_base.DbTestCase): @mock.patch.object(task_manager.TaskManager, 'spawn_after') def test_vendor_passthru_async(self, mock_spawn): @@ -892,9 +613,9 @@ class VendorPassthruTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): self.assertFalse(test_method.called) -@_mock_record_keepalive +@mgr_utils.mock_record_keepalive @mock.patch.object(images, 'is_whole_disk_image') -class ServiceDoNodeDeployTestCase(_ServiceSetUpMixin, +class ServiceDoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase): def test_do_node_deploy_invalid_state(self, mock_iwdi): mock_iwdi.return_value = False @@ -1178,8 +899,8 @@ class ServiceDoNodeDeployTestCase(_ServiceSetUpMixin, self.assertFalse(node.driver_internal_info['is_whole_disk_image']) -@_mock_record_keepalive -class DoNodeDeployTearDownTestCase(_ServiceSetUpMixin, +@mgr_utils.mock_record_keepalive +class DoNodeDeployTearDownTestCase(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase): @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') @mock.patch('ironic.drivers.modules.fake.FakeDeploy.prepare') @@ -1620,8 +1341,9 @@ class DoNodeDeployTearDownTestCase(_ServiceSetUpMixin, self.assertTrue(task.node.maintenance) -@_mock_record_keepalive -class DoNodeCleanTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): +@mgr_utils.mock_record_keepalive +class DoNodeCleanTestCase(mgr_utils.ServiceSetUpMixin, + tests_db_base.DbTestCase): def setUp(self): super(DoNodeCleanTestCase, self).setUp() self.config(clean_nodes=True, group='conductor') @@ -2095,8 +1817,9 @@ class DoNodeCleanTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): self.service._get_node_next_clean_steps, task) -@_mock_record_keepalive -class DoNodeVerifyTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): +@mgr_utils.mock_record_keepalive +class DoNodeVerifyTestCase(mgr_utils.ServiceSetUpMixin, + tests_db_base.DbTestCase): @mock.patch('ironic.drivers.modules.fake.FakePower.get_power_state') @mock.patch('ironic.drivers.modules.fake.FakePower.validate') def test__do_node_verify(self, mock_validate, mock_get_power_state): @@ -2180,8 +1903,9 @@ class DoNodeVerifyTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): self.assertTrue(node.last_error) -@_mock_record_keepalive -class MiscTestCase(_ServiceSetUpMixin, _CommonMixIn, tests_db_base.DbTestCase): +@mgr_utils.mock_record_keepalive +class MiscTestCase(mgr_utils.ServiceSetUpMixin, mgr_utils.CommonMixIn, + tests_db_base.DbTestCase): def test_get_driver_known(self): self._start_service() driver = self.service._get_driver('fake') @@ -2260,8 +1984,8 @@ class MiscTestCase(_ServiceSetUpMixin, _CommonMixIn, tests_db_base.DbTestCase): last_error=mock.ANY) -@_mock_record_keepalive -class ConsoleTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): +@mgr_utils.mock_record_keepalive +class ConsoleTestCase(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase): def test_set_console_mode_worker_pool_full(self): node = obj_utils.create_test_node(self.context, driver='fake') self._start_service() @@ -2411,8 +2135,9 @@ class ConsoleTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): self.assertEqual(exception.InvalidParameterValue, exc.exc_info[0]) -@_mock_record_keepalive -class DestroyNodeTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): +@mgr_utils.mock_record_keepalive +class DestroyNodeTestCase(mgr_utils.ServiceSetUpMixin, + tests_db_base.DbTestCase): def test_destroy_node(self): self._start_service() @@ -2498,8 +2223,9 @@ class DestroyNodeTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): node.uuid) -@_mock_record_keepalive -class UpdatePortTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): +@mgr_utils.mock_record_keepalive +class UpdatePortTestCase(mgr_utils.ServiceSetUpMixin, + tests_db_base.DbTestCase): def test_update_port(self): node = obj_utils.create_test_node(self.context, driver='fake') @@ -2765,8 +2491,8 @@ class UpdatePortTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): exc.exc_info[0]) -@_mock_record_keepalive -class RaidTestCases(_ServiceSetUpMixin, tests_db_base.DbTestCase): +@mgr_utils.mock_record_keepalive +class RaidTestCases(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase): def setUp(self): super(RaidTestCases, self).setUp() @@ -3089,7 +2815,8 @@ class ManagerDoSyncPowerStateTestCase(tests_db_base.DbTestCase): @mock.patch.object(task_manager, 'acquire') @mock.patch.object(manager.ConductorManager, '_mapped_to_this_conductor') @mock.patch.object(dbapi.IMPL, 'get_nodeinfo_list') -class ManagerSyncPowerStatesTestCase(_CommonMixIn, tests_db_base.DbTestCase): +class ManagerSyncPowerStatesTestCase(mgr_utils.CommonMixIn, + tests_db_base.DbTestCase): def setUp(self): super(ManagerSyncPowerStatesTestCase, self).setUp() self.service = manager.ConductorManager('hostname', 'test-topic') @@ -3317,7 +3044,7 @@ class ManagerSyncPowerStatesTestCase(_CommonMixIn, tests_db_base.DbTestCase): @mock.patch.object(task_manager, 'acquire') @mock.patch.object(manager.ConductorManager, '_mapped_to_this_conductor') @mock.patch.object(dbapi.IMPL, 'get_nodeinfo_list') -class ManagerCheckDeployTimeoutsTestCase(_CommonMixIn, +class ManagerCheckDeployTimeoutsTestCase(mgr_utils.CommonMixIn, tests_db_base.DbTestCase): def setUp(self): super(ManagerCheckDeployTimeoutsTestCase, self).setUp() @@ -3562,7 +3289,7 @@ class ManagerCheckDeployTimeoutsTestCase(_CommonMixIn, self.assertFalse(mac_update_mock.called) -@_mock_record_keepalive +@mgr_utils.mock_record_keepalive class ManagerTestProperties(tests_db_base.DbTestCase): def setUp(self): @@ -3685,7 +3412,8 @@ class ManagerTestProperties(tests_db_base.DbTestCase): @mock.patch.object(task_manager, 'acquire') @mock.patch.object(manager.ConductorManager, '_mapped_to_this_conductor') @mock.patch.object(dbapi.IMPL, 'get_nodeinfo_list') -class ManagerSyncLocalStateTestCase(_CommonMixIn, tests_db_base.DbTestCase): +class ManagerSyncLocalStateTestCase(mgr_utils.CommonMixIn, + tests_db_base.DbTestCase): def setUp(self): super(ManagerSyncLocalStateTestCase, self).setUp() @@ -3883,8 +3611,8 @@ class StoreConfigDriveTestCase(tests_base.TestCase): self.assertEqual(expected_instance_info, self.node.instance_info) -@_mock_record_keepalive -class NodeInspectHardware(_ServiceSetUpMixin, +@mgr_utils.mock_record_keepalive +class NodeInspectHardware(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase): @mock.patch('ironic.drivers.modules.fake.FakeInspect.inspect_hardware') @@ -4018,7 +3746,7 @@ class NodeInspectHardware(_ServiceSetUpMixin, @mock.patch.object(task_manager, 'acquire') @mock.patch.object(manager.ConductorManager, '_mapped_to_this_conductor') @mock.patch.object(dbapi.IMPL, 'get_nodeinfo_list') -class ManagerCheckInspectTimeoutsTestCase(_CommonMixIn, +class ManagerCheckInspectTimeoutsTestCase(mgr_utils.CommonMixIn, tests_db_base.DbTestCase): def setUp(self): super(ManagerCheckInspectTimeoutsTestCase, self).setUp() @@ -4239,8 +3967,9 @@ class ManagerCheckInspectTimeoutsTestCase(_CommonMixIn, self.task.process_event.call_args_list) -@_mock_record_keepalive -class DestroyPortTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): +@mgr_utils.mock_record_keepalive +class DestroyPortTestCase(mgr_utils.ServiceSetUpMixin, + tests_db_base.DbTestCase): def test_destroy_port(self): node = obj_utils.create_test_node(self.context, driver='fake') @@ -4261,11 +3990,11 @@ class DestroyPortTestCase(_ServiceSetUpMixin, tests_db_base.DbTestCase): self.assertEqual(exception.NodeLocked, exc.exc_info[0]) -@_mock_record_keepalive +@mgr_utils.mock_record_keepalive @mock.patch.object(manager.ConductorManager, '_fail_if_in_state') @mock.patch.object(manager.ConductorManager, '_mapped_to_this_conductor') @mock.patch.object(dbapi.IMPL, 'get_offline_conductors') -class ManagerCheckDeployingStatusTestCase(_ServiceSetUpMixin, +class ManagerCheckDeployingStatusTestCase(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase): def setUp(self): super(ManagerCheckDeployingStatusTestCase, self).setUp() @@ -4463,8 +4192,8 @@ class TestIndirectionApiConductor(tests_db_base.DbTestCase): target_version='1.0', version_manifest=fake_version_manifest) -@_mock_record_keepalive -class DoNodeTakeOverTestCase(_ServiceSetUpMixin, +@mgr_utils.mock_record_keepalive +class DoNodeTakeOverTestCase(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase): @mock.patch('ironic.drivers.modules.fake.FakeConsole.start_console')