db: Final cleanups

Some things that were missed in previous patches and are thrown together
here:

- Add alembic as an explicit dependency (we were getting it transitively
  from oslo.db). We also bump the sqlalchemy dependency to a 1.4.x
  release, which is the minimum supported by our chosen version of
  alembic (more on this below)
- Remove tooling related to the old migrations
- Fix the tox whitelisting of the flaky MySQL tests

On the SQLAlchemy front, we opt for 1.4.13. Technically alembic should
support anything from 1.4.0, however, with SQLAlchemy >= 1.4.0, < 1.4.13
we see errors like the following in some tests:

  sqlalchemy.exc.InvalidRequestError: Entity namespace for
  "count(instance_mappings.id)" has no property "queued_for_delete"

There's nothing specific about this in the release notes for 1.4.13 [1]
but it definitely fixes things.

[1] https://docs.sqlalchemy.org/en/14/changelog/changelog_14.html#change-1.4.13

Change-Id: I4c8eb13f11aa7471c26a5ba326319aef245c9836
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane 2021-07-12 15:34:26 +01:00
parent a7584ec1a5
commit eb728e877a
6 changed files with 6 additions and 500 deletions

View File

@ -1,4 +1,4 @@
alembic==0.9.8 alembic==1.5.0
amqp==2.5.0 amqp==2.5.0
appdirs==1.4.3 appdirs==1.4.3
asn1crypto==0.24.0 asn1crypto==0.24.0
@ -137,7 +137,7 @@ simplejson==3.13.2
six==1.15.0 six==1.15.0
smmap2==2.0.3 smmap2==2.0.3
sortedcontainers==2.1.0 sortedcontainers==2.1.0
SQLAlchemy==1.2.19 SQLAlchemy==1.4.13
sqlalchemy-migrate==0.13.0 sqlalchemy-migrate==0.13.0
sqlparse==0.2.4 sqlparse==0.2.4
statsd==3.2.2 statsd==3.2.2

View File

@ -1,9 +1,5 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
pbr>=5.5.1 # Apache-2.0 pbr>=5.5.1 # Apache-2.0
SQLAlchemy>=1.2.19 # MIT SQLAlchemy>=1.4.13 # MIT
decorator>=4.1.0 # BSD decorator>=4.1.0 # BSD
eventlet>=0.30.1 # MIT eventlet>=0.30.1 # MIT
Jinja2>=2.10 # BSD License (3 clause) Jinja2>=2.10 # BSD License (3 clause)
@ -19,6 +15,7 @@ PasteDeploy>=1.5.0 # MIT
Paste>=2.0.2 # MIT Paste>=2.0.2 # MIT
PrettyTable>=0.7.1 # BSD PrettyTable>=0.7.1 # BSD
sqlalchemy-migrate>=0.13.0 # Apache-2.0 sqlalchemy-migrate>=0.13.0 # Apache-2.0
alembic>=1.5.0 # MIT
netaddr>=0.7.18 # BSD netaddr>=0.7.18 # BSD
netifaces>=0.10.4 # MIT netifaces>=0.10.4 # MIT
paramiko>=2.7.1 # LGPLv2.1+ paramiko>=2.7.1 # LGPLv2.1+

View File

