summaryrefslogtreecommitdiff
path: root/nova/db/migration.py
blob: 80410c3192734fdc62bf7b017965a8986f11aefb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# 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