Load user lists & auth config from a YAML file

- Load config only once at startup.
  A lot of jupyterhub config (like user lists) take effect only at
  startup, so live reload is not super useful. It will make the
  software more complex, so let's not do it.
- Add pyyaml as a dependency of tljh.
- Remove escapism dependency since it is not actually used
This commit is contained in:
yuvipanda
2018-06-27 01:18:07 -07:00
parent 4bfc04c225
commit 459b985a19
3 changed files with 97 additions and 5 deletions

View File

@@ -11,6 +11,6 @@ setup(
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=[
'escapism==1.*' 'pyyaml==4.*'
] ]
) )

93
tljh/configurer.py Normal file
View File

@@ -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

View File

@@ -1,10 +1,9 @@
""" """
JupyterHub config for the littlest jupyterhub. JupyterHub config for the littlest jupyterhub.
""" """
from escapism import escape
import os import os
from systemdspawner import SystemdSpawner from systemdspawner import SystemdSpawner
from tljh import user from tljh import user, configurer
INSTALL_PREFIX = os.environ.get('TLJH_INSTALL_PREFIX') INSTALL_PREFIX = os.environ.get('TLJH_INSTALL_PREFIX')
USER_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'user') 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') user.remove_user_group(self.user.name, 'jupyterhub-admins')
return super().start() return super().start()
c.JupyterHub.spawner_class = CustomSpawner c.JupyterHub.spawner_class = CustomSpawner
c.JupyterHub.authenticator_class = 'dummyauthenticator.DummyAuthenticator'
c.SystemdSpawner.extra_paths = [os.path.join(USER_ENV_PREFIX, 'bin')] c.SystemdSpawner.extra_paths = [os.path.join(USER_ENV_PREFIX, 'bin')]
c.SystemdSpawner.use_sudo = True c.SystemdSpawner.use_sudo = True
configurer.apply_yaml_config('/etc/jupyterhub/jupyterhub.yaml', c)