@ -1,283 +0,0 @@
#!/usr/bin/env python
# Copyright 2012 OpenStack Foundation
#
# 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.
"""
Utility for diff'ing two versions of the DB schema.
Each release cycle the plan is to compact all of the migrations from that
release into a single file. This is a manual and, unfortunately, error-prone
process. To ensure that the schema doesn't change, this tool can be used to
diff the compacted DB schema to the original, uncompacted form.
The database is specified by providing a SQLAlchemy connection URL WITHOUT the
database-name portion (that will be filled in automatically with a temporary
database name).
The schema versions are specified by providing a git ref (a branch name or
commit hash) and a SQLAlchemy-Migrate version number:
Run like:
MYSQL:
./tools/db/schema_diff.py mysql+pymysql://root@localhost \
master:latest my_branch:82
POSTGRESQL:
./tools/db/schema_diff.py postgresql://localhost \
master:latest my_branch:82
"""
import datetime
import glob
import os
import subprocess
import sys
from nova.i18n import _
# Dump
def dump_db(db_driver, db_name, db_url, migration_version, dump_filename):
if not db_url.endswith('/'):
db_url += '/'
db_url += db_name
db_driver.create(db_name)
try:
_migrate(db_url, migration_version)
db_driver.dump(db_name, dump_filename)
finally:
db_driver.drop(db_name)
# Diff
def diff_files(filename1, filename2):
pipeline = ['diff -U 3 %(filename1)s %(filename2)s'
% {'filename1': filename1, 'filename2': filename2}]
# Use colordiff if available
if subprocess.call(['which', 'colordiff']) == 0:
pipeline.append('colordiff')
pipeline.append('less -R')
cmd = ' | '.join(pipeline)
subprocess.check_call(cmd, shell=True)
# Database
class Mysql(object):
def create(self, name):
subprocess.check_call(['mysqladmin', '-u', 'root', 'create', name])
def drop(self, name):
subprocess.check_call(['mysqladmin', '-f', '-u', 'root', 'drop', name])
def dump(self, name, dump_filename):
subprocess.check_call(
'mysqldump -u root %(name)s > %(dump_filename)s'
% {'name': name, 'dump_filename': dump_filename},
shell=True)
class Postgresql(object):
def create(self, name):
subprocess.check_call(['createdb', name])
def drop(self, name):
subprocess.check_call(['dropdb', name])
def dump(self, name, dump_filename):
subprocess.check_call(
'pg_dump %(name)s > %(dump_filename)s'
% {'name': name, 'dump_filename': dump_filename},
shell=True)
def _get_db_driver_class(db_url):
try:
return globals()[db_url.split('://')[0].capitalize()]
except KeyError:
raise Exception(_("database %s not supported") % db_url)
# Migrate
MIGRATE_REPO = os.path.join(os.getcwd(), "nova/db/main/legacy_migrations")
def _migrate(db_url, migration_version):
earliest_version = _migrate_get_earliest_version()
# NOTE(sirp): sqlalchemy-migrate currently cannot handle the skipping of
# migration numbers.
_migrate_cmd(
db_url, 'version_control', str(earliest_version - 1))
upgrade_cmd = ['upgrade']
if migration_version != 'latest':
upgrade_cmd.append(str(migration_version))
_migrate_cmd(db_url, *upgrade_cmd)
def _migrate_cmd(db_url, *cmd):
manage_py = os.path.join(MIGRATE_REPO, 'manage.py')
args = ['python', manage_py]
args += cmd
args += ['--repository=%s' % MIGRATE_REPO,
'--url=%s' % db_url]
subprocess.check_call(args)
def _migrate_get_earliest_version():
versions_glob = os.path.join(MIGRATE_REPO, 'versions', '???_*.py')
versions = []
for path in glob.iglob(versions_glob):
filename = os.path.basename(path)
prefix = filename.split('_', 1)[0]
try:
version = int(prefix)
except ValueError:
pass
versions.append(version)
versions.sort()
return versions[0]
# Git
def git_current_branch_name():
ref_name = git_symbolic_ref('HEAD', quiet=True)
current_branch_name = ref_name.replace('refs/heads/', '')
return current_branch_name
def git_symbolic_ref(ref, quiet=False):
args = ['git', 'symbolic-ref', ref]
if quiet:
args.append('-q')
proc = subprocess.Popen(args, stdout=subprocess.PIPE)
stdout, stderr = proc.communicate()
return stdout.strip()
def git_checkout(branch_name):
subprocess.check_call(['git', 'checkout', branch_name])
def git_has_uncommited_changes():
return subprocess.call(['git', 'diff', '--quiet', '--exit-code']) == 1
# Command
def die(msg):
print("ERROR: %s" % msg, file=sys.stderr)
sys.exit(1)
def usage(msg=None):
if msg:
print("ERROR: %s" % msg, file=sys.stderr)
prog = "schema_diff.py"
args = ["<db-url>", "<orig-branch:orig-version>",
"<new-branch:new-version>"]
print("usage: %s %s" % (prog, ' '.join(args)), file=sys.stderr)
sys.exit(1)
def parse_options():
try:
db_url = sys.argv[1]
except IndexError:
usage("must specify DB connection url")
try:
orig_branch, orig_version = sys.argv[2].split(':')
except IndexError:
usage('original branch and version required (e.g. master:82)')
try:
new_branch, new_version = sys.argv[3].split(':')
except IndexError:
usage('new branch and version required (e.g. master:82)')
return db_url, orig_branch, orig_version, new_branch, new_version
def main():
timestamp = datetime.datetime.utcnow().strftime("%Y%m%d_%H%M%S")
ORIG_DB = 'orig_db_%s' % timestamp
NEW_DB = 'new_db_%s' % timestamp
ORIG_DUMP = ORIG_DB + ".dump"
NEW_DUMP = NEW_DB + ".dump"
options = parse_options()
db_url, orig_branch, orig_version, new_branch, new_version = options
# Since we're going to be switching branches, ensure user doesn't have any
# uncommitted changes
if git_has_uncommited_changes():
die("You have uncommitted changes. Please commit them before running "
"this command.")
db_driver = _get_db_driver_class(db_url)()
users_branch = git_current_branch_name()
git_checkout(orig_branch)
try:
# Dump Original Schema
dump_db(db_driver, ORIG_DB, db_url, orig_version, ORIG_DUMP)
# Dump New Schema
git_checkout(new_branch)
dump_db(db_driver, NEW_DB, db_url, new_version, NEW_DUMP)
diff_files(ORIG_DUMP, NEW_DUMP)
finally:
git_checkout(users_branch)
if os.path.exists(ORIG_DUMP):
os.unlink(ORIG_DUMP)
if os.path.exists(NEW_DUMP):
os.unlink(NEW_DUMP)
if __name__ == "__main__":
main()

