2020-03-05 12:54:34 +00:00
|
|
|
import importlib.util
|
|
|
|
from datetime import datetime
|
2022-09-29 16:29:36 +00:00
|
|
|
from inspect import signature
|
2020-03-05 12:54:34 +00:00
|
|
|
from logging import Logger
|
|
|
|
from pathlib import Path
|
|
|
|
|
2022-09-29 16:29:36 +00:00
|
|
|
import pymongo.database
|
2020-03-05 12:54:34 +00:00
|
|
|
from mongoengine.connection import get_db
|
2021-05-03 14:33:47 +00:00
|
|
|
from packaging.version import Version, parse
|
2020-03-05 12:54:34 +00:00
|
|
|
|
2024-12-05 16:54:23 +00:00
|
|
|
from apiserver.config_repo import config
|
2021-01-05 14:28:49 +00:00
|
|
|
from apiserver.database import utils
|
|
|
|
from apiserver.database import Database
|
|
|
|
from apiserver.database.model.version import Version as DatabaseVersion
|
2024-12-05 16:54:23 +00:00
|
|
|
from apiserver.utilities.dicts import nested_get
|
2020-03-05 12:54:34 +00:00
|
|
|
|
2021-05-03 14:46:00 +00:00
|
|
|
_migrations = "migrations"
|
|
|
|
_parent_dir = Path(__file__).resolve().parents[1]
|
|
|
|
_migration_dir = _parent_dir / _migrations
|
2024-12-05 16:54:23 +00:00
|
|
|
log = config.logger(__file__)
|
2020-03-05 12:54:34 +00:00
|
|
|
|
|
|
|
|
2020-08-10 05:53:41 +00:00
|
|
|
def check_mongo_empty() -> bool:
|
2023-05-25 16:39:17 +00:00
|
|
|
for alias in utils.get_options(Database):
|
|
|
|
collection_names = get_db(alias).list_collection_names()
|
|
|
|
if collection_names and any(
|
|
|
|
name in collection_names
|
|
|
|
for name in ["company", "user", "versions"]
|
|
|
|
):
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
2020-03-05 12:54:34 +00:00
|
|
|
|
2020-08-10 05:53:41 +00:00
|
|
|
|
|
|
|
def get_last_server_version() -> Version:
|
2020-03-05 12:54:34 +00:00
|
|
|
try:
|
|
|
|
previous_versions = sorted(
|
|
|
|
(Version(ver.num) for ver in DatabaseVersion.objects().only("num")),
|
|
|
|
reverse=True,
|
|
|
|
)
|
|
|
|
except ValueError as ex:
|
|
|
|
raise ValueError(f"Invalid database version number encountered: {ex}")
|
|
|
|
|
2020-08-10 05:53:41 +00:00
|
|
|
return previous_versions[0] if previous_versions else Version("0.0.0")
|
|
|
|
|
|
|
|
|
2024-12-05 16:54:23 +00:00
|
|
|
def _ensure_mongodb_version():
|
|
|
|
db: pymongo.database.Database = get_db(Database.backend)
|
|
|
|
db_version = db.client.server_info()["version"]
|
|
|
|
if not db_version.startswith("5.0"):
|
|
|
|
log.warning(f"Database version should be 5.0.x. Instead: {str(db_version)}")
|
|
|
|
return
|
|
|
|
|
|
|
|
res = db.client.admin.command({"getParameter": 1, "featureCompatibilityVersion": 1})
|
|
|
|
version = nested_get(res, ("featureCompatibilityVersion", "version"))
|
|
|
|
if version == "5.0":
|
|
|
|
return
|
|
|
|
if version != "4.4":
|
|
|
|
log.warning(f"Cannot upgrade DB version. Should be 4.4. {str(res)}")
|
|
|
|
return
|
|
|
|
|
|
|
|
log.info("Upgrading db version from 4.4 to 5.0")
|
|
|
|
res = db.client.admin.command({"setFeatureCompatibilityVersion": "5.0"})
|
|
|
|
log.info(res)
|
|
|
|
|
|
|
|
|
2020-08-10 05:53:41 +00:00
|
|
|
def _apply_migrations(log: Logger):
|
|
|
|
"""
|
|
|
|
Apply migrations as found in the migration dir.
|
|
|
|
Returns a boolean indicating whether the database was empty prior to migration.
|
|
|
|
"""
|
|
|
|
log = log.getChild(Path(__file__).stem)
|
|
|
|
|
|
|
|
log.info(f"Started mongodb migrations")
|
|
|
|
|
2024-12-05 16:54:23 +00:00
|
|
|
_ensure_mongodb_version()
|
|
|
|
|
2021-05-03 14:46:00 +00:00
|
|
|
if not _migration_dir.is_dir():
|
|
|
|
raise ValueError(f"Invalid migration dir {_migration_dir}")
|
2020-08-10 05:53:41 +00:00
|
|
|
|
|
|
|
empty_dbs = check_mongo_empty()
|
|
|
|
last_version = get_last_server_version()
|
2020-03-05 12:54:34 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
new_scripts = {
|
|
|
|
ver: path
|
2021-05-03 14:46:00 +00:00
|
|
|
for ver, path in (
|
|
|
|
(parse(f.stem.replace("_", ".")), f)
|
|
|
|
for f in _migration_dir.glob("*.py")
|
|
|
|
)
|
2020-03-05 12:54:34 +00:00
|
|
|
if ver > last_version
|
|
|
|
}
|
|
|
|
except ValueError as ex:
|
|
|
|
raise ValueError(f"Failed parsing migration version from file: {ex}")
|
|
|
|
|
|
|
|
dbs = {Database.auth: "migrate_auth", Database.backend: "migrate_backend"}
|
|
|
|
|
|
|
|
for script_version in sorted(new_scripts):
|
|
|
|
script = new_scripts[script_version]
|
|
|
|
|
|
|
|
if empty_dbs:
|
|
|
|
log.info(f"Skipping migration {script.name} (empty databases)")
|
|
|
|
else:
|
2021-05-03 14:46:00 +00:00
|
|
|
spec = importlib.util.spec_from_file_location(
|
2021-05-03 15:13:25 +00:00
|
|
|
".".join(("apiserver", _parent_dir.name, _migrations, script.stem)),
|
|
|
|
str(script),
|
2021-05-03 14:46:00 +00:00
|
|
|
)
|
2020-03-05 12:54:34 +00:00
|
|
|
module = importlib.util.module_from_spec(spec)
|
|
|
|
spec.loader.exec_module(module)
|
|
|
|
|
|
|
|
for alias, func_name in dbs.items():
|
|
|
|
func = getattr(module, func_name, None)
|
|
|
|
if not func:
|
|
|
|
continue
|
|
|
|
try:
|
2022-09-29 16:29:36 +00:00
|
|
|
sig = signature(func)
|
|
|
|
kwargs = {}
|
|
|
|
if len(sig.parameters) == 2:
|
|
|
|
name, param = list(sig.parameters.items())[-1]
|
|
|
|
key = name.replace("_", "-")
|
|
|
|
if issubclass(param.annotation, pymongo.database.Database) and key in dbs:
|
|
|
|
kwargs[name] = get_db(key)
|
2020-03-05 12:54:34 +00:00
|
|
|
log.info(f"Applying {script.stem}/{func_name}()")
|
2022-09-29 16:29:36 +00:00
|
|
|
func(get_db(alias), **kwargs)
|
2020-03-05 12:54:34 +00:00
|
|
|
except Exception:
|
|
|
|
log.exception(f"Failed applying {script}:{func_name}()")
|
|
|
|
raise ValueError(
|
|
|
|
"Migration failed, aborting. Please restore backup."
|
|
|
|
)
|
|
|
|
|
|
|
|
DatabaseVersion(
|
2021-01-05 14:28:49 +00:00
|
|
|
id=utils.id(),
|
2021-05-03 14:46:00 +00:00
|
|
|
num=str(script_version),
|
2020-03-05 12:54:34 +00:00
|
|
|
created=datetime.utcnow(),
|
|
|
|
desc="Applied on server startup",
|
|
|
|
).save()
|
|
|
|
|
|
|
|
log.info("Finished mongodb migrations")
|