diff --git a/setup.py b/setup.py index 07c630b..5909b90 100644 --- a/setup.py +++ b/setup.py @@ -11,6 +11,6 @@ setup( packages=find_packages(), include_package_data=True, install_requires=[ - 'escapism==1.*' + 'pyyaml==4.*' ] ) diff --git a/tljh/configurer.py b/tljh/configurer.py new file mode 100644 index 0000000..abadb46 --- /dev/null +++ b/tljh/configurer.py @@ -0,0 +1,93 @@ +""" +Parse YAML config file & update JupyterHub config. + +Config should never append or mutate, only set. Functions here could +be called many times per lifetime of a jupyterhub. + +Traitlets that modify the startup of JupyterHub should not be here. +FIXME: A strong feeling that JSON Schema should be involved somehow. +""" +import copy +import os +import yaml + +# Default configuration for tljh +# User provided config is merged into this +default = { + 'auth': { + 'type': 'dummy', + 'dummy': {} + }, + 'users': { + 'allowed': [], + 'banned': [], + 'admin': [] + } +} + + +def apply_yaml_config(path, c): + if not os.path.exists(path): + user_config = copy.deepcopy(default) + + with open(path) as f: + user_config = _merge_dictionaries(yaml.safe_load(f), default) + + update_auth(c, user_config) + update_userlists(c, user_config) + + +def update_auth(c, config): + """ + Set auth related configuration from YAML config file + """ + auth = config.get('auth') + + if auth['type'] == 'dummy': + c.JupyterHub.authenticator_class = 'dummyauthenticator.DummyAuthenticator' + password = auth['dummy'].get('password') + if password is not None: + c.DummyAuthenticator.password = password + return + + +def update_userlists(c, config): + """ + Set user whitelists & admin lists + """ + users = config['users'] + + c.Authenticator.whitelist = set(users['allowed']) + c.Authenticator.blacklist = set(users['banned']) + c.Authenticator.admin_users = set(users['admin']) + + +def _merge_dictionaries(a, b, path=None, update=True): + """ + Merge two dictionaries recursively. + + From https://stackoverflow.com/a/25270947 + """ + if path is None: + path = [] + for key in b: + if key in a: + if isinstance(a[key], dict) and isinstance(b[key], dict): + _merge_dictionaries(a[key], b[key], path + [str(key)]) + elif a[key] == b[key]: + pass # same leaf value + elif isinstance(a[key], list) and isinstance(b[key], list): + for idx, val in enumerate(b[key]): + a[key][idx] = _merge_dictionaries( + a[key][idx], + b[key][idx], + path + [str(key), str(idx)], + update=update + ) + elif update: + a[key] = b[key] + else: + raise Exception('Conflict at %s' % '.'.join(path + [str(key)])) + else: + a[key] = b[key] + return a diff --git a/tljh/jupyterhub_config.py b/tljh/jupyterhub_config.py index 0e10ba6..12ec5f3 100644 --- a/tljh/jupyterhub_config.py +++ b/tljh/jupyterhub_config.py @@ -1,10 +1,9 @@ """ JupyterHub config for the littlest jupyterhub. """ -from escapism import escape import os from systemdspawner import SystemdSpawner -from tljh import user +from tljh import user, configurer INSTALL_PREFIX = os.environ.get('TLJH_INSTALL_PREFIX') USER_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'user') @@ -23,9 +22,9 @@ class CustomSpawner(SystemdSpawner): user.remove_user_group(self.user.name, 'jupyterhub-admins') return super().start() - c.JupyterHub.spawner_class = CustomSpawner -c.JupyterHub.authenticator_class = 'dummyauthenticator.DummyAuthenticator' c.SystemdSpawner.extra_paths = [os.path.join(USER_ENV_PREFIX, 'bin')] c.SystemdSpawner.use_sudo = True + +configurer.apply_yaml_config('/etc/jupyterhub/jupyterhub.yaml', c)