]> xenbits.xensource.com Git - osstest/openstack-nova.git/commitdiff
Add a CellDatabases test fixture
authormelanie witt <melwittt@gmail.com>
Fri, 18 Nov 2016 17:18:24 +0000 (17:18 +0000)
committerDan Smith <dansmith@redhat.com>
Tue, 6 Dec 2016 18:08:35 +0000 (10:08 -0800)
As we progress with the Cells v2 scheduling interaction work, we need
to be able to have switching between multiple databases work in our
functional tests. The existing Database fixture doesn't work in this
case because each connection switch via target_cell results in a new,
empty sqlite database, and main_context_manager is global in the DB
API and always points at the same sqlite database.

This adds a fixture that creates a new sqlite database per cell
database, runs migrations, and keeps track of the databases using
identifiers provided when cell databases are added to the fixture.
It patches get_context_manager, create_context_manager, and target_cell
to return the matching database connection according to identifier,
simulating switching between multiple databases in a single test.

Change-Id: I00748cbbb682813987a2ad8c69948f71223daee7

nova/tests/fixtures.py
nova/tests/functional/db/test_connection_switch.py

index a19b051139b9ca258130e038d6bc66caf512749d..fb30db4fc4768a5c33264e78b9f285bbba3b3b27 100644 (file)
 """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
@@ -247,6 +249,136 @@ class DatabasePoisonFixture(fixtures.Fixture):
                         '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.
index 70f524c2dfe96ef66c664d4ec455cdb9c1dc7b91..41453e57d08e98ba66417796361a3f15698632b0 100644 (file)
@@ -18,7 +18,7 @@ from nova import context
 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):
@@ -31,7 +31,7 @@ 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:
@@ -64,3 +64,63 @@ class ConnectionSwitchTestCase(test.TestCase):
         # 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)