"""Fixtures for Nova tests."""
from __future__ import absolute_import
+from contextlib import contextmanager
import logging as std_logging
import os
import warnings
import fixtures
import mock
+from oslo_concurrency import lockutils
from oslo_config import cfg
from oslo_db.sqlalchemy import enginefacade
from oslo_messaging import conffixture as messaging_conffixture
'do use the database and cause failures later.')
+class CellDatabases(fixtures.Fixture):
+ """Create per-cell databases for testing.
+
+ How to use::
+
+ fix = CellDatabases()
+ fix.add_cell_database('connection1')
+ fix.add_cell_database('connection2', default=True)
+ self.useFixture(fix)
+
+ Passing default=True tells the fixture which database should
+ be given to code that doesn't target a specific cell.
+ """
+ def __init__(self):
+ self._ctxt_mgrs = {}
+ self._last_ctxt_mgr = None
+ self._default_ctxt_mgr = None
+
+ # NOTE(danms): Use a ReaderWriterLock to synchronize our
+ # global database muckery here. If we change global db state
+ # to point to a cell, we need to take an exclusive lock to
+ # prevent any other calls to get_context_manager() until we
+ # reset to the default.
+ self._cell_lock = lockutils.ReaderWriterLock()
+
+ def _cache_schema(self, connection_str):
+ # NOTE(melwitt): See the regular Database fixture for why
+ # we do this.
+ global DB_SCHEMA
+ if not DB_SCHEMA['main']:
+ ctxt_mgr = self._ctxt_mgrs[connection_str]
+ engine = ctxt_mgr.get_legacy_facade().get_engine()
+ conn = engine.connect()
+ migration.db_sync(database='main')
+ DB_SCHEMA['main'] = "".join(line for line
+ in conn.connection.iterdump())
+ engine.dispose()
+
+ @contextmanager
+ def _wrap_target_cell(self, context, cell_mapping):
+ with self._cell_lock.write_lock():
+ ctxt_mgr = self._ctxt_mgrs[cell_mapping.database_connection]
+ # This assumes the next local DB access is the same cell that
+ # was targeted last time.
+ self._last_ctxt_mgr = ctxt_mgr
+ try:
+ with self._real_target_cell(context, cell_mapping) as ccontext:
+ yield ccontext
+ finally:
+ # Once we have returned from the context, we need
+ # to restore the default context manager for any
+ # subsequent calls
+ self._last_ctxt_mgr = self._default_ctxt_mgr
+
+ def _wrap_create_context_manager(self, connection=None):
+ ctxt_mgr = self._ctxt_mgrs[connection]
+ return ctxt_mgr
+
+ def _wrap_get_context_manager(self, context):
+ # NOTE(melwitt): This is a hack to try to deal with
+ # local accesses i.e. non target_cell accesses.
+ with self._cell_lock.read_lock():
+ return self._last_ctxt_mgr
+
+ def add_cell_database(self, connection_str, default=False):
+ """Add a cell database to the fixture.
+
+ :param connection_str: An identifier used to represent the connection
+ string for this database. It should match the database_connection field
+ in the corresponding CellMapping.
+ """
+
+ # NOTE(danms): Create a new context manager for the cell, which
+ # will house the sqlite:// connection for this cell's in-memory
+ # database. Store/index it by the connection string, which is
+ # how we identify cells in CellMapping.
+ ctxt_mgr = session.create_context_manager()
+ self._ctxt_mgrs[connection_str] = ctxt_mgr
+
+ # NOTE(melwitt): The first DB access through service start is
+ # local so this initializes _last_ctxt_mgr for that and needs
+ # to be a compute cell.
+ self._last_ctxt_mgr = ctxt_mgr
+
+ # NOTE(danms): Record which context manager should be the default
+ # so we can restore it when we return from target-cell contexts.
+ # If none has been provided yet, store the current one in case
+ # no default is ever specified.
+ if self._default_ctxt_mgr is None or default:
+ self._default_ctxt_mgr = ctxt_mgr
+
+ def get_context_manager(context):
+ return ctxt_mgr
+
+ # NOTE(danms): This is a temporary MonkeyPatch just to get
+ # a new database created with the schema we need and the
+ # context manager for it stashed.
+ with fixtures.MonkeyPatch(
+ 'nova.db.sqlalchemy.api.get_context_manager',
+ get_context_manager):
+ self._cache_schema(connection_str)
+ engine = ctxt_mgr.get_legacy_facade().get_engine()
+ engine.dispose()
+ conn = engine.connect()
+ conn.connection.executescript(DB_SCHEMA['main'])
+
+ def setUp(self):
+ super(CellDatabases, self).setUp()
+ self.addCleanup(self.cleanup)
+ self._real_target_cell = context.target_cell
+
+ # NOTE(danms): These context managers are in place for the
+ # duration of the test (unlike the temporary ones above) and
+ # provide the actual "runtime" switching of connections for us.
+ self.useFixture(fixtures.MonkeyPatch(
+ 'nova.db.sqlalchemy.api.create_context_manager',
+ self._wrap_create_context_manager))
+ self.useFixture(fixtures.MonkeyPatch(
+ 'nova.db.sqlalchemy.api.get_context_manager',
+ self._wrap_get_context_manager))
+ self.useFixture(fixtures.MonkeyPatch(
+ 'nova.context.target_cell',
+ self._wrap_target_cell))
+
+ def cleanup(self):
+ for ctxt_mgr in self._ctxt_mgrs.values():
+ engine = ctxt_mgr.get_legacy_facade().get_engine()
+ engine.dispose()
+
+
class Database(fixtures.Fixture):
def __init__(self, database='main', connection=None):
"""Create a database fixture.
from nova import exception
from nova import objects
from nova import test
-from nova.tests import fixtures
+from nova.tests import fixtures as nova_fixtures
class ConnectionSwitchTestCase(test.TestCase):
# Use a file-based sqlite database so data will persist across new
# connections
# The 'main' database connection will stay open, so in-memory is fine
- self.useFixture(fixtures.Database(connection=self.fake_conn))
+ self.useFixture(nova_fixtures.Database(connection=self.fake_conn))
def cleanup(self):
try:
# Verify the instance isn't found in the main database
self.assertRaises(exception.InstanceNotFound,
objects.Instance.get_by_uuid, ctxt, uuid)
+
+
+class CellDatabasesTestCase(test.NoDBTestCase):
+ USES_DB_SELF = True
+
+ def setUp(self):
+ super(CellDatabasesTestCase, self).setUp()
+ self.useFixture(nova_fixtures.Database(database='api'))
+ fix = nova_fixtures.CellDatabases()
+ fix.add_cell_database('blah')
+ fix.add_cell_database('wat')
+ self.useFixture(fix)
+
+ def test_cell_dbs(self):
+ ctxt = context.RequestContext('fake-user', 'fake-project')
+ mapping1 = objects.CellMapping(context=ctxt,
+ uuid=uuidutils.generate_uuid(),
+ database_connection='blah',
+ transport_url='none:///')
+ mapping2 = objects.CellMapping(context=ctxt,
+ uuid=uuidutils.generate_uuid(),
+ database_connection='wat',
+ transport_url='none:///')
+ mapping1.create()
+ mapping2.create()
+
+ # Create an instance and read it from cell1
+ uuid = uuidutils.generate_uuid()
+ with context.target_cell(ctxt, mapping1):
+ instance = objects.Instance(context=ctxt, uuid=uuid,
+ project_id='fake-project')
+ instance.create()
+
+ inst = objects.Instance.get_by_uuid(ctxt, uuid)
+ self.assertEqual(uuid, inst.uuid)
+
+ # Make sure it can't be read from cell2
+ with context.target_cell(ctxt, mapping2):
+ self.assertRaises(exception.InstanceNotFound,
+ objects.Instance.get_by_uuid, ctxt, uuid)
+
+ # Make sure it can still be read from cell1
+ with context.target_cell(ctxt, mapping1):
+ inst = objects.Instance.get_by_uuid(ctxt, uuid)
+ self.assertEqual(uuid, inst.uuid)
+
+ # Create an instance and read it from cell2
+ uuid = uuidutils.generate_uuid()
+ with context.target_cell(ctxt, mapping2):
+ instance = objects.Instance(context=ctxt, uuid=uuid,
+ project_id='fake-project')
+ instance.create()
+
+ inst = objects.Instance.get_by_uuid(ctxt, uuid)
+ self.assertEqual(uuid, inst.uuid)
+
+ # Make sure it can't be read from cell1
+ with context.target_cell(ctxt, mapping1):
+ self.assertRaises(exception.InstanceNotFound,
+ objects.Instance.get_by_uuid, ctxt, uuid)