From 86e9a3217c91b94a09deabfff6ac4c85db1aca93 Mon Sep 17 00:00:00 2001 From: Min RK Date: Wed, 13 Apr 2016 16:34:36 +0200 Subject: [PATCH] add jupyterhub upgrade-db entry point don't do automatic upgrades (yet) I'm not sure if auto upgrades are a good idea or not. --- jupyterhub/app.py | 32 +++++++++++++++-- jupyterhub/dbutil.py | 82 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 jupyterhub/dbutil.py diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 3e31848b..4a465e39 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -46,7 +46,7 @@ import jupyterhub from . import handlers, apihandlers from .handlers.static import CacheControlStaticFilesHandler, LogoHandler -from . import orm +from . import dbutil, orm from .user import User, UserDict from ._data import DATA_FILES_PATH from .log import CoroutineLogFormatter, log_request @@ -103,6 +103,7 @@ SECRET_BYTES = 2048 # the number of bytes to use when generating new secrets class NewToken(Application): """Generate and print a new API token""" name = 'jupyterhub-token' + version = jupyterhub.__version__ description = """Generate and return new API token for a user. Usage: @@ -142,6 +143,26 @@ class NewToken(Application): token = user.new_api_token() print(token) +class UpgradeDB(Application): + """Upgrade the JupyterHub database schema.""" + + name = 'jupyterhub-upgrade-db' + version = jupyterhub.__version__ + description = """Upgrade the JupyterHub database to the current schema. + + Usage: + + jupyterhub upgrade-db + """ + aliases = common_aliases + classes = [] + + def start(self): + hub = JupyterHub(parent=self) + hub.load_config_file(hub.config_file) + self.log.info("Upgrading %s", hub.db_url) + dbutil.upgrade(hub.db_url) + class JupyterHub(Application): """An Application for starting a Multi-User Jupyter Notebook server.""" @@ -170,7 +191,8 @@ class JupyterHub(Application): flags = Dict(flags) subcommands = { - 'token': (NewToken, "Generate an API token for a user") + 'token': (NewToken, "Generate an API token for a user"), + 'upgrade-db': (UpgradeDB, "Upgrade your JupyterHub state database to the current version."), } classes = List([ @@ -706,6 +728,11 @@ class JupyterHub(Application): self.log.debug("Database error was:", exc_info=True) if self.db_url.startswith('sqlite:///'): self._check_db_path(self.db_url.split(':///', 1)[1]) + self.log.critical('\n'.join([ + "If you recently upgraded JupyterHub, try running", + " jupyterhub upgrade-db", + "to upgrade your JupyterHub database schema", + ])) self.exit(1) def init_hub(self): @@ -1307,6 +1334,7 @@ class JupyterHub(Application): print("\nInterrupted") NewToken.classes.append(JupyterHub) +UpgradeDB.classes.append(JupyterHub) main = JupyterHub.launch_instance diff --git a/jupyterhub/dbutil.py b/jupyterhub/dbutil.py new file mode 100644 index 00000000..e092da43 --- /dev/null +++ b/jupyterhub/dbutil.py @@ -0,0 +1,82 @@ +"""Database utilities for JupyterHub""" +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +# Based on pgcontents.utils.migrate, used under the Apache license. + +from contextlib import contextmanager +import os +from subprocess import check_call +import sys +from tempfile import TemporaryDirectory + +_here = os.path.abspath(os.path.dirname(__file__)) + +ALEMBIC_INI_TEMPLATE_PATH = os.path.join(_here, 'alembic.ini') +ALEMBIC_DIR = os.path.join(_here, 'alembic') + + +def write_alembic_ini(alembic_ini='alembic.ini', db_url='sqlite:///jupyterhub.sqlite'): + """Write a complete alembic.ini from our template. + + Parameters + ---------- + + alembic_ini: str + path to the alembic.ini file that should be written. + db_url: str + The SQLAlchemy database url, e.g. `sqlite:///jupyterhub.sqlite`. + """ + with open(ALEMBIC_INI_TEMPLATE_PATH) as f: + alembic_ini_tpl = f.read() + + with open(alembic_ini, 'w') as f: + f.write( + alembic_ini_tpl.format( + alembic_dir=ALEMBIC_DIR, + db_url=db_url, + ) + ) + + + +@contextmanager +def _temp_alembic_ini(db_url): + """Context manager for temporary JupyterHub alembic directory + + Temporarily write an alembic.ini file for use with alembic migration scripts. + + Context manager yields alembic.ini path. + + Parameters + ---------- + + db_url: str + The SQLAlchemy database url, e.g. `sqlite:///jupyterhub.sqlite`. + + Returns + ------- + + alembic_ini: str + The path to the temporary alembic.ini that we have created. + This file will be cleaned up on exit from the context manager. + """ + with TemporaryDirectory() as td: + alembic_ini = os.path.join(td, 'alembic.ini') + write_alembic_ini(alembic_ini) + yield alembic_ini + + +def upgrade(db_url, revision='head'): + """Upgrade the given database to revision. + + db_url: str + The SQLAlchemy database url, e.g. `sqlite:///jupyterhub.sqlite`. + revision: str [default: head] + The alembic revision to upgrade to. + """ + with _temp_alembic_ini(db_url) as alembic_ini: + check_call( + ['alembic', '-c', alembic_ini, 'upgrade', revision] + ) +