Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preseed all the databases #8468

Merged
merged 8 commits into from
Sep 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 2 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,7 @@ writeversion:
@echo "Current version is now `cat kolibri/VERSION`"

preseeddb:
$(eval TEMPKHOME := $(shell mktemp -d /tmp/kolibri-preseeddb-XXXXXXXX))
PYTHONPATH=".:$$PYTHONPATH" KOLIBRI_HOME=$(TEMPKHOME) python -m kolibri manage migrate
yes yes | PYTHONPATH=".:$$PYTHONPATH" KOLIBRI_HOME=$(TEMPKHOME) python -m kolibri manage deprovision
mkdir kolibri/dist/home
mv $(TEMPKHOME)/db.sqlite3 kolibri/dist/home/db.sqlite3
mv $(TEMPKHOME)/notifications.sqlite3 kolibri/dist/home/notifications.sqlite3
rm -r $(TEMPKHOME)
PYTHONPATH=".:$PYTHONPATH" python build_tools/preseed_home.py

setrequirements:
rm -r requirements.txt || true # remove requirements.txt
Expand All @@ -182,7 +176,7 @@ buildconfig:
git checkout -- kolibri/utils/build_config # restore __init__.py
python build_tools/customize_build.py

dist: setrequirements writeversion staticdeps staticdeps-cext buildconfig i18n-extract-frontend assets i18n-django-compilemessages
dist: setrequirements writeversion staticdeps staticdeps-cext buildconfig i18n-extract-frontend assets i18n-django-compilemessages preseeddb
python setup.py sdist --format=gztar > /dev/null # silence the sdist output! Too noisy!
python setup.py bdist_wheel
ls -l dist
Expand Down
32 changes: 32 additions & 0 deletions build_tools/preseed_home.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import os
import shutil
import tempfile

temphome = tempfile.mkdtemp()
os.environ["KOLIBRI_HOME"] = temphome

from kolibri.main import initialize # noqa E402
from kolibri.deployment.default.sqlite_db_names import ( # noqa E402
ADDITIONAL_SQLITE_DATABASES,
)
from django.core.management import call_command # noqa E402

move_to = os.path.join(os.path.dirname(__file__), "..", "kolibri", "dist", "home")
shutil.rmtree(move_to, ignore_errors=True)
os.mkdir(move_to)

print("Generating preseeded home data in {}".format(temphome))

initialize()
call_command(
"deprovision", "--destroy-all-user-data", "--permanent-irrevocable-data-loss"
)

shutil.move(os.path.join(temphome, "db.sqlite3"), move_to)

for db_name in ADDITIONAL_SQLITE_DATABASES:
shutil.move(os.path.join(temphome, "{}.sqlite3".format(db_name)), move_to)

print("Moved all preseeded home data to {}".format(move_to))

shutil.rmtree(temphome)
32 changes: 25 additions & 7 deletions kolibri/core/auth/management/commands/deprovision.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@
class Command(AsyncCommand):
help = "Delete all facility user data from the local database, and put it back to a clean state (but leaving content as-is)."

def add_arguments(self, parser):
parser.add_argument(
"--destroy-all-user-data",
action="store_true",
dest="confirmation1",
default=False,
)
parser.add_argument(
"--permanent-irrevocable-data-loss",
action="store_true",
dest="confirmation2",
default=False,
)

