From a47f093732ec32bf135abd2ee95603c55c0e334d Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Wed, 25 Nov 2015 18:29:24 +0100 Subject: [PATCH] Split BaseConductorManager from ConductorManager This is the second patch in a chain of refactoring patches in preparation for moving to Futurist library. See blueprint futurist for details. This patch moves common conductor initialization and lifetime code, and some common conductor methods to a separate class BaseConductorManager. I expect most of the work in blueprint futurist to touch this class. Implements: blueprint futurist Change-Id: If4f018d6e0473e62134a0427a0ad0ea733c10ea4 --- ironic/conductor/base_manager.py | 318 +++++++++++++++ ironic/conductor/manager.py | 281 +------------- ironic/tests/unit/conductor/mgr_utils.py | 130 +++++++ .../tests/unit/conductor/test_base_manager.py | 187 +++++++++ ironic/tests/unit/conductor/test_manager.py | 365 +++--------------- 5 files changed, 685 insertions(+), 596 deletions(-) create mode 100644 ironic/conductor/base_manager.py create mode 100644 ironic/tests/unit/conductor/test_base_manager.py 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 35e5b1e032..47f91f7cd1 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, @@ -1451,42 +1271,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. @@ -2023,65 +1807,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 4e745b8ebd..9b6026c393 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') @@ -2081,8 +1803,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): @@ -2166,8 +1889,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') @@ -2246,8 +1970,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() @@ -2397,8 +2121,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() @@ -2484,8 +2209,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') @@ -2751,8 +2477,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() @@ -3075,7 +2801,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') @@ -3303,7 +3030,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() @@ -3548,7 +3275,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): @@ -3671,7 +3398,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() @@ -3869,8 +3597,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') @@ -4004,7 +3732,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() @@ -4225,8 +3953,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') @@ -4247,11 +3976,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() @@ -4449,8 +4178,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')