import argparse import os import secrets import subprocess import sys import time from urllib.error import HTTPError from urllib.request import urlopen, URLError from ruamel.yaml import YAML from tljh import conda, systemd, user, apt INSTALL_PREFIX = os.environ.get('TLJH_INSTALL_PREFIX', '/opt/tljh') HUB_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'hub') USER_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'user') STATE_DIR = os.path.join(INSTALL_PREFIX, 'state') HERE = os.path.abspath(os.path.dirname(__file__)) rt_yaml = YAML() 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) apt.add_source('nodesource', f'https://deb.nodesource.com/node_10.x', 'main') apt.install_packages(['nodejs']) def ensure_chp_package(prefix): """ Ensure CHP is installed """ if not os.path.exists(os.path.join(prefix, 'node_modules', '.bin', 'configurable-http-proxy')): subprocess.check_call([ 'npm', 'install', 'configurable-http-proxy@3.1.0' ], cwd=prefix) def ensure_jupyterhub_service(prefix): """ Ensure JupyterHub & CHP Services are set up properly """ with open(os.path.join(HERE, 'systemd-units', 'jupyterhub.service')) as f: hub_unit_template = f.read() with open(os.path.join(HERE, 'systemd-units', 'configurable-http-proxy.service')) as f: proxy_unit_template = f.read() unit_params = dict( python_interpreter_path=sys.executable, jupyterhub_config_path=os.path.join(HERE, 'jupyterhub_config.py'), install_prefix=INSTALL_PREFIX ) systemd.install_unit('configurable-http-proxy.service', proxy_unit_template.format(**unit_params)) systemd.install_unit('jupyterhub.service', hub_unit_template.format(**unit_params)) systemd.reload_daemon() os.makedirs(STATE_DIR, mode=0o700, exist_ok=True) # Set up proxy / hub secret oken if it is not already setup proxy_secret_path = os.path.join(STATE_DIR, 'configurable-http-proxy.secret') if not os.path.exists(proxy_secret_path): with open(proxy_secret_path, 'w') as f: f.write('CONFIGPROXY_AUTH_TOKEN=' + secrets.token_hex(32)) # If we are changing CONFIGPROXY_AUTH_TOKEN, restart configurable-http-proxy! systemd.restart_service('configurable-http-proxy') # Start CHP if it has already not been started systemd.start_service('configurable-http-proxy') # If JupyterHub is running, we want to restart it. systemd.restart_service('jupyterhub') # Mark JupyterHub & CHP to start at boot ime systemd.enable_service('jupyterhub') systemd.enable_service('configurable-http-proxy') def ensure_jupyterhub_package(prefix): """ Install JupyterHub into our conda environment if needed. 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! """ conda.ensure_pip_packages(prefix, [ 'jupyterhub==0.9.1', 'jupyterhub-dummyauthenticator==0.3.1', 'jupyterhub-systemdspawner==0.11', 'jupyterhub-firstuseauthenticator==0.10', 'jupyterhub-ldapauthenticator==1.2.2', 'oauthenticator==0.7.3', ]) def ensure_usergroups(): """ Sets up user groups & sudo rules """ user.ensure_group('jupyterhub-admins') user.ensure_group('jupyterhub-users') print("Granting passwordless sudo to JupyterHub admins...") 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 `, # `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') def ensure_user_environment(user_requirements_txt_file): """ Set up user conda environment with required packages """ print("Setting up user environment...") miniconda_version = '4.5.4' miniconda_installer_md5 = "a946ea1d0c4a642ddf0c3a26a18bb16d" if not conda.check_miniconda_version(USER_ENV_PREFIX, miniconda_version): print('Downloading & setting up user environment...') with conda.download_miniconda_installer(miniconda_version, miniconda_installer_md5) as installer_path: conda.install_miniconda(installer_path, USER_ENV_PREFIX) conda.ensure_conda_packages(USER_ENV_PREFIX, [ # Conda's latest version is on conda much more so than on PyPI. 'conda==4.5.8' ]) conda.ensure_pip_packages(USER_ENV_PREFIX, [ # JupyterHub + notebook package are base requirements for user environment 'jupyterhub==0.9.1', 'notebook==5.6.0', # Install additional notebook frontends! 'jupyterlab==0.32.1', 'nteract-on-jupyter==1.8.1', # nbgitpuller for easily pulling in Git repositories 'nbgitpuller==0.6.1' ]) 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) def ensure_admins(admins): """ Setup given list of users as admins. """ if not admins: return print("Setting up admin users") config_path = os.path.join(INSTALL_PREFIX, 'config.yaml') if os.path.exists(config_path): with open(config_path, 'r') as f: config = rt_yaml.load(f) else: config = {} config['users'] = config.get('users', {}) config['users']['admin'] = list(admins) with open(config_path, 'w+') as f: rt_yaml.dump(config, f) def ensure_jupyterhub_running(times=4): """ Ensure that JupyterHub is up and running Loops given number of times, waiting a second each. """ for i in range(times): try: print('Waiting for JupyterHub to come up ({}/{} tries)'.format(i + 1, times)) urlopen('http://127.0.0.1') return except HTTPError as h: if h.code in [404, 503]: # May be transient time.sleep(1) continue # Everything else should immediately abort raise except URLError as e: if isinstance(e.reason, ConnectionRefusedError): # Hub isn't up yet, sleep & loop time.sleep(1) continue # Everything else should immediately abort raise raise Exception("Installation failed: JupyterHub did not start in {}s".format(times)) def main(): argparser = argparse.ArgumentParser() argparser.add_argument( '--admin', nargs='*', help='List of usernames set to be admin' ) argparser.add_argument( '--user-requirements-txt-url', help='URL to a requirements.txt file that should be installed in the user enviornment' ) args = argparser.parse_args() ensure_admins(args.admin) ensure_usergroups() ensure_user_environment(args.user_requirements_txt_url) print("Setting up JupyterHub...") ensure_node() ensure_jupyterhub_package(HUB_ENV_PREFIX) ensure_chp_package(HUB_ENV_PREFIX) ensure_jupyterhub_service(HUB_ENV_PREFIX) ensure_jupyterhub_running() print("Done!") if __name__ == '__main__': main()