View File

@ -1,123 +0,0 @@
#!/usr/bin/env bash
#
# Script to generate schemas for the various versions.
#
# Some setup is required, similar to the opportunistic tests.
#
# MySQL ->
#
# $ mysql -uroot
# MariaDB [(none)]> CREATE DATABASE nova
# MariaDB [(none)]> GRANT ALL PRIVILEGES ON nova.* TO 'nova'@'localhost' IDENTIFIED BY 'password';
# MariaDB [(none)]> quit;
#
# Postgres ->
#
# $ sudo -u postgres psql
# postgres=# create user nova with createdb login password 'password';
# postgres=# create database nova with owner nova;
# postgres=# quit;
#
# Note that you may also have to configure 'pg_hba.conf' to use password-based
# auth instead of "ident", if you haven't done so already. You can locate this
# with 'locate pg_hba.conf'. More details at
# https://ubuntu.com/server/docs/databases-postgresql
set -o xtrace
set -e
source .tox/py36/bin/activate
pushd nova/db/main/legacy_migrations
INIT_VERSION=$(ls -1 versions/ | head -1 | awk -F_ '{print $1}')
INIT_VERSION=$(($INIT_VERSION-1))
echo "Detected init version of $INIT_VERSION"
mkdir -p schemas
rm -f "schemas/$INIT_VERSION-*.sql"
#
# sqlite
#
# cleanup from previous runs
rm -f nova.db
# sync schema
python manage.py version_control \
--database 'sqlite:///nova.db' \
--version $INIT_VERSION
python manage.py upgrade \
--database 'sqlite:///nova.db'
# dump the schema
sqlite3 nova.db << EOF
.output "schemas/${INIT_VERSION}-sqlite.sql"
.schema
.quit
EOF
rm -f nova.db
#
# mysql
#
# cleanup from previous runs
mysql -u nova -ppassword << EOF
DROP DATABASE IF EXISTS nova;
CREATE DATABASE nova;
EOF
# sync schema
python manage.py version_control \
--database 'mysql+pymysql://nova:password@localhost/nova' \
--version "$INIT_VERSION"
python manage.py upgrade \
--database 'mysql+pymysql://nova:password@localhost/nova'
# dump the schema
mysqldump --no-data --skip-comments -u nova -ppassword \
nova > "schemas/${INIT_VERSION}-mysql.sql"
mysql -u nova -ppassword << EOF
DROP DATABASE IF EXISTS nova;
EOF
#
# postgres
#
# cleanup from previous runs
sudo -u postgres dropdb --if-exists nova
sudo -u postgres createdb --owner=nova nova
# sync to initial version
python manage.py version_control \
--database 'postgresql://nova:password@localhost/nova' \
--version "$INIT_VERSION"
python manage.py upgrade \
--database 'postgresql://nova:password@localhost/nova'
# dump the schema
pg_dump postgresql://nova:password@localhost/nova \
--schema-only > "schemas/${INIT_VERSION}-postgres.sql"
sudo -u postgres dropdb --if-exists nova
popd
deactivate

