nova/nova/db/migration.py
Balazs Gibizer a359753006 Fix nova-manage db version
When nova switched to alembic implementation was added to nova-manage db
version CLI to query the current db revision from alembic. However
multiple mistake was made.

The code called alembic_api.current[1] with an Engine object while that
call expects a Config object instead. This leads to but/1943436.

Also the same code expected that this call returns the revision. But
that call just prints the revision to the standard output instead.

So the implementations has been change from calling the alembic command
API which is mostly created for CLI consumption to
MigrationContext.get_current_revision() call that is intended to be used
as a python API instead.

[1] https://alembic.sqlalchemy.org/en/latest/api/commands.html#alembic.command.current

Co-Authored-By: Sean Mooney <smooney@redhat.com>
Closes-Bug: #1943436

Change-Id: I9fa7c03310c5bdb82e9a9c39727edb12eeae77f0
2021-09-13 14:08:11 +02:00

178 lines
6.2 KiB
Python

# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
from alembic import command as alembic_api
from alembic import config as alembic_config
from alembic.runtime import migration as alembic_migration
from migrate import exceptions as migrate_exceptions
from migrate.versioning import api as migrate_api
from migrate.versioning import repository as migrate_repository
from oslo_log import log as logging
from nova.db.api import api as api_db_api
from nova.db.main import api as main_db_api
from nova import exception
MIGRATE_INIT_VERSION = {
'main': 401,
'api': 66,
}
ALEMBIC_INIT_VERSION = {
'main': '8f2f1571d55b',
'api': 'd67eeaabee36',
}
LOG = logging.getLogger(__name__)
def _get_engine(database='main', context=None):
if database == 'main':
return main_db_api.get_engine(context=context)
if database == 'api':
return api_db_api.get_engine()
def _find_migrate_repo(database='main'):
"""Get the path for the migrate repository."""
path = os.path.join(
os.path.abspath(os.path.dirname(__file__)),
database, 'legacy_migrations')
return migrate_repository.Repository(path)
def _find_alembic_conf(database='main'):
"""Get the path for the alembic repository."""
path = os.path.join(
os.path.abspath(os.path.dirname(__file__)),
database, 'alembic.ini')
config = alembic_config.Config(path)
# we don't want to use the logger configuration from the file, which is
# only really intended for the CLI
# https://stackoverflow.com/a/42691781/613428
config.attributes['configure_logger'] = False
return config
def _is_database_under_migrate_control(engine, repository):
try:
migrate_api.db_version(engine, repository)
return True
except migrate_exceptions.DatabaseNotControlledError:
return False
def _is_database_under_alembic_control(engine):
with engine.connect() as conn:
context = alembic_migration.MigrationContext.configure(conn)
return bool(context.get_current_revision())
def _init_alembic_on_legacy_database(engine, database, repository, config):
"""Init alembic in an existing environment with sqlalchemy-migrate."""
LOG.info(
'The database is still under sqlalchemy-migrate control; '
'applying any remaining sqlalchemy-migrate-based migrations '
'and fake applying the initial alembic migration'
)
migrate_api.upgrade(engine, repository)
# re-use the connection rather than creating a new one
with engine.begin() as connection:
config.attributes['connection'] = connection
alembic_api.stamp(config, ALEMBIC_INIT_VERSION[database])
def _upgrade_alembic(engine, config, version):
# re-use the connection rather than creating a new one
with engine.begin() as connection:
config.attributes['connection'] = connection
alembic_api.upgrade(config, version or 'head')
def db_sync(version=None, database='main', context=None):
"""Migrate the database to `version` or the most recent version."""
if database not in ('main', 'api'):
raise exception.Invalid('%s is not a valid database' % database)
# if the user requested a specific version, check if it's an integer:
# if so, we're almost certainly in sqlalchemy-migrate land and won't
# support that
if version is not None and version.isdigit():
raise exception.Invalid(
'You requested an sqlalchemy-migrate database version; this is '
'no longer supported'
)
engine = _get_engine(database, context=context)
repository = _find_migrate_repo(database)
config = _find_alembic_conf(database)
# discard the URL stored in alembic.ini in favour of the URL configured
# for the engine, casting from 'sqlalchemy.engine.url.URL' to str in the
# process
# NOTE(sean-k-mooney): the engine has already url encoded the connection
# string using a mix of url encode styles for different parts of the url.
# since we are updating the alembic config parser instance we need to
# escape '%' to '%%' to account for ConfigParser's string interpolation.
url = str(engine.url).replace('%', '%%')
config.set_main_option('sqlalchemy.url', url)
# if we're in a deployment where sqlalchemy-migrate is already present,
# then apply all the updates for that and fake apply the initial alembic
# migration; if we're not then 'upgrade' will take care of everything
# this should be a one-time operation
if (
_is_database_under_migrate_control(engine, repository) and
not _is_database_under_alembic_control(engine)
):
_init_alembic_on_legacy_database(engine, database, repository, config)
# apply anything later
LOG.info('Applying migration(s)')
_upgrade_alembic(engine, config, version)
LOG.info('Migration(s) applied')
def db_version(database='main', context=None):
"""Display the current database version."""
if database not in ('main', 'api'):
raise exception.Invalid('%s is not a valid database' % database)
repository = _find_migrate_repo(database)
engine = _get_engine(database, context=context)
migrate_version = None
if _is_database_under_migrate_control(engine, repository):
migrate_version = migrate_api.db_version(engine, repository)
alembic_version = None
if _is_database_under_alembic_control(engine):
with engine.connect() as conn:
m_context = alembic_migration.MigrationContext.configure(conn)
alembic_version = m_context.get_current_revision()
return alembic_version or migrate_version