|
14 | 14 | # License for the specific language governing permissions and limitations
|
15 | 15 | # under the License.
|
16 | 16 |
|
17 |
| -"""Database setup and migration commands.""" |
| 17 | +import os |
18 | 18 |
|
19 |
| -from nova.db.sqlalchemy import migration |
| 19 | +from migrate import exceptions as versioning_exceptions |
| 20 | +from migrate.versioning import api as versioning_api |
| 21 | +from migrate.versioning.repository import Repository |
| 22 | +from oslo_log import log as logging |
| 23 | +import sqlalchemy |
20 | 24 |
|
21 |
| -IMPL = migration |
| 25 | +from nova.db.sqlalchemy import api as db_session |
| 26 | +from nova import exception |
| 27 | +from nova.i18n import _ |
| 28 | + |
| 29 | +INIT_VERSION = {} |
| 30 | +INIT_VERSION['main'] = 401 |
| 31 | +INIT_VERSION['api'] = 66 |
| 32 | +_REPOSITORY = {} |
| 33 | + |
| 34 | +LOG = logging.getLogger(__name__) |
| 35 | + |
| 36 | + |
| 37 | +def get_engine(database='main', context=None): |
| 38 | + if database == 'main': |
| 39 | + return db_session.get_engine(context=context) |
| 40 | + |
| 41 | + if database == 'api': |
| 42 | + return db_session.get_api_engine() |
| 43 | + |
| 44 | + |
| 45 | +def find_migrate_repo(database='main'): |
| 46 | + """Get the path for the migrate repository.""" |
| 47 | + global _REPOSITORY |
| 48 | + rel_path = os.path.join('sqlalchemy', 'migrate_repo') |
| 49 | + if database == 'api': |
| 50 | + rel_path = os.path.join('sqlalchemy', 'api_migrations', 'migrate_repo') |
| 51 | + path = os.path.join(os.path.abspath(os.path.dirname(__file__)), rel_path) |
| 52 | + assert os.path.exists(path) |
| 53 | + if _REPOSITORY.get(database) is None: |
| 54 | + _REPOSITORY[database] = Repository(path) |
| 55 | + return _REPOSITORY[database] |
22 | 56 |
|
23 | 57 |
|
24 | 58 | def db_sync(version=None, database='main', context=None):
|
25 | 59 | """Migrate the database to `version` or the most recent version."""
|
26 |
| - return IMPL.db_sync(version=version, database=database, context=context) |
| 60 | + if version is not None: |
| 61 | + try: |
| 62 | + version = int(version) |
| 63 | + except ValueError: |
| 64 | + raise exception.NovaException(_("version should be an integer")) |
| 65 | + |
| 66 | + current_version = db_version(database, context=context) |
| 67 | + repository = find_migrate_repo(database) |
| 68 | + engine = get_engine(database, context=context) |
| 69 | + if version is None or version > current_version: |
| 70 | + return versioning_api.upgrade(engine, repository, version) |
| 71 | + else: |
| 72 | + return versioning_api.downgrade(engine, repository, version) |
27 | 73 |
|
28 | 74 |
|
29 | 75 | def db_version(database='main', context=None):
|
30 | 76 | """Display the current database version."""
|
31 |
| - return IMPL.db_version(database=database, context=context) |
| 77 | + repository = find_migrate_repo(database) |
| 78 | + |
| 79 | + # NOTE(mdbooth): This is a crude workaround for races in _db_version. The 2 |
| 80 | + # races we have seen in practise are: |
| 81 | + # * versioning_api.db_version() fails because the migrate_version table |
| 82 | + # doesn't exist, but meta.tables subsequently contains tables because |
| 83 | + # another thread has already started creating the schema. This results in |
| 84 | + # the 'Essex' error. |
| 85 | + # * db_version_control() fails with pymysql.error.InternalError(1050) |
| 86 | + # (Create table failed) because of a race in sqlalchemy-migrate's |
| 87 | + # ControlledSchema._create_table_version, which does: |
| 88 | + # if not table.exists(): table.create() |
| 89 | + # This means that it doesn't raise the advertised |
| 90 | + # DatabaseAlreadyControlledError, which we could have handled explicitly. |
| 91 | + # |
| 92 | + # I believe the correct fix should be: |
| 93 | + # * Delete the Essex-handling code as unnecessary complexity which nobody |
| 94 | + # should still need. |
| 95 | + # * Fix the races in sqlalchemy-migrate such that version_control() always |
| 96 | + # raises a well-defined error, and then handle that error here. |
| 97 | + # |
| 98 | + # Until we do that, though, we should be able to just try again if we |
| 99 | + # failed for any reason. In both of the above races, trying again should |
| 100 | + # succeed the second time round. |
| 101 | + # |
| 102 | + # For additional context, see: |
| 103 | + # * https://bugzilla.redhat.com/show_bug.cgi?id=1652287 |
| 104 | + # * https://bugs.launchpad.net/nova/+bug/1804652 |
| 105 | + try: |
| 106 | + return _db_version(repository, database, context) |
| 107 | + except Exception: |
| 108 | + return _db_version(repository, database, context) |
| 109 | + |
| 110 | + |
| 111 | +def _db_version(repository, database, context): |
| 112 | + engine = get_engine(database, context=context) |
| 113 | + try: |
| 114 | + return versioning_api.db_version(engine, repository) |
| 115 | + except versioning_exceptions.DatabaseNotControlledError as exc: |
| 116 | + meta = sqlalchemy.MetaData() |
| 117 | + meta.reflect(bind=engine) |
| 118 | + tables = meta.tables |
| 119 | + if len(tables) == 0: |
| 120 | + db_version_control( |
| 121 | + INIT_VERSION[database], database, context=context) |
| 122 | + return versioning_api.db_version(engine, repository) |
| 123 | + else: |
| 124 | + LOG.exception(exc) |
| 125 | + # Some pre-Essex DB's may not be version controlled. |
| 126 | + # Require them to upgrade using Essex first. |
| 127 | + raise exception.NovaException( |
| 128 | + _("Upgrade DB using Essex release first.")) |
32 | 129 |
|
33 | 130 |
|
34 | 131 | def db_initial_version(database='main'):
|
35 | 132 | """The starting version for the database."""
|
36 |
| - return IMPL.db_initial_version(database=database) |
| 133 | + return INIT_VERSION[database] |
| 134 | + |
| 135 | + |
| 136 | +def db_version_control(version=None, database='main', context=None): |
| 137 | + repository = find_migrate_repo(database) |
| 138 | + engine = get_engine(database, context=context) |
| 139 | + versioning_api.version_control(engine, repository, version) |
| 140 | + return version |
0 commit comments