2018-08-28 14:00:43 +02:00
|
|
|
"""Installation logic for TLJH"""
|
|
|
|
|
|
2018-07-19 00:02:15 -07:00
|
|
|
import argparse
|
2019-07-10 15:07:40 +03:00
|
|
|
import dbm
|
2018-08-28 14:00:43 +02:00
|
|
|
import itertools
|
|
|
|
|
import logging
|
2018-06-26 18:37:24 -07:00
|
|
|
import os
|
2018-06-27 02:00:52 -07:00
|
|
|
import secrets
|
2018-07-19 00:02:15 -07:00
|
|
|
import subprocess
|
|
|
|
|
import sys
|
2018-07-11 12:56:52 -07:00
|
|
|
import time
|
2019-05-29 12:35:19 +03:00
|
|
|
import warnings
|
2018-07-19 00:02:15 -07:00
|
|
|
|
2019-07-10 15:07:40 +03:00
|
|
|
import bcrypt
|
2018-08-28 14:00:43 +02:00
|
|
|
import pluggy
|
2019-05-24 14:37:42 +03:00
|
|
|
import requests
|
2019-05-29 12:35:19 +03:00
|
|
|
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
2019-07-10 15:07:40 +03:00
|
|
|
from getpass import getpass
|
2018-06-26 18:37:24 -07:00
|
|
|
|
2018-08-31 12:17:16 +02:00
|
|
|
from tljh import (
|
|
|
|
|
apt,
|
|
|
|
|
conda,
|
|
|
|
|
hooks,
|
|
|
|
|
migrator,
|
|
|
|
|
systemd,
|
|
|
|
|
traefik,
|
|
|
|
|
user,
|
2019-05-19 13:45:57 -07:00
|
|
|
utils
|
2018-08-31 12:17:16 +02:00
|
|
|
)
|
2018-11-01 11:34:16 +01:00
|
|
|
from .config import (
|
2018-08-28 14:00:43 +02:00
|
|
|
CONFIG_DIR,
|
|
|
|
|
CONFIG_FILE,
|
|
|
|
|
HUB_ENV_PREFIX,
|
|
|
|
|
INSTALL_PREFIX,
|
|
|
|
|
STATE_DIR,
|
|
|
|
|
USER_ENV_PREFIX,
|
|
|
|
|
)
|
2018-11-01 11:34:16 +01:00
|
|
|
from .yaml import yaml
|
2018-06-26 18:37:24 -07:00
|
|
|
|
|
|
|
|
HERE = os.path.abspath(os.path.dirname(__file__))
|
|
|
|
|
|
2018-12-12 14:45:39 -05:00
|
|
|
logger = logging.getLogger("tljh")
|
2018-07-29 02:17:12 -07:00
|
|
|
|
2018-07-19 17:30:09 -07:00
|
|
|
def ensure_node():
|
|
|
|
|
"""
|
|
|
|
|
Ensure nodejs from nodesource is installed
|
|
|
|
|
"""
|
|
|
|
|
key = b"""
|
|
|
|
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
|
|
|
Version: GnuPG v1
|
|
|
|
|
Comment: GPGTools - https://gpgtools.org
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
=CLGF
|
|
|
|
|
-----END PGP PUBLIC KEY BLOCK-----
|
|
|
|
|
""".strip()
|
|
|
|
|
apt.trust_gpg_key(key)
|
2018-11-23 11:57:44 -08:00
|
|
|
apt.add_source('nodesource', 'https://deb.nodesource.com/node_10.x', 'main')
|
2018-07-19 17:30:09 -07:00
|
|
|
apt.install_packages(['nodejs'])
|
|
|
|
|
|
2019-02-18 15:08:53 +02:00
|
|
|
def remove_chp():
|
2018-07-19 17:30:09 -07:00
|
|
|
"""
|
2019-02-18 15:08:53 +02:00
|
|
|
Ensure CHP is not running
|
2018-07-19 17:30:09 -07:00
|
|
|
"""
|
2019-02-19 17:28:43 +02:00
|
|
|
if os.path.exists("/etc/systemd/system/configurable-http-proxy.service"):
|
2019-02-18 15:08:53 +02:00
|
|
|
if systemd.check_service_active('configurable-http-proxy.service'):
|
|
|
|
|
try:
|
|
|
|
|
systemd.stop_service('configurable-http-proxy.service')
|
|
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
|
logger.info("Cannot stop configurable-http-proxy...")
|
|
|
|
|
if systemd.check_service_enabled('configurable-http-proxy.service'):
|
|
|
|
|
try:
|
|
|
|
|
systemd.disable_service('configurable-http-proxy.service')
|
|
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
|
logger.info("Cannot disable configurable-http-proxy...")
|
|
|
|
|
try:
|
|
|
|
|
systemd.uninstall_unit('configurable-http-proxy.service')
|
|
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
|
logger.info("Cannot uninstall configurable-http-proxy...")
|
2018-07-19 17:30:09 -07:00
|
|
|
|
|
|
|
|
|
2018-06-26 18:37:24 -07:00
|
|
|
def ensure_jupyterhub_service(prefix):
|
2018-06-27 02:00:52 -07:00
|
|
|
"""
|
2018-07-21 00:20:29 -07:00
|
|
|
Ensure JupyterHub Services are set up properly
|
2018-07-03 16:18:32 -07:00
|
|
|
"""
|
2018-07-22 23:56:58 -07:00
|
|
|
|
2019-02-18 15:08:53 +02:00
|
|
|
remove_chp()
|
|
|
|
|
systemd.reload_daemon()
|
|
|
|
|
|
2018-06-26 18:37:24 -07:00
|
|
|
with open(os.path.join(HERE, 'systemd-units', 'jupyterhub.service')) as f:
|
2018-06-27 02:00:52 -07:00
|
|
|
hub_unit_template = f.read()
|
2018-06-26 18:37:24 -07:00
|
|
|
|
2019-02-13 14:10:28 +02:00
|
|
|
|
2018-07-21 00:20:29 -07:00
|
|
|
with open(os.path.join(HERE, 'systemd-units', 'traefik.service')) as f:
|
|
|
|
|
traefik_unit_template = f.read()
|
|
|
|
|
|
2019-02-13 14:10:28 +02:00
|
|
|
#Set up proxy / hub secret token if it is not already setup
|
|
|
|
|
proxy_secret_path = os.path.join(STATE_DIR, 'traefik-api.secret')
|
|
|
|
|
if not os.path.exists(proxy_secret_path):
|
|
|
|
|
with open(proxy_secret_path, 'w') as f:
|
|
|
|
|
f.write(secrets.token_hex(32))
|
|
|
|
|
|
2018-07-21 00:20:29 -07:00
|
|
|
traefik.ensure_traefik_config(STATE_DIR)
|
|
|
|
|
|
2018-06-27 02:00:52 -07:00
|
|
|
unit_params = dict(
|
2018-06-26 18:37:24 -07:00
|
|
|
python_interpreter_path=sys.executable,
|
|
|
|
|
jupyterhub_config_path=os.path.join(HERE, 'jupyterhub_config.py'),
|
2018-07-21 00:20:29 -07:00
|
|
|
install_prefix=INSTALL_PREFIX,
|
2018-06-26 18:37:24 -07:00
|
|
|
)
|
2018-06-27 02:00:52 -07:00
|
|
|
systemd.install_unit('jupyterhub.service', hub_unit_template.format(**unit_params))
|
2018-07-21 00:20:29 -07:00
|
|
|
systemd.install_unit('traefik.service', traefik_unit_template.format(**unit_params))
|
2018-06-28 00:06:11 -07:00
|
|
|
systemd.reload_daemon()
|
2018-06-27 02:00:52 -07:00
|
|
|
|
2018-06-28 00:06:11 -07:00
|
|
|
# If JupyterHub is running, we want to restart it.
|
|
|
|
|
systemd.restart_service('jupyterhub')
|
2018-07-21 00:20:29 -07:00
|
|
|
systemd.restart_service('traefik')
|
2018-06-27 14:21:08 -07:00
|
|
|
|
2019-02-13 14:10:28 +02:00
|
|
|
# Mark JupyterHub & traefik to start at boot time
|
2018-06-28 00:49:36 -07:00
|
|
|
systemd.enable_service('jupyterhub')
|
2018-07-21 00:20:29 -07:00
|
|
|
systemd.enable_service('traefik')
|
2018-06-28 00:49:36 -07:00
|
|
|
|
2018-06-26 18:37:24 -07:00
|
|
|
|
2018-08-13 15:36:20 -06:00
|
|
|
def ensure_jupyterlab_extensions():
|
|
|
|
|
"""
|
|
|
|
|
Install the JupyterLab extensions we want.
|
|
|
|
|
"""
|
|
|
|
|
extensions = [
|
2018-12-22 11:06:43 -08:00
|
|
|
'@jupyterlab/hub-extension',
|
|
|
|
|
'@jupyter-widgets/jupyterlab-manager'
|
2018-08-13 15:36:20 -06:00
|
|
|
]
|
2019-05-19 13:45:57 -07:00
|
|
|
utils.run_subprocess([
|
2018-08-13 15:36:20 -06:00
|
|
|
os.path.join(USER_ENV_PREFIX, 'bin/jupyter'),
|
|
|
|
|
'labextension',
|
|
|
|
|
'install'
|
|
|
|
|
] + extensions)
|
|
|
|
|
|
|
|
|
|
|
2018-06-26 18:37:24 -07:00
|
|
|
def ensure_jupyterhub_package(prefix):
|
|
|
|
|
"""
|
|
|
|
|
Install JupyterHub into our conda environment if needed.
|
|
|
|
|
|
2018-07-16 01:28:18 -07:00
|
|
|
We install all python packages from PyPI as much as possible in the
|
|
|
|
|
hub environment. A lot of spawners & authenticators do not have conda-forge
|
|
|
|
|
packages, but do have pip packages. Keeping all python packages in the
|
|
|
|
|
hub environment be installed with pip prevents accidental mixing of python
|
|
|
|
|
and conda packages!
|
2018-06-26 18:37:24 -07:00
|
|
|
"""
|
2019-05-29 11:34:23 -07:00
|
|
|
# Install pycurl. JupyterHub prefers pycurl over SimpleHTTPClient automatically
|
|
|
|
|
# pycurl is generally more bugfree - see https://github.com/jupyterhub/the-littlest-jupyterhub/issues/289
|
|
|
|
|
# build-essential is also generally useful to everyone involved, and required for pycurl
|
|
|
|
|
apt.install_packages([
|
|
|
|
|
'libssl-dev',
|
|
|
|
|
'libcurl4-openssl-dev',
|
|
|
|
|
'build-essential'
|
|
|
|
|
])
|
|
|
|
|
conda.ensure_pip_packages(prefix, [
|
|
|
|
|
'pycurl==7.43.*'
|
|
|
|
|
])
|
|
|
|
|
|
2018-06-26 18:37:24 -07:00
|
|
|
conda.ensure_pip_packages(prefix, [
|
2019-05-03 16:39:43 +02:00
|
|
|
'jupyterhub==1.0.0',
|
2018-06-26 18:37:24 -07:00
|
|
|
'jupyterhub-dummyauthenticator==0.3.1',
|
2019-04-30 12:29:01 -07:00
|
|
|
'jupyterhub-systemdspawner==0.13',
|
2020-01-08 10:54:17 +02:00
|
|
|
'jupyterhub-firstuseauthenticator==0.13.0',
|
2019-02-20 17:18:21 +00:00
|
|
|
'jupyterhub-nativeauthenticator==0.0.4',
|
2018-07-16 16:19:16 -07:00
|
|
|
'jupyterhub-ldapauthenticator==1.2.2',
|
2019-06-05 15:17:58 +02:00
|
|
|
'jupyterhub-tmpauthenticator==0.6',
|
2020-01-08 10:54:17 +02:00
|
|
|
'oauthenticator==0.10.0',
|
2018-06-26 18:37:24 -07:00
|
|
|
])
|
2018-07-21 00:20:29 -07:00
|
|
|
traefik.ensure_traefik_binary(prefix)
|
2018-06-26 18:37:24 -07:00
|
|
|
|
|
|
|
|
|
2018-07-03 16:18:32 -07:00
|
|
|
def ensure_usergroups():
|
|
|
|
|
"""
|
|
|
|
|
Sets up user groups & sudo rules
|
|
|
|
|
"""
|
2018-07-02 15:12:26 -07:00
|
|
|
user.ensure_group('jupyterhub-admins')
|
|
|
|
|
user.ensure_group('jupyterhub-users')
|
|
|
|
|
|
2018-07-29 02:17:12 -07:00
|
|
|
logger.info("Granting passwordless sudo to JupyterHub admins...")
|
2018-07-02 15:12:26 -07:00
|
|
|
with open('/etc/sudoers.d/jupyterhub-admins', 'w') as f:
|
|
|
|
|
# JupyterHub admins should have full passwordless sudo access
|
|
|
|
|
f.write('%jupyterhub-admins ALL = (ALL) NOPASSWD: ALL\n')
|
|
|
|
|
# `sudo -E` should preserve the $PATH we set. This allows
|
|
|
|
|
# admins in jupyter terminals to do `sudo -E pip install <package>`,
|
|
|
|
|
# `pip` is in the $PATH we set in jupyterhub_config.py to include the user conda env.
|
|
|
|
|
f.write('Defaults exempt_group = jupyterhub-admins\n')
|
|
|
|
|
|
2018-07-03 16:18:32 -07:00
|
|
|
|
2018-07-18 01:00:48 -07:00
|
|
|
def ensure_user_environment(user_requirements_txt_file):
|
2018-07-03 16:18:32 -07:00
|
|
|
"""
|
|
|
|
|
Set up user conda environment with required packages
|
|
|
|
|
"""
|
2018-07-29 02:17:12 -07:00
|
|
|
logger.info("Setting up user environment...")
|
2019-09-18 16:36:52 +03:00
|
|
|
if "python3.7" in sys.executable:
|
2019-09-18 17:57:56 +03:00
|
|
|
conda_version = '4.7.10'
|
|
|
|
|
miniconda_version = '4.7.10'
|
|
|
|
|
miniconda_installer_md5 = "1c945f2b3335c7b2b15130b1b2dc5cf4"
|
2019-09-18 16:36:52 +03:00
|
|
|
else:
|
|
|
|
|
conda_version = '4.5.8'
|
|
|
|
|
miniconda_version = '4.5.4'
|
|
|
|
|
miniconda_installer_md5 = "a946ea1d0c4a642ddf0c3a26a18bb16d"
|
2018-07-19 17:30:09 -07:00
|
|
|
|
|
|
|
|
if not conda.check_miniconda_version(USER_ENV_PREFIX, miniconda_version):
|
2018-07-29 02:17:12 -07:00
|
|
|
logger.info('Downloading & setting up user environment...')
|
2018-07-19 17:30:09 -07:00
|
|
|
with conda.download_miniconda_installer(miniconda_version, miniconda_installer_md5) as installer_path:
|
|
|
|
|
conda.install_miniconda(installer_path, USER_ENV_PREFIX)
|
|
|
|
|
|
2018-07-02 15:12:26 -07:00
|
|
|
conda.ensure_conda_packages(USER_ENV_PREFIX, [
|
|
|
|
|
# Conda's latest version is on conda much more so than on PyPI.
|
2019-09-18 16:36:52 +03:00
|
|
|
'conda==' + conda_version
|
2018-07-02 15:12:26 -07:00
|
|
|
])
|
|
|
|
|
|
|
|
|
|
conda.ensure_pip_packages(USER_ENV_PREFIX, [
|
|
|
|
|
# JupyterHub + notebook package are base requirements for user environment
|
2019-05-03 16:39:43 +02:00
|
|
|
'jupyterhub==1.0.0',
|
2019-04-10 10:43:39 +02:00
|
|
|
'notebook==5.7.8',
|
2018-07-02 15:12:26 -07:00
|
|
|
# Install additional notebook frontends!
|
2019-03-30 12:56:37 +05:30
|
|
|
'jupyterlab==0.35.4',
|
|
|
|
|
'nteract-on-jupyter==2.0.7',
|
2018-07-18 15:48:40 -07:00
|
|
|
# nbgitpuller for easily pulling in Git repositories
|
2018-08-03 15:09:38 -07:00
|
|
|
'nbgitpuller==0.6.1',
|
|
|
|
|
# nbresuse to show people how much RAM they are using
|
2018-12-22 11:06:43 -08:00
|
|
|
'nbresuse==0.3.0',
|
|
|
|
|
# Most people consider ipywidgets to be part of the core notebook experience
|
2019-03-08 14:12:09 +01:00
|
|
|
'ipywidgets==7.4.2',
|
|
|
|
|
# Pin tornado
|
2019-05-03 16:39:43 +02:00
|
|
|
'tornado<6.0',
|
2018-07-02 15:12:26 -07:00
|
|
|
])
|
|
|
|
|
|
2018-07-18 01:00:48 -07:00
|
|
|
if user_requirements_txt_file:
|
|
|
|
|
# FIXME: This currently fails hard, should fail soft and not abort installer
|
|
|
|
|
conda.ensure_pip_requirements(USER_ENV_PREFIX, user_requirements_txt_file)
|
|
|
|
|
|
2018-07-03 16:18:32 -07:00
|
|
|
|
2019-07-16 11:49:15 +03:00
|
|
|
def ensure_admins(admin_password_list):
|
2018-07-03 16:18:32 -07:00
|
|
|
"""
|
|
|
|
|
Setup given list of users as admins.
|
|
|
|
|
"""
|
2019-07-16 20:18:45 +03:00
|
|
|
os.makedirs(STATE_DIR, mode=0o700, exist_ok=True)
|
|
|
|
|
|
2019-07-16 11:49:15 +03:00
|
|
|
if not admin_password_list:
|
2018-07-03 16:18:32 -07:00
|
|
|
return
|
2018-07-29 02:17:12 -07:00
|
|
|
logger.info("Setting up admin users")
|
2018-08-28 14:00:43 +02:00
|
|
|
config_path = CONFIG_FILE
|
2018-07-03 16:18:32 -07:00
|
|
|
if os.path.exists(config_path):
|
|
|
|
|
with open(config_path, 'r') as f:
|
2018-11-01 11:34:16 +01:00
|
|
|
config = yaml.load(f)
|
2018-07-03 16:18:32 -07:00
|
|
|
else:
|
|
|
|
|
config = {}
|
|
|
|
|
|
|
|
|
|
config['users'] = config.get('users', {})
|
|
|
|
|
|
2019-07-16 11:49:15 +03:00
|
|
|
db_passw = os.path.join(STATE_DIR, 'passwords.dbm')
|
|
|
|
|
|
|
|
|
|
admins = []
|
2019-07-16 20:18:45 +03:00
|
|
|
for admin_password_entry in admin_password_list:
|
|
|
|
|
for admin_password_pair in admin_password_entry:
|
2019-07-16 11:49:15 +03:00
|
|
|
if ":" in admin_password_pair:
|
|
|
|
|
admin, password = admin_password_pair.split(':')
|
|
|
|
|
admins.append(admin)
|
|
|
|
|
# Add admin:password to the db
|
|
|
|
|
password = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
|
|
|
|
|
with dbm.open(db_passw, 'c', 0o600) as db:
|
|
|
|
|
db[admin] = password
|
|
|
|
|
else:
|
|
|
|
|
admins.append(admin_password_pair)
|
|
|
|
|
config['users']['admin'] = admins
|
2019-07-10 15:07:40 +03:00
|
|
|
|
2018-07-03 16:18:32 -07:00
|
|
|
with open(config_path, 'w+') as f:
|
2018-11-01 11:34:16 +01:00
|
|
|
yaml.dump(config, f)
|
2018-07-03 16:18:32 -07:00
|
|
|
|
|
|
|
|
|
2019-01-22 16:24:38 +02:00
|
|
|
def ensure_jupyterhub_running(times=20):
|
2018-07-11 12:56:52 -07:00
|
|
|
"""
|
|
|
|
|
Ensure that JupyterHub is up and running
|
|
|
|
|
|
|
|
|
|
Loops given number of times, waiting a second each.
|
|
|
|
|
"""
|
|
|
|
|
|
2018-07-13 15:36:44 +02:00
|
|
|
for i in range(times):
|
2018-07-11 12:56:52 -07:00
|
|
|
try:
|
2018-07-29 02:17:12 -07:00
|
|
|
logger.info('Waiting for JupyterHub to come up ({}/{} tries)'.format(i + 1, times))
|
2019-05-29 12:35:19 +03:00
|
|
|
# Because we don't care at this level that SSL is valid, we can suppress
|
|
|
|
|
# InsecureRequestWarning for this request.
|
|
|
|
|
with warnings.catch_warnings():
|
|
|
|
|
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
|
|
|
|
|
requests.get('http://127.0.0.1', verify=False)
|
2018-07-11 13:01:57 -07:00
|
|
|
return
|
2019-05-24 14:37:42 +03:00
|
|
|
except requests.HTTPError as h:
|
|
|
|
|
if h.response.status_code in [404, 502, 503]:
|
2018-07-13 14:56:00 +00:00
|
|
|
# May be transient
|
2018-07-11 12:56:52 -07:00
|
|
|
time.sleep(1)
|
|
|
|
|
continue
|
|
|
|
|
# Everything else should immediately abort
|
|
|
|
|
raise
|
2019-05-24 14:37:42 +03:00
|
|
|
except requests.ConnectionError:
|
2018-07-13 14:56:00 +00:00
|
|
|
# Hub isn't up yet, sleep & loop
|
2018-07-13 15:55:41 +02:00
|
|
|
time.sleep(1)
|
|
|
|
|
continue
|
2019-05-24 14:37:42 +03:00
|
|
|
except Exception:
|
2018-07-13 15:55:41 +02:00
|
|
|
# Everything else should immediately abort
|
|
|
|
|
raise
|
2018-07-11 12:56:52 -07:00
|
|
|
|
|
|
|
|
raise Exception("Installation failed: JupyterHub did not start in {}s".format(times))
|
|
|
|
|
|
|
|
|
|
|
2018-07-31 13:16:57 -07:00
|
|
|
def ensure_symlinks(prefix):
|
|
|
|
|
"""
|
2018-08-12 21:52:04 -07:00
|
|
|
Ensure we symlink appropriate things into /usr/bin
|
2018-07-31 13:16:57 -07:00
|
|
|
|
|
|
|
|
We add the user conda environment to PATH for notebook terminals,
|
|
|
|
|
but not the hub venv. This means tljh-config is not actually accessible.
|
|
|
|
|
|
2018-08-12 21:52:04 -07:00
|
|
|
We symlink to /usr/bin and not /usr/local/bin, since /usr/local/bin is
|
|
|
|
|
not place, and works with sudo -E in sudo's search $PATH. We can work
|
|
|
|
|
around this with sudo -E and extra entries in the sudoers file, but this
|
|
|
|
|
is far more secure at the cost of upsetting some FHS purists.
|
2018-07-31 13:16:57 -07:00
|
|
|
"""
|
|
|
|
|
tljh_config_src = os.path.join(prefix, 'bin', 'tljh-config')
|
2018-08-12 21:52:04 -07:00
|
|
|
tljh_config_dest = '/usr/bin/tljh-config'
|
2018-08-01 18:53:50 -07:00
|
|
|
if os.path.exists(tljh_config_dest):
|
|
|
|
|
if os.path.realpath(tljh_config_dest) != tljh_config_src:
|
|
|
|
|
# tljh-config exists that isn't ours. We should *not* delete this file,
|
|
|
|
|
# instead we throw an error and abort. Deleting files owned by other people
|
|
|
|
|
# while running as root is dangerous, especially with symlinks involved.
|
2018-08-12 21:52:04 -07:00
|
|
|
raise FileExistsError(f'/usr/bin/tljh-config exists but is not a symlink to {tljh_config_src}')
|
2018-08-01 18:53:50 -07:00
|
|
|
else:
|
|
|
|
|
# We have a working symlink, so do nothing
|
|
|
|
|
return
|
|
|
|
|
os.symlink(tljh_config_src, tljh_config_dest)
|
2018-07-31 13:16:57 -07:00
|
|
|
|
2018-08-11 01:19:51 -07:00
|
|
|
|
2018-08-28 14:16:09 +02:00
|
|
|
def setup_plugins(plugins=None):
|
2018-08-11 00:42:28 -07:00
|
|
|
"""
|
2018-08-11 01:19:51 -07:00
|
|
|
Install plugins & setup a pluginmanager
|
2018-08-11 00:42:28 -07:00
|
|
|
"""
|
|
|
|
|
# Install plugins
|
|
|
|
|
if plugins:
|
|
|
|
|
conda.ensure_pip_packages(HUB_ENV_PREFIX, plugins)
|
|
|
|
|
|
|
|
|
|
# Set up plugin infrastructure
|
|
|
|
|
pm = pluggy.PluginManager('tljh')
|
|
|
|
|
pm.add_hookspecs(hooks)
|
|
|
|
|
pm.load_setuptools_entrypoints('tljh')
|
|
|
|
|
|
2018-08-11 01:19:51 -07:00
|
|
|
return pm
|
|
|
|
|
|
2018-08-28 14:00:43 +02:00
|
|
|
|
2018-08-11 01:19:51 -07:00
|
|
|
def run_plugin_actions(plugin_manager, plugins):
|
|
|
|
|
"""
|
|
|
|
|
Run installer hooks defined in plugins
|
|
|
|
|
"""
|
|
|
|
|
hook = plugin_manager.hook
|
2018-08-11 00:42:28 -07:00
|
|
|
# Install apt packages
|
2018-08-11 01:19:51 -07:00
|
|
|
apt_packages = list(set(itertools.chain(*hook.tljh_extra_apt_packages())))
|
2018-08-11 00:42:28 -07:00
|
|
|
if apt_packages:
|
|
|
|
|
logger.info('Installing {} apt packages collected from plugins: {}'.format(
|
|
|
|
|
len(apt_packages), ' '.join(apt_packages)
|
|
|
|
|
))
|
|
|
|
|
apt.install_packages(apt_packages)
|
|
|
|
|
|
2019-05-31 16:52:51 -07:00
|
|
|
# Install hub pip packages
|
|
|
|
|
hub_pip_packages = list(set(itertools.chain(*hook.tljh_extra_hub_pip_packages())))
|
|
|
|
|
if hub_pip_packages:
|
|
|
|
|
logger.info('Installing {} hub pip packages collected from plugins: {}'.format(
|
|
|
|
|
len(hub_pip_packages), ' '.join(hub_pip_packages)
|
|
|
|
|
))
|
|
|
|
|
conda.ensure_pip_packages(HUB_ENV_PREFIX, hub_pip_packages)
|
|
|
|
|
|
2018-08-11 00:42:28 -07:00
|
|
|
# Install conda packages
|
2018-08-11 01:19:51 -07:00
|
|
|
conda_packages = list(set(itertools.chain(*hook.tljh_extra_user_conda_packages())))
|
2018-08-11 00:42:28 -07:00
|
|
|
if conda_packages:
|
2019-05-31 16:52:51 -07:00
|
|
|
logger.info('Installing {} user conda packages collected from plugins: {}'.format(
|
2018-08-11 00:42:28 -07:00
|
|
|
len(conda_packages), ' '.join(conda_packages)
|
|
|
|
|
))
|
|
|
|
|
conda.ensure_conda_packages(USER_ENV_PREFIX, conda_packages)
|
|
|
|
|
|
|
|
|
|
# Install pip packages
|
2019-05-31 16:52:51 -07:00
|
|
|
user_pip_packages = list(set(itertools.chain(*hook.tljh_extra_user_pip_packages())))
|
|
|
|
|
if user_pip_packages:
|
|
|
|
|
logger.info('Installing {} user pip packages collected from plugins: {}'.format(
|
|
|
|
|
len(user_pip_packages), ' '.join(user_pip_packages)
|
2018-08-11 00:42:28 -07:00
|
|
|
))
|
2019-05-31 16:52:51 -07:00
|
|
|
conda.ensure_pip_packages(USER_ENV_PREFIX, user_pip_packages)
|
2018-08-11 00:42:28 -07:00
|
|
|
|
2019-06-27 11:45:36 +02:00
|
|
|
# Custom post install actions
|
|
|
|
|
hook.tljh_post_install()
|
|
|
|
|
|
2018-07-31 13:16:57 -07:00
|
|
|
|
2018-08-11 01:19:51 -07:00
|
|
|
def ensure_config_yaml(plugin_manager):
|
|
|
|
|
"""
|
|
|
|
|
Ensure we have a config.yaml present
|
|
|
|
|
"""
|
2018-08-28 14:00:43 +02:00
|
|
|
# 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)
|
|
|
|
|
|
2018-08-31 12:17:16 +02:00
|
|
|
migrator.migrate_config_files()
|
2018-08-28 14:00:43 +02:00
|
|
|
|
2018-08-11 01:19:51 -07:00
|
|
|
if os.path.exists(CONFIG_FILE):
|
|
|
|
|
with open(CONFIG_FILE, 'r') as f:
|
2018-11-01 11:34:16 +01:00
|
|
|
config = yaml.load(f)
|
2018-08-11 01:19:51 -07:00
|
|
|
else:
|
|
|
|
|
config = {}
|
|
|
|
|
|
|
|
|
|
hook = plugin_manager.hook
|
|
|
|
|
hook.tljh_config_post_install(config=config)
|
|
|
|
|
|
|
|
|
|
with open(CONFIG_FILE, 'w+') as f:
|
2018-11-01 11:34:16 +01:00
|
|
|
yaml.dump(config, f)
|
2018-08-11 01:19:51 -07:00
|
|
|
|
|
|
|
|
|
2018-07-03 16:18:32 -07:00
|
|
|
def main():
|
2018-08-31 12:00:42 +02:00
|
|
|
from .log import init_logging
|
|
|
|
|
init_logging()
|
|
|
|
|
|
2018-07-03 16:18:32 -07:00
|
|
|
argparser = argparse.ArgumentParser()
|
|
|
|
|
argparser.add_argument(
|
|
|
|
|
'--admin',
|
|
|
|
|
nargs='*',
|
2019-07-11 20:51:51 +03:00
|
|
|
action='append',
|
2018-07-03 16:18:32 -07:00
|
|
|
help='List of usernames set to be admin'
|
|
|
|
|
)
|
2018-07-18 01:00:48 -07:00
|
|
|
argparser.add_argument(
|
|
|
|
|
'--user-requirements-txt-url',
|
|
|
|
|
help='URL to a requirements.txt file that should be installed in the user enviornment'
|
|
|
|
|
)
|
2018-08-11 00:42:28 -07:00
|
|
|
argparser.add_argument(
|
|
|
|
|
'--plugin',
|
|
|
|
|
nargs='*',
|
|
|
|
|
help='Plugin pip-specs to install'
|
|
|
|
|
)
|
2018-07-03 16:18:32 -07:00
|
|
|
|
|
|
|
|
args = argparser.parse_args()
|
|
|
|
|
|
2018-08-11 01:19:51 -07:00
|
|
|
pm = setup_plugins(args.plugin)
|
|
|
|
|
|
2018-08-28 14:00:43 +02:00
|
|
|
ensure_config_yaml(pm)
|
2019-07-16 11:49:15 +03:00
|
|
|
ensure_admins(args.admin)
|
2018-07-03 16:18:32 -07:00
|
|
|
ensure_usergroups()
|
2018-07-18 01:00:48 -07:00
|
|
|
ensure_user_environment(args.user_requirements_txt_url)
|
2018-07-03 16:18:32 -07:00
|
|
|
|
2018-07-29 02:17:12 -07:00
|
|
|
logger.info("Setting up JupyterHub...")
|
2018-07-19 17:30:09 -07:00
|
|
|
ensure_node()
|
2018-07-03 16:18:32 -07:00
|
|
|
ensure_jupyterhub_package(HUB_ENV_PREFIX)
|
2018-08-13 15:36:20 -06:00
|
|
|
ensure_jupyterlab_extensions()
|
2018-07-03 16:18:32 -07:00
|
|
|
ensure_jupyterhub_service(HUB_ENV_PREFIX)
|
2018-07-11 12:56:52 -07:00
|
|
|
ensure_jupyterhub_running()
|
2018-07-31 13:16:57 -07:00
|
|
|
ensure_symlinks(HUB_ENV_PREFIX)
|
2018-07-03 16:18:32 -07:00
|
|
|
|
2018-08-11 00:42:28 -07:00
|
|
|
# Run installer plugins last
|
2018-08-11 01:19:51 -07:00
|
|
|
run_plugin_actions(pm, args.plugin)
|
2018-08-11 00:42:28 -07:00
|
|
|
|
2018-07-29 02:17:12 -07:00
|
|
|
logger.info("Done!")
|
2018-07-02 15:12:26 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
main()
|