2018-06-27 01:18:07 -07:00
|
|
|
"""
|
|
|
|
|
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.
|
|
|
|
|
"""
|
2018-07-21 00:18:58 -07:00
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
import yaml
|
|
|
|
|
|
2018-07-31 12:02:47 +02:00
|
|
|
from tljh.config import CONFIG_FILE
|
2018-07-21 00:18:58 -07:00
|
|
|
|
2018-06-27 01:18:07 -07:00
|
|
|
# Default configuration for tljh
|
|
|
|
|
# User provided config is merged into this
|
|
|
|
|
default = {
|
|
|
|
|
'auth': {
|
2018-07-16 16:05:44 -07:00
|
|
|
'type': 'firstuseauthenticator.FirstUseAuthenticator',
|
|
|
|
|
'FirstUseAuthenticator': {
|
2018-07-15 13:40:17 -07:00
|
|
|
'create_users': False
|
2018-07-03 16:28:20 -07:00
|
|
|
}
|
2018-06-27 01:18:07 -07:00
|
|
|
},
|
|
|
|
|
'users': {
|
|
|
|
|
'allowed': [],
|
|
|
|
|
'banned': [],
|
2018-07-21 00:20:29 -07:00
|
|
|
'admin': [],
|
2018-06-27 01:30:34 -07:00
|
|
|
},
|
|
|
|
|
'limits': {
|
|
|
|
|
'memory': '1G',
|
2018-07-21 00:20:29 -07:00
|
|
|
'cpu': None,
|
|
|
|
|
},
|
|
|
|
|
'http': {
|
|
|
|
|
'port': 80,
|
|
|
|
|
},
|
|
|
|
|
'https': {
|
|
|
|
|
'enabled': False,
|
|
|
|
|
'port': 443,
|
|
|
|
|
'tls': {
|
|
|
|
|
'cert': '',
|
|
|
|
|
'key': '',
|
|
|
|
|
},
|
|
|
|
|
'letsencrypt': {
|
|
|
|
|
'email': '',
|
|
|
|
|
'domains': [],
|
|
|
|
|
},
|
2018-06-29 00:47:08 -07:00
|
|
|
},
|
2018-07-15 13:40:17 -07:00
|
|
|
'user_environment': {
|
2018-07-21 00:20:29 -07:00
|
|
|
'default_app': 'classic',
|
|
|
|
|
},
|
2018-06-27 01:18:07 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2018-07-21 00:18:58 -07:00
|
|
|
def load_config(config_file=CONFIG_FILE):
|
|
|
|
|
"""Load the current config as a dictionary
|
|
|
|
|
|
|
|
|
|
merges overrides from config.yaml with default config
|
|
|
|
|
"""
|
|
|
|
|
if os.path.exists(config_file):
|
|
|
|
|
with open(config_file) as f:
|
|
|
|
|
config_overrides = yaml.safe_load(f)
|
|
|
|
|
else:
|
|
|
|
|
config_overrides = {}
|
|
|
|
|
return _merge_dictionaries(dict(default), config_overrides)
|
|
|
|
|
|
|
|
|
|
|
2018-07-15 13:40:17 -07:00
|
|
|
def apply_config(config_overrides, c):
|
|
|
|
|
"""
|
|
|
|
|
Merge config_overrides with config defaults & apply to JupyterHub config c
|
|
|
|
|
"""
|
|
|
|
|
tljh_config = _merge_dictionaries(dict(default), config_overrides)
|
2018-06-27 01:18:07 -07:00
|
|
|
|
2018-06-27 01:30:34 -07:00
|
|
|
update_auth(c, tljh_config)
|
|
|
|
|
update_userlists(c, tljh_config)
|
|
|
|
|
update_limits(c, tljh_config)
|
2018-06-29 00:47:08 -07:00
|
|
|
update_user_environment(c, tljh_config)
|
2018-07-12 13:33:24 -07:00
|
|
|
update_user_account_config(c, tljh_config)
|
2018-06-27 01:18:07 -07:00
|
|
|
|
|
|
|
|
|
2018-07-15 13:40:17 -07:00
|
|
|
def set_if_not_none(parent, key, value):
|
|
|
|
|
"""
|
|
|
|
|
Set attribute 'key' on parent if value is not None
|
|
|
|
|
"""
|
|
|
|
|
if value is not None:
|
|
|
|
|
setattr(parent, key, value)
|
|
|
|
|
|
|
|
|
|
|
2018-06-27 01:18:07 -07:00
|
|
|
def update_auth(c, config):
|
|
|
|
|
"""
|
|
|
|
|
Set auth related configuration from YAML config file
|
2018-07-15 13:40:17 -07:00
|
|
|
|
|
|
|
|
Use auth.type to determine authenticator to use. All parameters
|
|
|
|
|
in the config under auth.{auth.type} will be passed straight to the
|
|
|
|
|
authenticators themselves.
|
2018-06-27 01:18:07 -07:00
|
|
|
"""
|
|
|
|
|
auth = config.get('auth')
|
|
|
|
|
|
2018-07-16 16:05:44 -07:00
|
|
|
# FIXME: Make sure this is something importable.
|
|
|
|
|
# FIXME: SECURITY: Class must inherit from Authenticator, to prevent us being
|
|
|
|
|
# used to set arbitrary properties on arbitrary types of objects!
|
|
|
|
|
authenticator_class = auth['type']
|
|
|
|
|
# When specifying fully qualified name, use classname as key for config
|
|
|
|
|
authenticator_configname = authenticator_class.split('.')[-1]
|
2018-07-16 12:03:45 -07:00
|
|
|
c.JupyterHub.authenticator_class = authenticator_class
|
2018-07-16 01:19:24 -07:00
|
|
|
# Use just class name when setting config. If authenticator is dummyauthenticator.DummyAuthenticator,
|
|
|
|
|
# its config will be set under c.DummyAuthenticator
|
2018-07-16 12:03:45 -07:00
|
|
|
authenticator_parent = getattr(c, authenticator_class.split('.')[-1])
|
2018-07-15 13:40:17 -07:00
|
|
|
|
2018-07-16 12:03:45 -07:00
|
|
|
for k, v in auth.get(authenticator_configname, {}).items():
|
2018-07-15 13:40:17 -07:00
|
|
|
set_if_not_none(authenticator_parent, k, v)
|
2018-06-27 01:18:07 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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'])
|
|
|
|
|
|
|
|
|
|
|
2018-06-27 01:30:34 -07:00
|
|
|
def update_limits(c, config):
|
|
|
|
|
"""
|
|
|
|
|
Set user server limits
|
|
|
|
|
"""
|
|
|
|
|
limits = config['limits']
|
|
|
|
|
|
|
|
|
|
c.SystemdSpawner.mem_limit = limits['memory']
|
|
|
|
|
c.SystemdSpawner.cpu_limit = limits['cpu']
|
|
|
|
|
|
|
|
|
|
|
2018-06-29 00:47:08 -07:00
|
|
|
def update_user_environment(c, config):
|
|
|
|
|
"""
|
|
|
|
|
Set user environment configuration
|
|
|
|
|
"""
|
2018-07-15 13:40:17 -07:00
|
|
|
user_env = config['user_environment']
|
2018-06-29 00:47:08 -07:00
|
|
|
|
|
|
|
|
# Set default application users are launched into
|
2018-07-15 13:40:17 -07:00
|
|
|
if user_env['default_app'] == 'jupyterlab':
|
2018-06-29 00:47:08 -07:00
|
|
|
c.Spawner.default_url = '/lab'
|
2018-07-15 13:40:17 -07:00
|
|
|
elif user_env['default_app'] == 'nteract':
|
2018-06-29 00:47:08 -07:00
|
|
|
c.Spawner.default_url = '/nteract'
|
|
|
|
|
|
|
|
|
|
|
2018-07-12 13:33:24 -07:00
|
|
|
def update_user_account_config(c, config):
|
|
|
|
|
c.SystemdSpawner.username_template = 'jupyter-{USERNAME}'
|
|
|
|
|
|
|
|
|
|
|
2018-06-27 01:18:07 -07:00
|
|
|
def _merge_dictionaries(a, b, path=None, update=True):
|
|
|
|
|
"""
|
|
|
|
|
Merge two dictionaries recursively.
|
|
|
|
|
|
2018-07-10 11:45:07 -07:00
|
|
|
From https://stackoverflow.com/a/7205107
|
2018-06-27 01:18:07 -07:00
|
|
|
"""
|
|
|
|
|
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 update:
|
|
|
|
|
a[key] = b[key]
|
|
|
|
|
else:
|
|
|
|
|
raise Exception('Conflict at %s' % '.'.join(path + [str(key)]))
|
|
|
|
|
else:
|
|
|
|
|
a[key] = b[key]
|
|
|
|
|
return a
|