Skip to content
Snippets Groups Projects
Commit 968128e5 authored by Samuel GAIST's avatar Samuel GAIST
Browse files

[utils][commands][restore] Check for SQLite version < 3.6

Version 3.6 of SQLite introduced a change that broke the migration
mechanism of Django. This patch checks that, if the database backend
uses SQLite, and raises an exception if needed.
parent 300b3942
No related branches found
No related tags found
1 merge request!279Restore command: validate sqlite version
Pipeline #28116 passed
......@@ -27,113 +27,137 @@
import logging
logger = logging.getLogger(__name__)
import os
import shutil
import tarfile
import tempfile
import django.core.serializers.base
from django.contrib.auth.models import User
from django.core.management import call_command
from django.core.management.base import BaseCommand, CommandError
from django.conf import settings
from django.apps import apps, registry
from django.db import connection
from .backup import APPS
# Overrides deserialization to affect OneToOneFields for Users correctly
import django.core.serializers.base
from django.contrib.auth.models import User
from ....navigation.models import Agreement
from ....accounts.models import AccountSettings
from ....experiments.models import Experiment, Block
from ....experiments.models import Experiment
from .backup import APPS
logger = logging.getLogger(__name__)
_original_save = django.core.serializers.base.DeserializedObject.save
def save(self, *args, **kwargs):
if isinstance(self.object, Agreement):
# if the user in question has an agreement, delete and write new
user = User.objects.get(pk=self.object.user_id)
if hasattr(user, 'agreement'): user.agreement.delete()
if hasattr(user, "agreement"):
user.agreement.delete()
elif isinstance(self.object, AccountSettings):
# if the user in question has account settings, just update it
user = User.objects.get(pk=self.object.owner_id)
if hasattr(user, 'accountsettings'): user.accountsettings.delete()
if hasattr(user, "accountsettings"):
user.accountsettings.delete()
# use the built-in function for storing the new object
_original_save(self, *args, **kwargs)
django.core.serializers.base.DeserializedObject.save = save
class Command(BaseCommand):
help = 'Resets the current database with a (stored) backup'
help = "Resets the current database with a (stored) backup"
def add_arguments(self, parser):
parser.add_argument('-e', '--exclude', dest='exclude', action='append',
default=[], help='An app_label to exclude (use multiple ' \
'--exclude to exclude multiple apps).')
parser.add_argument('backup', type=str,
help='The backup you wish to restore from')
parser.add_argument(
"-e",
"--exclude",
dest="exclude",
action="append",
default=[],
help="An app_label to exclude (use multiple "
"--exclude to exclude multiple apps).",
)
parser.add_argument(
"backup", type=str, help="The backup you wish to restore from"
)
def handle(self, *ignored, **arguments):
if connection.vendor == "sqlite":
import sqlite3 # noqa
from pkg_resources import parse_version # noqa
if parse_version(sqlite3.sqlite_version) >= parse_version("3.26"):
raise CommandError(
"Version 3.26 of sqlite is known to break the restore function.\n"
"More information:\n"
"https://code.djangoproject.com/ticket/29182\n"
"https://www.sqlite.org/src/info/f44bc7a8b3fac82a\n"
"https://bugs.debian.org/915626#27"
)
# Setup this command's logging level
global logger
arguments['verbosity'] = int(arguments['verbosity'])
if arguments['verbosity'] >= 1:
if arguments['verbosity'] == 1: logger.setLevel(logging.INFO)
elif arguments['verbosity'] >= 2: logger.setLevel(logging.DEBUG)
arguments["verbosity"] = int(arguments["verbosity"])
if arguments["verbosity"] >= 1:
if arguments["verbosity"] == 1:
logger.setLevel(logging.INFO)
elif arguments["verbosity"] >= 2:
logger.setLevel(logging.DEBUG)
try:
tmpdir = tempfile.mkdtemp('.backup', 'beat.web-')
tmpdir = tempfile.mkdtemp(".backup", "beat.web-")
# extracts the tarball on the temporary directory
logger.info("Reading archive `%s'" % arguments['backup'])
with tarfile.open(arguments['backup']) as tar:
logger.info("Reading archive `%s'" % arguments["backup"])
with tarfile.open(arguments["backup"]) as tar:
tar.extractall(tmpdir)
exclude = arguments.get('exclude')
exclude = arguments.get("exclude")
# remove uninstalled apps
global APPS
installed_apps = registry.apps.all_models.keys()
exclude += [app for app in APPS if app not in installed_apps]
arguments = dict(
verbosity=arguments.get('verbosity'),
interactive=False,
)
arguments = dict(verbosity=arguments.get("verbosity"), interactive=False)
# reset database contents
logger.info("Loading initial data...")
call_command('migrate', **arguments)
call_command("migrate", **arguments)
# loads the initial data
srcfile = os.path.join(tmpdir, 'initial.json')
logger.info("Loading unspecified initial data <- `%s'" % \
srcfile)
call_command('loaddata', srcfile, **arguments)
srcfile = os.path.join(tmpdir, "initial.json")
logger.info("Loading unspecified initial data <- `%s'" % srcfile)
call_command("loaddata", srcfile, **arguments)
# reset all user tokens (so they can be re-inserted)
apps.get_model('authtoken.Token').objects.all().delete()
apps.get_model("authtoken.Token").objects.all().delete()
# and loads the apps respecting the imposed order
for app in APPS:
if app in exclude: continue
if app in exclude:
continue
# copy prefix data
path = os.path.join(settings.PREFIX, app)
srcdir = os.path.join(tmpdir, 'prefix', app)
srcdir = os.path.join(tmpdir, "prefix", app)
if os.path.exists(srcdir):
logger.info("Restoring core objects for `%s' <- " \
"`%s'" % (app, srcdir))
logger.info(
"Restoring core objects for `%s' <- " "`%s'" % (app, srcdir)
)
if os.path.exists(path):
logger.info("Removing `%s'..." % path)
shutil.rmtree(path)
......@@ -141,10 +165,9 @@ class Command(BaseCommand):
else:
logger.info("No core objects found for `%s'" % app)
srcfile = os.path.join(tmpdir, '%s.json' % app)
logger.info("Loading data for `%s' <- `%s'" % \
(app, srcfile))
call_command('loaddata', srcfile, **arguments)
srcfile = os.path.join(tmpdir, "%s.json" % app)
logger.info("Loading data for `%s' <- `%s'" % (app, srcfile))
call_command("loaddata", srcfile, **arguments)
# creates the cache directory
cache = settings.CACHE_ROOT
......@@ -152,8 +175,6 @@ class Command(BaseCommand):
os.makedirs(cache)
logger.info("Created directory `%s'" % cache)
finally:
# removes the temporary directory
......@@ -163,4 +184,5 @@ class Command(BaseCommand):
# JobSplit are not backed-up because of circular dependence issues
# on the "experiments" app).
transient = (Experiment.RUNNING, Experiment.CANCELLING)
for e in Experiment.objects.filter(status__in=transient): e.reset()
for e in Experiment.objects.filter(status__in=transient):
e.reset()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment