From: melanie witt Date: Fri, 18 Nov 2016 17:18:24 +0000 (+0000) Subject: Add a CellDatabases test fixture X-Git-Url: http://xenbits.xensource.com/gitweb?a=commitdiff_plain;h=f9205894a25f1e4ca2d55c56e30ed6c8b8556938;p=osstest%2Fopenstack-nova.git Add a CellDatabases test fixture 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 --- diff --git a/nova/tests/fixtures.py b/nova/tests/fixtures.py index a19b051139..fb30db4fc4 100644 --- a/nova/tests/fixtures.py +++ b/nova/tests/fixtures.py @@ -17,12 +17,14 @@ """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. diff --git a/nova/tests/functional/db/test_connection_switch.py b/nova/tests/functional/db/test_connection_switch.py index 70f524c2df..41453e57d0 100644 --- a/nova/tests/functional/db/test_connection_switch.py +++ b/nova/tests/functional/db/test_connection_switch.py @@ -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)