summaryrefslogtreecommitdiff
path: root/nova/tests/fixtures/nova.py
diff options
context:
space:
mode:
Diffstat (limited to 'nova/tests/fixtures/nova.py')
-rw-r--r--nova/tests/fixtures/nova.py375
1 files changed, 290 insertions, 85 deletions
diff --git a/nova/tests/fixtures/nova.py b/nova/tests/fixtures/nova.py
index ef873f6654..abfc3ecc6c 100644
--- a/nova/tests/fixtures/nova.py
+++ b/nova/tests/fixtures/nova.py
@@ -20,14 +20,17 @@ import collections
import contextlib
from contextlib import contextmanager
import functools
+from importlib.abc import MetaPathFinder
import logging as std_logging
import os
+import sys
+import time
+from unittest import mock
import warnings
import eventlet
import fixtures
import futurist
-import mock
from openstack import service_description
from oslo_concurrency import lockutils
from oslo_config import cfg
@@ -62,6 +65,7 @@ from nova.scheduler import weights
from nova import service
from nova.tests.functional.api import client
from nova import utils
+from nova.virt import node
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
@@ -451,6 +455,13 @@ class CellDatabases(fixtures.Fixture):
# yield to do the actual work. We can do schedulable things
# here and not exclude other threads from making progress.
# If an exception is raised, we capture that and save it.
+ # Note that it is possible that another thread has changed the
+ # global state (step #2) after we released the writer lock but
+ # before we acquired the reader lock. If this happens, we will
+ # detect the global state change and retry step #2 a limited number
+ # of times. If we happen to race repeatedly with another thread and
+ # exceed our retry limit, we will give up and raise a RuntimeError,
+ # which will fail the test.
# 4. If we changed state in #2, we need to change it back. So we grab
# a writer lock again and do that.
# 5. Finally, if an exception was raised in #3 while state was
@@ -469,29 +480,47 @@ class CellDatabases(fixtures.Fixture):
raised_exc = None
- with self._cell_lock.write_lock():
- if cell_mapping is not None:
- # This assumes the next local DB access is the same cell that
- # was targeted last time.
- self._last_ctxt_mgr = desired
+ def set_last_ctxt_mgr():
+ with self._cell_lock.write_lock():
+ if cell_mapping is not None:
+ # This assumes the next local DB access is the same cell
+ # that was targeted last time.
+ self._last_ctxt_mgr = desired
- with self._cell_lock.read_lock():
- if self._last_ctxt_mgr != desired:
- # NOTE(danms): This is unlikely to happen, but it's possible
- # another waiting writer changed the state between us letting
- # it go and re-acquiring as a reader. If lockutils supported
- # upgrading and downgrading locks, this wouldn't be a problem.
- # Regardless, assert that it is still as we left it here
- # so we don't hit the wrong cell. If this becomes a problem,
- # we just need to retry the write section above until we land
- # here with the cell we want.
- raise RuntimeError('Global DB state changed underneath us')
+ # Set last context manager to the desired cell's context manager.
+ set_last_ctxt_mgr()
+ # Retry setting the last context manager if we detect that a writer
+ # changed global DB state before we take the read lock.
+ for retry_time in range(0, 3):
try:
- with self._real_target_cell(context, cell_mapping) as ccontext:
- yield ccontext
- except Exception as exc:
- raised_exc = exc
+ with self._cell_lock.read_lock():
+ if self._last_ctxt_mgr != desired:
+ # NOTE(danms): This is unlikely to happen, but it's
+ # possible another waiting writer changed the state
+ # between us letting it go and re-acquiring as a
+ # reader. If lockutils supported upgrading and
+ # downgrading locks, this wouldn't be a problem.
+ # Regardless, assert that it is still as we left it
+ # here so we don't hit the wrong cell. If this becomes
+ # a problem, we just need to retry the write section
+ # above until we land here with the cell we want.
+ raise RuntimeError(
+ 'Global DB state changed underneath us')
+ try:
+ with self._real_target_cell(
+ context, cell_mapping
+ ) as ccontext:
+ yield ccontext
+ except Exception as exc:
+ raised_exc = exc
+ # Leave the retry loop after calling target_cell
+ break
+ except RuntimeError:
+ # Give other threads a chance to make progress, increasing the
+ # wait time between attempts.
+ time.sleep(retry_time)
+ set_last_ctxt_mgr()
with self._cell_lock.write_lock():
# Once we have returned from the context, we need
@@ -537,11 +566,10 @@ class CellDatabases(fixtures.Fixture):
call_monitor_timeout=None):
"""Mirror rpc.get_client() but with our special sauce."""
serializer = CheatingSerializer(serializer)
- return messaging.RPCClient(rpc.TRANSPORT,
- target,
- version_cap=version_cap,
- serializer=serializer,
- call_monitor_timeout=call_monitor_timeout)
+ return messaging.get_rpc_client(rpc.TRANSPORT, target,
+ version_cap=version_cap,
+ serializer=serializer,
+ call_monitor_timeout=call_monitor_timeout)
def add_cell_database(self, connection_str, default=False):
"""Add a cell database to the fixture.
@@ -780,7 +808,7 @@ class WarningsFixture(fixtures.Fixture):
"""Filters out warnings during test runs."""
def setUp(self):
- super(WarningsFixture, self).setUp()
+ super().setUp()
self._original_warning_filters = warnings.filters[:]
@@ -793,15 +821,19 @@ class WarningsFixture(fixtures.Fixture):
# forward on is_admin, the deprecation is definitely really premature.
warnings.filterwarnings(
'ignore',
- message='Policy enforcement is depending on the value of is_admin.'
- ' This key is deprecated. Please update your policy '
- 'file to use the standard policy values.')
+ message=(
+ 'Policy enforcement is depending on the value of is_admin. '
+ 'This key is deprecated. Please update your policy '
+ 'file to use the standard policy values.'
+ ),
+ )
# NOTE(mriedem): Ignore scope check UserWarnings from oslo.policy.
warnings.filterwarnings(
'ignore',
message="Policy .* failed scope check",
- category=UserWarning)
+ category=UserWarning,
+ )
# NOTE(gibi): The UUIDFields emits a warning if the value is not a
# valid UUID. Let's escalate that to an exception in the test to
@@ -813,70 +845,36 @@ class WarningsFixture(fixtures.Fixture):
# how to handle (or isn't given a fallback callback).
warnings.filterwarnings(
'error',
- message="Cannot convert <oslo_db.sqlalchemy.enginefacade"
- "._Default object at ",
- category=UserWarning)
-
- warnings.filterwarnings(
- 'error', message='Evaluating non-mapped column expression',
- category=sqla_exc.SAWarning)
+ message=(
+ 'Cannot convert <oslo_db.sqlalchemy.enginefacade._Default '
+ 'object at '
+ ),
+ category=UserWarning,
+ )
# Enable deprecation warnings for nova itself to capture upcoming
# SQLAlchemy changes
warnings.filterwarnings(
'ignore',
- category=sqla_exc.SADeprecationWarning)
+ category=sqla_exc.SADeprecationWarning,
+ )
warnings.filterwarnings(
'error',
module='nova',
- category=sqla_exc.SADeprecationWarning)
-
- # ...but filter everything out until we get around to fixing them
- # TODO(stephenfin): Fix all of these
-
- warnings.filterwarnings(
- 'ignore',
- module='nova',
- message=r'The current statement is being autocommitted .*',
- category=sqla_exc.SADeprecationWarning)
-
- warnings.filterwarnings(
- 'ignore',
- module='nova',
- message=r'The Column.copy\(\) method is deprecated .*',
- category=sqla_exc.SADeprecationWarning)
-
- warnings.filterwarnings(
- 'ignore',
- module='nova',
- message=r'The Connection.connect\(\) method is considered .*',
- category=sqla_exc.SADeprecationWarning)
-
- warnings.filterwarnings(
- 'ignore',
- module='nova',
- message=r'Using strings to indicate column or relationship .*',
- category=sqla_exc.SADeprecationWarning)
+ category=sqla_exc.SADeprecationWarning,
+ )
- warnings.filterwarnings(
- 'ignore',
- module='nova',
- message=r'Using strings to indicate relationship names .*',
- category=sqla_exc.SADeprecationWarning)
+ # Enable general SQLAlchemy warnings also to ensure we're not doing
+ # silly stuff. It's possible that we'll need to filter things out here
+ # with future SQLAlchemy versions, but that's a good thing
warnings.filterwarnings(
- 'ignore',
+ 'error',
module='nova',
- message=r'Invoking and_\(\) without arguments is deprecated, .*',
- category=sqla_exc.SADeprecationWarning)
-
- # TODO(stephenfin): Remove once we fix this in placement 5.0.2 or 6.0.0
- warnings.filterwarnings(
- 'ignore',
- message='Implicit coercion of SELECT and textual SELECT .*',
- category=sqla_exc.SADeprecationWarning)
+ category=sqla_exc.SAWarning,
+ )
self.addCleanup(self._reset_warning_filters)
@@ -1006,9 +1004,15 @@ class OSAPIFixture(fixtures.Fixture):
self.api = client.TestOpenStackClient(
'fake', base_url, project_id=self.project_id,
roles=['reader', 'member'])
+ self.alternative_api = client.TestOpenStackClient(
+ 'fake', base_url, project_id=self.project_id,
+ roles=['reader', 'member'])
self.admin_api = client.TestOpenStackClient(
'admin', base_url, project_id=self.project_id,
roles=['reader', 'member', 'admin'])
+ self.alternative_admin_api = client.TestOpenStackClient(
+ 'admin', base_url, project_id=self.project_id,
+ roles=['reader', 'member', 'admin'])
self.reader_api = client.TestOpenStackClient(
'reader', base_url, project_id=self.project_id,
roles=['reader'])
@@ -1104,9 +1108,9 @@ class PoisonFunctions(fixtures.Fixture):
# Don't poison the function if it's already mocked
import nova.virt.libvirt.host
if not isinstance(nova.virt.libvirt.host.Host._init_events, mock.Mock):
- self.useFixture(fixtures.MockPatch(
+ self.useFixture(fixtures.MonkeyPatch(
'nova.virt.libvirt.host.Host._init_events',
- side_effect=evloop))
+ evloop))
class IndirectionAPIFixture(fixtures.Fixture):
@@ -1314,6 +1318,77 @@ class PrivsepFixture(fixtures.Fixture):
nova.privsep.sys_admin_pctxt, 'client_mode', False))
+class CGroupsFixture(fixtures.Fixture):
+ """Mocks checks made for available subsystems on the host's control group.
+
+ The fixture mocks all calls made on the host to verify the capabilities
+ provided by its kernel. Through this, one can simulate the underlying
+ system hosts work on top of and have tests react to expected outcomes from
+ such.
+
+ Use sample:
+ >>> cgroups = self.useFixture(CGroupsFixture())
+ >>> cgroups = self.useFixture(CGroupsFixture(version=2))
+ >>> cgroups = self.useFixture(CGroupsFixture())
+ ... cgroups.version = 2
+
+ :attr version: Arranges mocks to simulate the host interact with nova
+ following the given version of cgroups.
+ Available values are:
+ - 0: All checks related to cgroups will return False.
+ - 1: Checks related to cgroups v1 will return True.
+ - 2: Checks related to cgroups v2 will return True.
+ Defaults to 1.
+ """
+
+ def __init__(self, version=1):
+ self._cpuv1 = None
+ self._cpuv2 = None
+
+ self._version = version
+
+ @property
+ def version(self):
+ return self._version
+
+ @version.setter
+ def version(self, value):
+ self._version = value
+ self._update_mocks()
+
+ def setUp(self):
+ super().setUp()
+ self._cpuv1 = self.useFixture(fixtures.MockPatch(
+ 'nova.virt.libvirt.host.Host._has_cgroupsv1_cpu_controller')).mock
+ self._cpuv2 = self.useFixture(fixtures.MockPatch(
+ 'nova.virt.libvirt.host.Host._has_cgroupsv2_cpu_controller')).mock
+ self._update_mocks()
+
+ def _update_mocks(self):
+ if not self._cpuv1:
+ return
+
+ if not self._cpuv2:
+ return
+
+ if self.version == 0:
+ self._cpuv1.return_value = False
+ self._cpuv2.return_value = False
+ return
+
+ if self.version == 1:
+ self._cpuv1.return_value = True
+ self._cpuv2.return_value = False
+ return
+
+ if self.version == 2:
+ self._cpuv1.return_value = False
+ self._cpuv2.return_value = True
+ return
+
+ raise ValueError(f"Unknown cgroups version: '{self.version}'.")
+
+
class NoopQuotaDriverFixture(fixtures.Fixture):
"""A fixture to run tests using the NoopQuotaDriver.
@@ -1459,7 +1534,7 @@ class AvailabilityZoneFixture(fixtures.Fixture):
``get_availability_zones``.
``get_instance_availability_zone`` will return the availability_zone
- requested when creating a server otherwise the instance.availabilty_zone
+ requested when creating a server otherwise the instance.availability_zone
or default_availability_zone is returned.
"""
@@ -1611,7 +1686,11 @@ class GenericPoisonFixture(fixtures.Fixture):
current = __import__(components[0], {}, {})
for component in components[1:]:
current = getattr(current, component)
- if not isinstance(getattr(current, attribute), mock.Mock):
+
+ # NOTE(stephenfin): There are a couple of mock libraries in use
+ # (including mocked versions of mock from oslotest) so we can't
+ # use isinstance checks here
+ if 'mock' not in str(type(getattr(current, attribute))):
self.useFixture(fixtures.MonkeyPatch(
meth, poison_configure(meth, why)))
except ImportError:
@@ -1733,3 +1812,129 @@ class ReaderWriterLock(lockutils.ReaderWriterLock):
'threading.current_thread', eventlet.getcurrent)
with mpatch if eventlet_patched else contextlib.ExitStack():
super().__init__(*a, **kw)
+
+
+class SysFsPoisonFixture(fixtures.Fixture):
+
+ def inject_poison(self, module_name, function_name):
+ import importlib
+ mod = importlib.import_module(module_name)
+ orig_f = getattr(mod, function_name)
+ if (
+ isinstance(orig_f, mock.Mock) or
+ # FIXME(gibi): Is this a bug in unittest.mock? If I remove this
+ # then LibvirtReportSevTraitsTests fails as builtins.open is mocked
+ # there at import time via @test.patch_open. That injects a
+ # MagicMock instance to builtins.open which we check here against
+ # Mock (or even MagicMock) via isinstance and that check says it is
+ # not a mock. More interestingly I cannot reproduce the same
+ # issue with @test.patch_open and isinstance in a simple python
+ # interpreter. So to make progress I'm checking the class name
+ # here instead as that works.
+ orig_f.__class__.__name__ == "MagicMock"
+ ):
+ # the target is already mocked, probably via a decorator run at
+ # import time, so we don't need to inject our poison
+ return
+
+ full_name = module_name + "." + function_name
+
+ def toxic_wrapper(*args, **kwargs):
+ path = args[0]
+ if isinstance(path, bytes):
+ pattern = b'/sys'
+ elif isinstance(path, str):
+ pattern = '/sys'
+ else:
+ # we ignore the rest of the potential pathlike types for now
+ pattern = None
+
+ if pattern and path.startswith(pattern):
+ raise Exception(
+ 'This test invokes %s on %s. It is bad, you '
+ 'should mock it.'
+ % (full_name, path)
+ )
+ else:
+ return orig_f(*args, **kwargs)
+
+ self.useFixture(fixtures.MonkeyPatch(full_name, toxic_wrapper))
+
+ def setUp(self):
+ super().setUp()
+ self.inject_poison("os.path", "isdir")
+ self.inject_poison("builtins", "open")
+ self.inject_poison("glob", "iglob")
+ self.inject_poison("os", "listdir")
+ self.inject_poison("glob", "glob")
+ # TODO(gibi): Would be good to poison these too but that makes
+ # a bunch of test to fail
+ # self.inject_poison("os.path", "exists")
+ # self.inject_poison("os", "stat")
+
+
+class ImportModulePoisonFixture(fixtures.Fixture):
+ """Poison imports of modules unsuitable for the test environment.
+
+ Examples are guestfs and libvirt. Ordinarily, these would not be installed
+ in the test environment but if they _are_ present, it can result in
+ actual calls to libvirt, for example, which could cause tests to fail.
+
+ This fixture will inspect module imports and if they are in the disallowed
+ list, it will fail the test with a helpful message about mocking needed in
+ the test.
+ """
+
+ class ForbiddenModules(MetaPathFinder):
+ def __init__(self, test, modules):
+ super().__init__()
+ self.test = test
+ self.modules = modules
+
+ def find_spec(self, fullname, path, target=None):
+ if fullname in self.modules:
+ self.test.fail_message = (
+ f"This test imports the '{fullname}' module, which it "
+ f'should not in the test environment. Please add '
+ f'appropriate mocking to this test.'
+ )
+ raise ImportError(fullname)
+
+ def __init__(self, module_names):
+ self.module_names = module_names
+ self.fail_message = ''
+ if isinstance(module_names, str):
+ self.module_names = {module_names}
+ self.meta_path_finder = self.ForbiddenModules(self, self.module_names)
+
+ def setUp(self):
+ super().setUp()
+ self.addCleanup(self.cleanup)
+ sys.meta_path.insert(0, self.meta_path_finder)
+
+ def cleanup(self):
+ sys.meta_path.remove(self.meta_path_finder)
+ # We use a flag and check it during the cleanup phase to fail the test
+ # if needed. This is done because some module imports occur inside of a
+ # try-except block that ignores all exceptions, so raising an exception
+ # there (which is also what self.assert* and self.fail() do underneath)
+ # will not work to cause a failure in the test.
+ if self.fail_message:
+ raise ImportError(self.fail_message)
+
+
+class ComputeNodeIdFixture(fixtures.Fixture):
+ def setUp(self):
+ super().setUp()
+
+ node.LOCAL_NODE_UUID = None
+ self.useFixture(fixtures.MockPatch(
+ 'nova.virt.node.read_local_node_uuid',
+ lambda: None))
+ self.useFixture(fixtures.MockPatch(
+ 'nova.virt.node.write_local_node_uuid',
+ lambda uuid: None))
+ self.useFixture(fixtures.MockPatch(
+ 'nova.compute.manager.ComputeManager.'
+ '_ensure_existing_node_identity',
+ mock.DEFAULT))