Merge pull request #163 from minrk/config-dir

put config in `$tljh/config` directory
This commit is contained in:
Yuvi Panda
2018-09-04 18:04:57 -07:00
committed by GitHub
12 changed files with 212 additions and 35 deletions

View File

@@ -59,8 +59,8 @@ The easiest & safest way to develop & test TLJH is with `Docker <https://www.doc
you can test it by running ``python3 -m tljh.installer``. you can test it by running ``python3 -m tljh.installer``.
* If you changed ``tljh/jupyterhub_config.py``, ``tljh/configurer.py``, * If you changed ``tljh/jupyterhub_config.py``, ``tljh/configurer.py``,
``/opt/tljh/config.yaml`` or any of their dependencies, you only need to ``/opt/tljh/config/`` or any of their dependencies, you only need to
restart jupyterhub for them to take effect. ``systemctl restart jupyterhub`` restart jupyterhub for them to take effect. ``tljh-config reload hub``
should do that. should do that.
:ref:`troubleshooting/logs` has information on looking at various logs in the container :ref:`troubleshooting/logs` has information on looking at various logs in the container

View File

@@ -48,7 +48,7 @@ You can change the default interface users get when they log in by modifying
.. code-block:: yaml .. code-block:: yaml
sudo tljh-config reload sudo tljh-config reload hub
If this causes problems, check the :ref:`troubleshoot_logs_jupyterhub` for clues If this causes problems, check the :ref:`troubleshoot_logs_jupyterhub` for clues
on what went wrong. on what went wrong.

View File

@@ -10,8 +10,8 @@ directory that lets you load multiple ``jupyterhub_config.py`` snippets for
your configuration. You need to create the directory when you use it for your configuration. You need to create the directory when you use it for
the first time. the first time.
Any files in ``/opt/tljh/jupyterhub_config.d`` that end in ``.py`` will be Any files in ``/opt/tljh/config/jupyterhub_config.d`` that end in ``.py`` will be
loaded in alphabetical order as python files to provide configuration for loaded in alphabetical order as python files to provide configuration for
JupyterHub. Any config that can go in a regular ``jupyterhub_config.py`` JupyterHub. Any config that can go in a regular ``jupyterhub_config.py``
file is valid in these files. They will be loaded *after* any of the config file is valid in these files. They will be loaded *after* any of the config
options specified with ``tljh-config`` are loaded. options specified with ``tljh-config`` are loaded.

View File

@@ -4,6 +4,7 @@ Test simplest plugin
from ruamel.yaml import YAML from ruamel.yaml import YAML
import os import os
import subprocess import subprocess
from tljh.config import CONFIG_FILE, USER_ENV_PREFIX
yaml = YAML(typ='rt') yaml = YAML(typ='rt')
@@ -20,7 +21,7 @@ def test_pip_packages():
Test extra user pip packages are installed Test extra user pip packages are installed
""" """
subprocess.check_call([ subprocess.check_call([
'/opt/tljh/user/bin/python3', f'{USER_ENV_PREFIX}/bin/python3',
'-c', '-c',
'import django' 'import django'
]) ])
@@ -31,7 +32,7 @@ def test_conda_packages():
Test extra user conda packages are installed Test extra user conda packages are installed
""" """
subprocess.check_call([ subprocess.check_call([
'/opt/tljh/user/bin/python3', f'{USER_ENV_PREFIX}/bin/python3',
'-c', '-c',
'import hypothesis' 'import hypothesis'
]) ])
@@ -41,7 +42,7 @@ def test_config_hook():
""" """
Check config changes are present Check config changes are present
""" """
with open('/opt/tljh/config.yaml') as f: with open(CONFIG_FILE) as f:
data = yaml.load(f) data = yaml.load(f)
assert data['simplest_plugin']['present'] assert data['simplest_plugin']['present']

View File

@@ -21,4 +21,5 @@ def tljh_dir(tmpdir):
reload(mod) reload(mod)
assert tljh.config.INSTALL_PREFIX == tljh_dir assert tljh.config.INSTALL_PREFIX == tljh_dir
os.makedirs(tljh.config.STATE_DIR) os.makedirs(tljh.config.STATE_DIR)
os.makedirs(tljh.config.CONFIG_DIR)
yield tljh_dir yield tljh_dir

View File

@@ -1,10 +1,21 @@
""" """
Unit test functions in installer.py Unit test functions in installer.py
""" """
from tljh import installer
import os import os
from tljh import installer
def test_ensure_node(): def test_ensure_node():
installer.ensure_node() installer.ensure_node()
assert os.path.exists('/usr/bin/node') assert os.path.exists('/usr/bin/node')
def test_ensure_config_yaml(tljh_dir):
pm = installer.setup_plugins()
installer.ensure_config_yaml(pm)
assert os.path.exists(installer.CONFIG_FILE)
assert os.path.isdir(installer.CONFIG_DIR)
assert os.path.isdir(os.path.join(installer.CONFIG_DIR, 'jupyterhub_config.d'))
# verify that old config doesn't exist
assert not os.path.exists(os.path.join(tljh_dir, 'config.yaml'))

57
tests/test_migrator.py Normal file
View File

@@ -0,0 +1,57 @@
"""
Unit test functions in installer.py
"""
import os
from datetime import date
from tljh import migrator, config
def test_migrate_config(tljh_dir):
CONFIG_FILE = config.CONFIG_FILE
CONFIG_DIR = config.CONFIG_DIR
OLD_CONFIG_FILE = os.path.join(tljh_dir, "config.yaml")
OLD_CONFIG_D = os.path.join(tljh_dir, "jupyterhub_config.d")
CONFIG_D = os.path.join(config.CONFIG_DIR, "jupyterhub_config.d")
old_config_py = os.path.join(OLD_CONFIG_D, "upgrade.py")
new_config_py = os.path.join(CONFIG_D, "upgrade.py")
# initial condition: nothing exists
assert not os.path.exists(CONFIG_FILE)
assert not os.path.exists(OLD_CONFIG_FILE)
assert os.path.isdir(CONFIG_DIR)
# run migration with old config and no new config
upgraded_config = "old: config\n"
with open(OLD_CONFIG_FILE, "w") as f:
f.write(upgraded_config)
os.makedirs(OLD_CONFIG_D, exist_ok=True)
with open(old_config_py, "w") as f:
f.write("c.JupyterHub.log_level = 10")
migrator.migrate_config_files()
assert os.path.exists(CONFIG_FILE)
assert not os.path.exists(OLD_CONFIG_FILE)
with open(CONFIG_FILE) as f:
assert f.read() == upgraded_config
assert os.path.exists(new_config_py)
assert not os.path.exists(OLD_CONFIG_D)
# run again, this time with both old and new config
duplicate_config = "dupe: config\n"
with open(OLD_CONFIG_FILE, "w") as f:
f.write(duplicate_config)
migrator.migrate_config_files()
assert os.path.exists(CONFIG_FILE)
assert not os.path.exists(OLD_CONFIG_FILE)
# didn't clobber config:
with open(CONFIG_FILE) as f:
assert f.read() == upgraded_config
# preserved old config
backup_config = CONFIG_FILE + f".old.{date.today().isoformat()}"
assert os.path.exists(backup_config)
with open(backup_config) as f:
assert f.read() == duplicate_config
# migrate jupyterhub_con

View File

@@ -27,7 +27,8 @@ INSTALL_PREFIX = os.environ.get('TLJH_INSTALL_PREFIX', '/opt/tljh')
HUB_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'hub') HUB_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'hub')
USER_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'user') USER_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'user')
STATE_DIR = os.path.join(INSTALL_PREFIX, 'state') STATE_DIR = os.path.join(INSTALL_PREFIX, 'state')
CONFIG_FILE = os.path.join(INSTALL_PREFIX, 'config.yaml') CONFIG_DIR = os.path.join(INSTALL_PREFIX, 'config')
CONFIG_FILE = os.path.join(CONFIG_DIR, 'config.yaml')
def set_item_in_config(config, property_path, value): def set_item_in_config(config, property_path, value):
@@ -221,6 +222,9 @@ def main(argv=None):
if argv is None: if argv is None:
argv = sys.argv[1:] argv = sys.argv[1:]
from .log import init_logging
init_logging()
argparser = argparse.ArgumentParser() argparser = argparse.ArgumentParser()
argparser.add_argument( argparser.add_argument(
'--config-path', '--config-path',

View File

@@ -1,38 +1,44 @@
"""Installation logic for TLJH"""
import argparse import argparse
import itertools
import logging
import os import os
import secrets import secrets
import subprocess import subprocess
import itertools
import sys import sys
import time import time
import logging
from urllib.error import HTTPError from urllib.error import HTTPError
from urllib.request import urlopen, URLError from urllib.request import urlopen, URLError
import pluggy
import pluggy
from ruamel.yaml import YAML from ruamel.yaml import YAML
from tljh import conda, systemd, traefik, user, apt, hooks from tljh import (
from tljh.config import INSTALL_PREFIX, HUB_ENV_PREFIX, USER_ENV_PREFIX, STATE_DIR, CONFIG_FILE apt,
conda,
hooks,
migrator,
systemd,
traefik,
user,
)
from tljh.config import (
CONFIG_DIR,
CONFIG_FILE,
HUB_ENV_PREFIX,
INSTALL_PREFIX,
STATE_DIR,
USER_ENV_PREFIX,
)
HERE = os.path.abspath(os.path.dirname(__file__)) HERE = os.path.abspath(os.path.dirname(__file__))
rt_yaml = YAML() rt_yaml = YAML()
# Set up logging to print to a file and to stderr
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
os.makedirs(INSTALL_PREFIX, exist_ok=True)
file_logger = logging.FileHandler(os.path.join(INSTALL_PREFIX, 'installer.log'))
file_logger.setFormatter(logging.Formatter('%(asctime)s %(message)s'))
logger.addHandler(file_logger)
stderr_logger = logging.StreamHandler()
stderr_logger.setFormatter(logging.Formatter('%(message)s'))
logger.addHandler(stderr_logger)
logger.setLevel(logging.INFO)
def ensure_node(): def ensure_node():
""" """
Ensure nodejs from nodesource is installed Ensure nodejs from nodesource is installed
@@ -254,7 +260,7 @@ def ensure_admins(admins):
if not admins: if not admins:
return return
logger.info("Setting up admin users") logger.info("Setting up admin users")
config_path = os.path.join(INSTALL_PREFIX, 'config.yaml') config_path = CONFIG_FILE
if os.path.exists(config_path): if os.path.exists(config_path):
with open(config_path, 'r') as f: with open(config_path, 'r') as f:
config = rt_yaml.load(f) config = rt_yaml.load(f)
@@ -324,7 +330,7 @@ def ensure_symlinks(prefix):
os.symlink(tljh_config_src, tljh_config_dest) os.symlink(tljh_config_src, tljh_config_dest)
def setup_plugins(plugins): def setup_plugins(plugins=None):
""" """
Install plugins & setup a pluginmanager Install plugins & setup a pluginmanager
""" """
@@ -339,6 +345,7 @@ def setup_plugins(plugins):
return pm return pm
def run_plugin_actions(plugin_manager, plugins): def run_plugin_actions(plugin_manager, plugins):
""" """
Run installer hooks defined in plugins Run installer hooks defined in plugins
@@ -373,6 +380,12 @@ def ensure_config_yaml(plugin_manager):
""" """
Ensure we have a config.yaml present Ensure we have a config.yaml present
""" """
# ensure config dir exists and is private
for path in [CONFIG_DIR, os.path.join(CONFIG_DIR, 'jupyterhub_config.d')]:
os.makedirs(path, mode=0o700, exist_ok=True)
migrator.migrate_config_files()
if os.path.exists(CONFIG_FILE): if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'r') as f: with open(CONFIG_FILE, 'r') as f:
config = rt_yaml.load(f) config = rt_yaml.load(f)
@@ -387,6 +400,9 @@ def ensure_config_yaml(plugin_manager):
def main(): def main():
from .log import init_logging
init_logging()
argparser = argparse.ArgumentParser() argparser = argparse.ArgumentParser()
argparser.add_argument( argparser.add_argument(
'--admin', '--admin',
@@ -407,8 +423,8 @@ def main():
pm = setup_plugins(args.plugin) pm = setup_plugins(args.plugin)
ensure_config_yaml(pm)
ensure_admins(args.admin) ensure_admins(args.admin)
ensure_usergroups() ensure_usergroups()
ensure_user_environment(args.user_requirements_txt_url) ensure_user_environment(args.user_requirements_txt_url)
@@ -416,7 +432,6 @@ def main():
ensure_node() ensure_node()
ensure_jupyterhub_package(HUB_ENV_PREFIX) ensure_jupyterhub_package(HUB_ENV_PREFIX)
ensure_chp_package(HUB_ENV_PREFIX) ensure_chp_package(HUB_ENV_PREFIX)
ensure_config_yaml(pm)
ensure_jupyterlab_extensions() ensure_jupyterlab_extensions()
ensure_jupyterhub_service(HUB_ENV_PREFIX) ensure_jupyterhub_service(HUB_ENV_PREFIX)
ensure_jupyterhub_running() ensure_jupyterhub_running()

View File

@@ -8,7 +8,7 @@ from glob import glob
from systemdspawner import SystemdSpawner from systemdspawner import SystemdSpawner
from tljh import user, configurer from tljh import user, configurer
from tljh.config import INSTALL_PREFIX, USER_ENV_PREFIX from tljh.config import INSTALL_PREFIX, USER_ENV_PREFIX, CONFIG_DIR
class CustomSpawner(SystemdSpawner): class CustomSpawner(SystemdSpawner):
@@ -43,7 +43,7 @@ c.SystemdSpawner.default_shell = '/bin/bash'
# Drop the '-singleuser' suffix present in the default template # Drop the '-singleuser' suffix present in the default template
c.SystemdSpawner.unit_name_template = 'jupyter-{USERNAME}' c.SystemdSpawner.unit_name_template = 'jupyter-{USERNAME}'
config_overrides_path = os.path.join(INSTALL_PREFIX, 'config.yaml') config_overrides_path = os.path.join(CONFIG_DIR, 'config.yaml')
if os.path.exists(config_overrides_path): if os.path.exists(config_overrides_path):
with open(config_overrides_path) as f: with open(config_overrides_path) as f:
config_overrides = yaml.safe_load(f) config_overrides = yaml.safe_load(f)
@@ -53,6 +53,6 @@ configurer.apply_config(config_overrides, c)
# Load arbitrary .py config files if they exist. # Load arbitrary .py config files if they exist.
# This is our escape hatch # This is our escape hatch
extra_configs = sorted(glob(os.path.join(INSTALL_PREFIX, 'jupyterhub_config.d', '*.py'))) extra_configs = sorted(glob(os.path.join(CONFIG_DIR, 'jupyterhub_config.d', '*.py')))
for ec in extra_configs: for ec in extra_configs:
load_subconfig(ec) load_subconfig(ec)

19
tljh/log.py Normal file
View File

@@ -0,0 +1,19 @@
"""Setup tljh logging"""
import os
import logging
from .config import INSTALL_PREFIX
def init_logging():
"""Setup default tljh logger"""
logger = logging.getLogger("tljh")
os.makedirs(INSTALL_PREFIX, exist_ok=True)
file_logger = logging.FileHandler(os.path.join(INSTALL_PREFIX, "installer.log"))
file_logger.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
logger.addHandler(file_logger)
stderr_logger = logging.StreamHandler()
stderr_logger.setFormatter(logging.Formatter("%(message)s"))
logger.addHandler(stderr_logger)
logger.setLevel(logging.INFO)

69
tljh/migrator.py Normal file
View File

@@ -0,0 +1,69 @@
"""Migration utilities for upgrading tljh"""
import os
from datetime import date
import logging
import shutil
from tljh.config import (
CONFIG_DIR,
CONFIG_FILE,
INSTALL_PREFIX,
)
logger = logging.getLogger(__name__)
def migrate_file(old_path, new_path):
"""Migrate one file from an old location to a new one
avoids collisions if the new file exists
"""
if not os.path.exists(old_path):
return
if os.path.exists(new_path):
# new config file already created! still move the config,
# but avoid collision
timestamp = date.today().isoformat()
dest = dest_base = f"{new_path}.old.{timestamp}"
i = 0
while os.path.exists(dest):
# avoid collisions
dest = dest_base + f".{i}"
i += 1
logger.warning(f"Found file in both old ({old_path}) and new ({new_path}).")
logger.warning(
f"Moving {old_path} to {dest} to avoid clobbering. Its contents will be ignored."
)
else:
dest = new_path
shutil.move(old_path, dest)
def migrate_directory(old_dir, new_dir):
"""Migrate a directory to a new location"""
if not os.path.exists(old_dir):
return
if os.path.exists(new_dir):
# both dirs exist
for f in os.listdir(old_dir):
src = os.path.join(old_dir, f)
dest = os.path.join(new_dir, f)
if os.path.isdir(src):
migrate_directory(src, dest)
else:
migrate_file(src, dest)
else:
logger.warning(f"Moving directory to new location {old_dir} -> {new_dir}")
shutil.move(old_dir, new_dir)
def migrate_config_files():
"""Migrate config files to their new locations"""
# handle old TLJH_DIR/config.yaml location
migrate_file(os.path.join(INSTALL_PREFIX, "config.yaml"), CONFIG_FILE)
migrate_directory(
os.path.join(INSTALL_PREFIX, "jupyterhub_config.d"),
os.path.join(CONFIG_DIR, "jupyterhub_config.d"),
)