View File

@ -1,75 +0,0 @@
#!/usr/bin/env python
import argparse
import glob
import os
import subprocess
BASE = 'nova/db/main/legacy_migrations/versions'.split('/')
API_BASE = 'nova/db/api/legacy_migrations/versions'.split('/')
STUB = \
"""# 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.
# This is a placeholder for backports.
# Do not use this number for new work. New work starts after
# all the placeholders.
#
# See this for more information:
# http://lists.openstack.org/pipermail/openstack-dev/2013-March/006827.html
def upgrade(migrate_engine):
pass
"""
def get_last_migration(base):
path = os.path.join(*tuple(base + ['[0-9]*.py']))
migrations = sorted([os.path.split(fn)[-1] for fn in glob.glob(path)])
return int(migrations[-1].split('_')[0])
def reserve_migrations(base, number, git_add):
last = get_last_migration(base)
for i in range(last + 1, last + number + 1):
name = '%03i_placeholder.py' % i
path = os.path.join(*tuple(base + [name]))
with open(path, 'w') as f:
f.write(STUB)
print('Created %s' % path)
if git_add:
subprocess.call('git add %s' % path, shell=True)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-n', '--number', default=10,
type=int,
help='Number of migrations to reserve')
parser.add_argument('-g', '--git-add', action='store_const',
const=True, default=False,
help='Automatically git-add new migrations')
parser.add_argument('-a', '--api', action='store_const',
const=True, default=False,
help='Reserve migrations for the API database')
args = parser.parse_args()
if args.api:
base = API_BASE
else:
base = BASE
reserve_migrations(base, args.number, args.git_add)
if __name__ == '__main__':
main()

14
tox.ini
View File

@ -32,11 +32,7 @@ passenv =
# there is also secret magic in subunit-trace which lets you run in a fail only # there is also secret magic in subunit-trace which lets you run in a fail only
# mode. To do this define the TRACE_FAILONLY environmental variable. # mode. To do this define the TRACE_FAILONLY environmental variable.
commands = commands =
# NOTE(gibi): The group-regex runs the matching tests in the same executor. stestr run {posargs}
# These tests runs against a real mysql instance and in an IO deprived CI VM they tend to time out.
# See bug https://launchpad.net/bugs/1823251 for details.
# By running them in the same executor we can spread the IO load of these tests in time.
stestr --group-regex=nova\.tests\.unit\.db\.test_migrations\.TestNovaMigrationsMySQL run {posargs}
env TEST_OSPROFILER=1 stestr run --combine --no-discover 'nova.tests.unit.test_profiler' env TEST_OSPROFILER=1 stestr run --combine --no-discover 'nova.tests.unit.test_profiler'
stestr slowest stestr slowest
@ -96,13 +92,7 @@ deps =
{[testenv]deps} {[testenv]deps}
openstack-placement>=1.0.0 openstack-placement>=1.0.0
commands = commands =
# NOTE(gibi): The group-regex runs the matching tests in the same executor. stestr --test-path=./nova/tests/functional run {posargs}
# These tests runs against a real db instance and in an IO deprived CI VM they tend to time out.
# See bug https://launchpad.net/bugs/1823251 for details.
# By running them in the same executor we can spread the IO load of these tests in time.
# NOTE(gibi): I was not able to group only the mysql tests this way as regex
# TestNovaAPIMigrations.*MySQL does not do what I expect
stestr --group-regex=nova\.tests\.functional\.db\.api\.test_migrations\.TestNovaAPIMigrations --test-path=./nova/tests/functional run {posargs}
stestr slowest stestr slowest
[testenv:functional-py36] [testenv:functional-py36]