def deprovision(self):
with DisablePostDeleteSignal(), self.start_progress(
total=len(MODELS_TO_DELETE)
Expand All @@ -60,13 +74,17 @@ def handle_async(self, *args, **options):
except server.NotRunning:
pass

# ensure the user REALLY wants to do this!
confirm_or_exit(
"Are you sure you wish to deprovision your database? This will DELETE ALL USER DATA!"
)
confirm_or_exit(
"ARE YOU SURE? If you do this, there is no way to recover the user data on this device."
)
if not options["confirmation1"]:
# ensure the user REALLY wants to do this!
confirm_or_exit(
"Are you sure you wish to deprovision your database? This will DELETE ALL USER DATA!"
)

if not options["confirmation2"]:
# ensure the user REALLY REALLY wants to do this!
confirm_or_exit(
"ARE YOU SURE? If you do this, there is no way to recover the user data on this device."
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had previously made the explicit choice not to make it that easy to deprovision without warnings, given how destructive it is.

If a one-liner is really needed, we've suggested yes yes|kolibri manage deprovision in the past. I guess that doesn't work on Windows, though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see it's for the purposes of call_command, though -- I wonder about using a nonstandard name like --accept-data-loss or something to make it a bit harder to do accidentally.

I'm probably being paranoid. Except if there's anything we've learned from what users do.....

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I only added it because of the python invocation, happy to make it something ridiculous!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throw some unicode for a few different languages in there and we should be good, then!


print("Proceeding with deprovisioning. Deleting all user data.")
self.deprovision()
Expand Down
56 changes: 31 additions & 25 deletions kolibri/utils/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from kolibri.core.tasks.main import import_tasks_module_from_django_apps
from kolibri.core.upgrade import matches_version
from kolibri.core.upgrade import run_upgrades
from kolibri.deployment.default.sqlite_db_names import ADDITIONAL_SQLITE_DATABASES
from kolibri.plugins.utils import autoremove_unavailable_plugins
from kolibri.plugins.utils import check_plugin_config_file_location
from kolibri.plugins.utils import enable_new_default_plugins
Expand Down Expand Up @@ -160,6 +161,29 @@ def _setup_django():
raise


def _copy_preseeded_db(db_name, target=None):
target = target or "{}.sqlite3".format(db_name)
target = os.path.join(KOLIBRI_HOME, target)
if not os.path.exists(target):
try:
import kolibri.dist

db_path = os.path.join(
os.path.dirname(kolibri.dist.__file__),
"home/{}.sqlite3".format(db_name),
)
shutil.copy(db_path, target)
logger.info(
"Copied preseeded database from {} to {}".format(db_path, target)
)
except (ImportError, IOError, OSError):
logger.warning(
"Unable to copy pre-migrated database from {} to {}".format(
db_path, target
)
)


def _upgrades_before_django_setup(updated, version):
if version and updated:
check_plugin_config_file_location(version)
Expand All @@ -170,35 +194,17 @@ def _upgrades_before_django_setup(updated, version):
# dbbackup relies on settings.INSTALLED_APPS
enable_new_default_plugins()

if not version and OPTIONS["Database"]["DATABASE_ENGINE"] == "sqlite":
# If there is no registered version, and we are using sqlite,
if OPTIONS["Database"]["DATABASE_ENGINE"] == "sqlite":
# If we are using sqlite,
# we can shortcut migrations by using the preseeded databases
# that we bundle in the Kolibri whl file.
logger.info("Attempting to setup using pre-migrated databases")
try:
import kolibri.dist
if not version:
logger.info("Attempting to setup using pre-migrated databases")

main_db_path = os.path.join(kolibri.dist.__file__, "home/db.sqlite3")
shutil.copy(main_db_path, KOLIBRI_HOME)
except (ImportError, IOError, OSError):
logger.warning(
"Unable to copy pre-migrated database from {} to {}".format(
main_db_path, KOLIBRI_HOME
)
)
try:
import kolibri.dist
_copy_preseeded_db("db", target=OPTIONS["Database"]["DATABASE_NAME"])

notifications_db_path = os.path.join(
kolibri.dist.__file__, "home/notifications.sqlite3"
)
shutil.copy(notifications_db_path, KOLIBRI_HOME)
except (ImportError, IOError, OSError):
logger.warning(
"Unable to copy pre-migrated database from {} to {}".format(
notifications_db_path, KOLIBRI_HOME
)
)
for db_name in ADDITIONAL_SQLITE_DATABASES:
_copy_preseeded_db(db_name)


def _upgrades_after_django_setup(updated, version):
Expand Down