├── files ├── jupyterhub.conf ├── ipython_config.py ├── usercustomize.py ├── page.html ├── jupyterhub_config.json ├── jupyterhub-login └── jupyterhub-auth ├── hiera.yaml ├── templates ├── 99-jupyterhub-user.epp ├── kernel.json.epp ├── jupyterhub.service.epp ├── submit.sh.epp ├── jupyterhub.conf.epp ├── hub-requirements.txt.epp └── node-requirements.txt.epp ├── metadata.json ├── LICENSE ├── manifests ├── uv.pp ├── kernel.pp ├── node.pp └── init.pp ├── data └── common.yaml └── README.md /files/jupyterhub.conf: -------------------------------------------------------------------------------- 1 | d /run/jupyterhub 0755 jupyterhub jupyterhub - -------------------------------------------------------------------------------- /hiera.yaml: -------------------------------------------------------------------------------- 1 | # jupyterhub/hiera.yaml 2 | --- 3 | version: 5 4 | defaults: 5 | datadir: data 6 | data_hash: yaml_data 7 | hierarchy: 8 | - name: "common" 9 | path: "common.yaml" 10 | -------------------------------------------------------------------------------- /files/ipython_config.py: -------------------------------------------------------------------------------- 1 | c = get_config() #noqa 2 | # Move IPython history to in-memory sqlite instead of on-disk. 3 | # This avoids kernel hanging issues with network filesystem like Lustre and NFS. 4 | c.HistoryAccessor.hist_file = ":memory:" -------------------------------------------------------------------------------- /templates/99-jupyterhub-user.epp: -------------------------------------------------------------------------------- 1 | Runas_Alias BLOCKED_USERS = <%= $blocked_users.join(', ') %> 2 | 3 | jupyterhub <%= $hostname %>=(ALL,!BLOCKED_USERS) NOPASSWD:NOEXEC:SETENV: <%= $slurm_home %>/bin/sbatch --parsable 4 | jupyterhub <%= $hostname %>=(ALL,!BLOCKED_USERS) NOPASSWD:NOEXEC:NOSETENV: <%= $slurm_home %>/bin/scancel [0-9]* 5 | -------------------------------------------------------------------------------- /templates/kernel.json.epp: -------------------------------------------------------------------------------- 1 | { 2 | "argv": [ 3 | "<%= $prefix %>/bin/python", 4 | "-m", 5 | "ipykernel_launcher", 6 | "-f", 7 | "{connection_file}" 8 | ], 9 | "display_name": "<%= $display_name %>", 10 | "language": "python", 11 | "metadata": { 12 | "debugger": true 13 | }, 14 | "env": <%= $env.to_json_pretty %> 15 | } 16 | -------------------------------------------------------------------------------- /templates/jupyterhub.service.epp: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Jupyterhub 3 | After=network-online.target 4 | 5 | [Service] 6 | User=jupyterhub 7 | Group=jupyterhub 8 | Environment=PATH=/bin:/usr/bin:<%= $prefix %>/bin:<%= $slurm_home %>/bin 9 | ExecStart=<%= $prefix %>/bin/jupyterhub --config /etc/jupyterhub/jupyterhub_config.json 10 | WorkingDirectory=/run/jupyterhub 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /files/usercustomize.py: -------------------------------------------------------------------------------- 1 | import site 2 | import os 3 | import sys 4 | 5 | pip_prefix = os.environ.get('PIP_PREFIX', None) 6 | if pip_prefix: 7 | path = os.path.join(pip_prefix, 'lib', 'python{0}.{1}'.format(*sys.version_info), 'site-packages') 8 | # Make sure the PIP_PREFIX is prepended to sys.path and not appended 9 | # by keeping the sys path list length, all paths that were appended at the end 10 | # are moved at the beginning and the rest of the paths are appened at the end. 11 | len_syspath = len(sys.path) 12 | site.addsitedir(path) 13 | sys.path = sys.path[len_syspath:] + sys.path[:len_syspath] 14 | -------------------------------------------------------------------------------- /files/page.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/page.html" %} 2 | {% block announcement %} 3 |
4 | {% endblock %} 5 | 6 | {% block script %} 7 | {{ super() }} 8 | 23 | {% endblock %} -------------------------------------------------------------------------------- /files/jupyterhub_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "JupyterHub": { 3 | "hub_ip": "0.0.0.0", 4 | "ssl_key": "/etc/jupyterhub/ssl/key.pem", 5 | "ssl_cert": "/etc/jupyterhub/ssl/cert.pem", 6 | "cleanup_servers": false, 7 | "shutdown_on_logout": true, 8 | "template_paths": ["/etc/jupyterhub/templates"], 9 | "proxy_class": "traefik_file" 10 | }, 11 | "PAMAuthenticator": { 12 | "open_sessions": false, 13 | "service" : "jupyterhub-login" 14 | }, 15 | "Spawner": { 16 | "args": [ 17 | "--KernelSpecManager.ensure_native_kernel=False" 18 | ] 19 | }, 20 | "SlurmFormSpawner": { 21 | "submit_template_path": "/etc/jupyterhub/submit.sh" 22 | } 23 | } -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppet-jupyterhub", 3 | "version": "7.0.1", 4 | "author": "Félix-Antoine Fortin", 5 | "summary": "Install, configure, and manage JupyterHub with slurmformspawner", 6 | "license": "MIT", 7 | "source": "https://github.com/ComputeCanada/puppet-jupyterhub", 8 | "project_page": "https://github.com/ComputeCanada/puppet-jupyterhub", 9 | "issues_url": "https://github.com/ComputeCanada/puppet-jupyterhub/issues", 10 | "dependencies": [ 11 | { "name":"puppet/archive", "version_requirement": ">= 5.0.0" }, 12 | { "name":"puppet/selinux", "version_requirement": ">= 1.6.1" }, 13 | { "name":"puppetlabs/stdlib", "version_requirement": ">= 5.2.0" }, 14 | { "name":"puppet/letsencrypt", "version_requirement": ">= 5.0.0" } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /templates/submit.sh.epp: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | {% if account %}#SBATCH --account={{account}}{% endif %} 3 | #SBATCH --time={{runtime}} 4 | #SBATCH --output={{homedir}}/.jupyterhub_slurmspawner_%j.log 5 | #SBATCH --job-name=spawner-jupyterhub 6 | #SBATCH --chdir={{homedir}} 7 | #SBATCH --mem={{memory}} 8 | #SBATCH --cpus-per-task={{nprocs}} 9 | #SBATCH --export={{keepvars}} 10 | {% if oversubscribe %}#SBATCH --oversubscribe{% endif %} 11 | {% if reservation %}#SBATCH --reservation={{reservation}}{% endif %} 12 | {% if gpus != "gpu:0" %}#SBATCH --gres={{gpus}}{% endif %} 13 | {% if partition %}#SBATCH --partition={{partition}}{% endif %} 14 | 15 | <%# write any additional script here -%> 16 | <%= $additions -%> 17 | 18 | {% if modules %} 19 | module load {{modules|join(' ')}} 20 | {% endif %} 21 | 22 | <%= $prologue %> 23 | 24 | # Launch jupyterhub single server 25 | {{cmd}} 26 | -------------------------------------------------------------------------------- /files/jupyterhub-login: -------------------------------------------------------------------------------- 1 | #%PAM-1.0 2 | auth [user_unknown=ignore success=ok ignore=ignore default=bad] pam_securetty.so 3 | auth substack jupyterhub-auth 4 | auth include postlogin 5 | account required pam_nologin.so 6 | account include jupyterhub-auth 7 | password include jupyterhub-auth 8 | # pam_selinux.so close should be the first session rule 9 | session required pam_selinux.so close 10 | session required pam_loginuid.so 11 | session optional pam_console.so 12 | # pam_selinux.so open should only be followed by sessions to be executed in the user context 13 | session required pam_selinux.so open 14 | session required pam_namespace.so 15 | session optional pam_keyinit.so force revoke 16 | session include jupyterhub-auth 17 | session include postlogin 18 | -session optional pam_ck_connector.so -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Félix-Antoine Fortin, Université Laval 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /files/jupyterhub-auth: -------------------------------------------------------------------------------- 1 | #%PAM-1.0 2 | # This file is auto-generated. 3 | # User changes will be destroyed the next time authconfig is run. 4 | auth required pam_env.so 5 | auth [default=1 ignore=ignore success=ok] pam_succeed_if.so uid >= 1000 quiet 6 | auth [default=1 ignore=ignore success=ok] pam_localuser.so 7 | auth sufficient pam_unix.so nullok try_first_pass nodelay 8 | auth requisite pam_succeed_if.so uid >= 1000 quiet_success 9 | auth sufficient pam_sss.so forward_pass 10 | auth required pam_deny.so 11 | 12 | account required pam_unix.so 13 | account sufficient pam_localuser.so 14 | account sufficient pam_succeed_if.so uid < 1000 quiet 15 | account [default=bad success=ok user_unknown=ignore] pam_sss.so 16 | account required pam_permit.so 17 | 18 | password requisite pam_pwquality.so try_first_pass local_users_only retry=3 authtok_type= 19 | password sufficient pam_unix.so sha512 shadow nullok try_first_pass use_authtok 20 | password sufficient pam_sss.so use_authtok 21 | password required pam_deny.so 22 | 23 | session optional pam_keyinit.so revoke 24 | session required pam_limits.so 25 | -session optional pam_systemd.so 26 | session optional pam_oddjob_mkhomedir.so umask=0077 27 | session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid 28 | session required pam_unix.so 29 | session optional pam_sss.so -------------------------------------------------------------------------------- /templates/jupyterhub.conf.epp: -------------------------------------------------------------------------------- 1 | # top-level http config for websocket headers 2 | # If Upgrade is defined, Connection = upgrade 3 | # If Upgrade is empty, Connection = close 4 | map $http_upgrade $connection_upgrade { 5 | default upgrade; 6 | '' close; 7 | } 8 | 9 | # HTTPS server to handle JupyterHub 10 | server { 11 | listen 443 ssl http2; 12 | 13 | server_name <%= $domains.join(' ') %>; 14 | 15 | location / { 16 | client_max_body_size 50M; 17 | 18 | proxy_pass https://127.0.0.1:8000; 19 | proxy_set_header X-Real-IP $remote_addr; 20 | proxy_set_header Host $host; 21 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 22 | 23 | # websocket headers 24 | proxy_set_header Upgrade $http_upgrade; 25 | proxy_set_header Connection $connection_upgrade; 26 | } 27 | 28 | # Managing requests to verify letsencrypt host 29 | location ~ /.well-known { 30 | allow all; 31 | } 32 | 33 | ssl_stapling on; 34 | ssl_stapling_verify on; 35 | 36 | gzip off; 37 | 38 | # HSTS (ngx_http_headers_module is required) (63072000 seconds) 39 | add_header Strict-Transport-Security "max-age=63072000" always; 40 | 41 | ssl_session_timeout 1d; 42 | ssl_session_cache shared:le_nginx_SSL:10m; 43 | 44 | # intermediate configuration 45 | ssl_protocols TLSv1.2; 46 | ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; 47 | ssl_prefer_server_ciphers off; 48 | ssl_dhparam /etc/nginx/ffdhe4096.pem; 49 | 50 | <% if $use_letsencrypt { %> 51 | ssl_certificate /etc/letsencrypt/live/<%= $certname %>/fullchain.pem; 52 | ssl_certificate_key /etc/letsencrypt/live/<%= $certname %>/privkey.pem; 53 | <% } else { %> 54 | ssl_certificate <%= $ssl_certificate_path %>; 55 | ssl_certificate_key <%= $ssl_certificate_key_path %>; 56 | <% } %> 57 | } 58 | 59 | server { 60 | <% $domains.each |$domain| { -%> 61 | if ($host = <%= $domain %>) { 62 | return 301 https://$host$request_uri; 63 | } 64 | <% } -%> 65 | 66 | listen 80; 67 | server_name <%= $domains.join(' ') %>; 68 | return 404; 69 | } 70 | -------------------------------------------------------------------------------- /templates/hub-requirements.txt.epp: -------------------------------------------------------------------------------- 1 | batchspawner==<%= $batchspawner_version %> 2 | jupyterhub==<%= $jupyterhub_version %> 3 | jupyterhub-idle-culler==<%= $idle_culler_version %> 4 | jupyterhub-announcement @ https://github.com/rcthomas/jupyterhub-announcement/archive/refs/tags/<%= $announcement_version %>.zip 5 | slurmformspawner==<%= $slurmformspawner_version %> 6 | wrapspawner==<%= $wrapspawner_version %> 7 | jupyterhub-traefik-proxy==<%= $jupyterhub_traefik_proxy_version %> 8 | pamela==<%= $pamela_version %> 9 | pammfauthenticator @ https://github.com/cmd-ntrf/pammfauthenticator/archive/refs/tags/v<%= $pammfauthenticator_version %>.zip 10 | oauthenticator==<%= $oauthenticator_version %> 11 | oauth2freeipa @ https://github.com/MagicCastle/oauth2freeipa/archive/refs/tags/v<%= $oauth2freeipa_version %>.zip 12 | jupyterhub-ltiauthenticator==<%= $ltiauthenticator_version %> 13 | 14 | <% if $frozen_deps { -%> 15 | aiofiles==24.1.0 16 | aiohappyeyeballs==2.6.1 17 | aiohttp==3.12.15 18 | aiosignal==1.4.0 19 | alembic==1.16.4 20 | annotated-types==0.7.0 21 | arrow==1.3.0 22 | attrs==25.3.0 23 | bcrypt==4.3.0 24 | beautifulsoup4==4.13.4 25 | cachetools==6.1.0 26 | certifi==2025.8.3 27 | certipy==0.2.2 28 | cffi==1.17.1 29 | charset-normalizer==3.4.3 30 | cryptography==45.0.6 31 | escapism==1.0.1 32 | fqdn==1.5.1 33 | frozenlist==1.7.0 34 | greenlet==3.2.4 35 | html-sanitizer==2.6.0 36 | idna==3.10 37 | isoduration==20.11.0 38 | jinja2==3.1.6 39 | jsonpointer==3.0.0 40 | jsonschema==4.25.1 41 | jsonschema-specifications==2025.4.1 42 | jupyter-events==0.12.0 43 | lark==1.2.2 44 | lxml==6.0.1 45 | lxml-html-clean==0.4.2 46 | mako==1.3.10 47 | markupsafe==3.0.2 48 | multidict==6.6.4 49 | oauthlib==3.3.1 50 | packaging==25.0 51 | prometheus-client==0.22.1 52 | propcache==0.3.2 53 | pycparser==2.22 54 | pydantic==2.11.7 55 | pydantic-core==2.33.2 56 | pyjwt==2.10.1 57 | python-dateutil==2.9.0.post0 58 | python-json-logger==3.3.0 59 | pyyaml==6.0.2 60 | referencing==0.36.2 61 | requests==2.32.5 62 | rfc3339-validator==0.1.4 63 | rfc3986-validator==0.1.1 64 | rfc3987-syntax==1.1.0 65 | rpds-py==0.27.0 66 | six==1.17.0 67 | soupsieve==2.7 68 | sqlalchemy==2.0.43 69 | toml==0.10.2 70 | tornado==6.5.2 71 | traitlets==5.14.3 72 | types-python-dateutil==2.9.0.20250822 73 | typing-extensions==4.14.1 74 | typing-inspection==0.4.1 75 | uri-template==1.3.0 76 | urllib3==2.5.0 77 | webcolors==24.11.1 78 | wtforms==2.3.1 79 | yarl==1.20.1 80 | <% } -%> 81 | 82 | <% if $extra_packages { %><%= join($extra_packages, '\n') %><% } %> 83 | -------------------------------------------------------------------------------- /manifests/uv.pp: -------------------------------------------------------------------------------- 1 | class jupyterhub::uv::install ( 2 | String $prefix, 3 | String $version, 4 | ) { 5 | ensure_resource('file', $prefix, { 'ensure' => 'directory' }) 6 | ensure_resource('file', "${prefix}/bin", { 'ensure' => 'directory', require => File[$prefix] }) 7 | $arch = $::facts['os']['architecture'] 8 | archive { 'jh_install_uv': 9 | path => '/tmp/uv', 10 | source => "https://github.com/astral-sh/uv/releases/download/${version}/uv-${arch}-unknown-linux-gnu.tar.gz", 11 | extract => true, 12 | extract_path => "${prefix}/bin", 13 | extract_command => 'tar xfz %s --strip-components=1', 14 | creates => "${$prefix}/bin/uv", 15 | cleanup => true, 16 | require => File["${prefix}/bin"], 17 | } 18 | } 19 | 20 | define jupyterhub::uv::venv ( 21 | String $prefix, 22 | Variant[Stdlib::Absolutepath, String] $python, 23 | String $requirements, 24 | Hash[String, Variant[String, Integer, Array[String]]] $pip_environment = {}, 25 | ) { 26 | $uv_prefix = lookup('jupyterhub::uv::install::prefix') 27 | 28 | $pip_env_list = $pip_environment.reduce([]) |Array $list, Array $value| { 29 | if $value[1] =~ Stdlib::Compat::Array { 30 | $concat = $value[1].reduce('') | String $concat, String $token | { 31 | "${token}:${concat}" 32 | } 33 | $list + ["${value[0]}=${concat}"] 34 | } 35 | else { 36 | $list + ["${value[0]}=${value[1]}"] 37 | } 38 | } 39 | 40 | if $python =~ Stdlib::Absolutepath { 41 | $path = ["${uv_prefix}/bin", dirname($python),'/bin', '/usr/bin'] 42 | $environ = [] 43 | } else { 44 | $path = ["${uv_prefix}/bin"] 45 | $environ = ["XDG_DATA_HOME=${uv_prefix}/share"] 46 | } 47 | 48 | exec { "${name}_venv": 49 | command => "uv venv --seed -p ${python} ${prefix}", 50 | creates => "${prefix}/bin/python", 51 | require => Class['jupyterhub::uv::install'], 52 | path => $path, 53 | environment => $environ, 54 | } 55 | 56 | file { "${prefix}/${name}-requirements.txt": 57 | content => $requirements, 58 | } 59 | 60 | if 'PIP_CONFIG_FILE' in $pip_environment { 61 | $pip_cmd = "pip install -r ${prefix}/${name}-requirements.txt" 62 | $pip_environ = $pip_env_list 63 | $pip_path = ["${prefix}/bin"] 64 | } else { 65 | $pip_cmd = "uv pip install -r ${prefix}/${name}-requirements.txt" 66 | $pip_environ = $pip_env_list + ["VIRTUAL_ENV=${prefix}"] 67 | $pip_path = ["${uv_prefix}/bin"] 68 | } 69 | exec { "${name}_pip_install": 70 | command => $pip_cmd, 71 | subscribe => File["${prefix}/${name}-requirements.txt"], 72 | refreshonly => true, 73 | environment => $pip_environ, 74 | timeout => 0, 75 | path => $pip_path, 76 | require => Exec["${name}_venv"], 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /manifests/kernel.pp: -------------------------------------------------------------------------------- 1 | # 2 | class jupyterhub::kernel ( 3 | Enum['none', 'venv'] $install_method = 'venv', 4 | Optional[Enum['venv', 'module']] $setup = undef, 5 | ) { 6 | if $setup { 7 | deprecation('jupyterhub::kernel::setup', 'jupyterhub::kernel::setup is deprecated, use jupyterhub::kernel::install_method instead') 8 | if $setup == 'venv' { 9 | include jupyterhub::kernel::venv 10 | } 11 | } elsif $install_method == 'venv' { 12 | include jupyterhub::kernel::venv 13 | } 14 | } 15 | 16 | class jupyterhub::kernel::venv ( 17 | Stdlib::Absolutepath $prefix, 18 | Variant[Stdlib::Absolutepath, String] $python, 19 | String $kernel_name = 'python3', 20 | String $display_name = 'Python 3', 21 | Array[String] $packages = [], 22 | Hash[String, Variant[String, Integer, Array[String]]] $pip_environment = {}, 23 | Hash $kernel_environment = {} 24 | ) { 25 | include jupyterhub::uv::install 26 | 27 | jupyterhub::uv::venv { 'kernel': 28 | prefix => $prefix, 29 | python => $python, 30 | requirements => join(['ipykernel'] + $packages, "\n"), 31 | pip_environment => $pip_environment, 32 | } 33 | 34 | file { "${prefix}/etc": 35 | ensure => directory, 36 | require => Jupyterhub::Uv::Venv['kernel'], 37 | } 38 | 39 | file { "${prefix}/etc/ipython": 40 | ensure => directory, 41 | require => File["${prefix}/etc"], 42 | } 43 | 44 | file { "${prefix}/share/jupyter/kernels/python3/kernel.json": 45 | ensure => absent, 46 | require => Jupyterhub::Uv::Venv['kernel'], 47 | } 48 | 49 | file { "${prefix}/etc/ipython/ipython_config.py": 50 | source => 'puppet:///modules/jupyterhub/ipython_config.py', 51 | } 52 | 53 | ensure_resource('file', "${prefix}/puppet-jupyter", { 'ensure' => 'directory', require => Jupyterhub::Uv::Venv['kernel'] }) 54 | ensure_resource('file', "${prefix}/puppet-jupyter/kernels", { 'ensure' => 'directory', require => File["${prefix}/puppet-jupyter"] }) 55 | ensure_resource('file', "${prefix}/puppet-jupyter/kernels/${kernel_name}", { 'ensure' => 'directory', require => File["${prefix}/puppet-jupyter/kernels"] }) 56 | file { "${prefix}/puppet-jupyter/kernels/${kernel_name}/kernel.json": 57 | content => epp('jupyterhub/kernel.json', { 'prefix' => $prefix, 'display_name' => $display_name, 'env' => $kernel_environment }), 58 | require => File["${prefix}/puppet-jupyter/kernels/${kernel_name}"], 59 | mode => '0644', 60 | owner => 'root', 61 | group => 'root', 62 | } 63 | 64 | file { "${prefix}/puppet-jupyter/kernels/${kernel_name}/logo-svg.svg": 65 | source => "file://${prefix}/share/jupyter/kernels/python3/logo-svg.svg", 66 | require => [ 67 | File["${prefix}/puppet-jupyter/kernels/${kernel_name}"], 68 | Jupyterhub::Uv::Venv['kernel'], 69 | ], 70 | mode => '0644', 71 | owner => 'root', 72 | group => 'root', 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /templates/node-requirements.txt.epp: -------------------------------------------------------------------------------- 1 | jupyterhub==<%= $jupyterhub_version %> 2 | jupyterlab==<%= $jupyterlab_version %> 3 | notebook==<%= $notebook_version %> 4 | batchspawner==<%= $batchspawner_version %> 5 | <% if $jupyter_remote_desktop_proxy_version { %>jupyter-remote-desktop-proxy==<%= $jupyter_remote_desktop_proxy_version %><% } %> 6 | <% if $jupyter_rsession_proxy_version { %>jupyter-rsession-proxy==<%= $jupyter_rsession_proxy_version %><% } %> 7 | <% if $jupyter_server_proxy_version { %>jupyter-server-proxy==<%= $jupyter_server_proxy_version %><% } %> 8 | <% if $jupyterlab_nvdashboard_version { %>jupyterlab-nvdashboard==<%= $jupyterlab_nvdashboard_version %><% } %> 9 | <% if $jupyterlmod_version { %>jupyterlmod==<%= $jupyterlmod_version %><% } %> 10 | <% if $nbgitpuller_version { %>nbgitpuller==<%= $nbgitpuller_version %><% } %> 11 | <% if $ipywidgets_version { %>ipywidgets==<%= $ipywidgets_version %><% } %> 12 | <% if $widgetsnbextension_version { %>widgetsnbextension==<%= $widgetsnbextension_version %><% } %> 13 | <% if $jupyterlab_widgets_version { %>jupyterlab_widgets==<%= $jupyterlab_widgets_version %><% } %> 14 | 15 | <% if $frozen_deps { -%> 16 | aiohappyeyeballs==2.6.1 17 | aiohttp==3.12.15 18 | aiosignal==1.4.0 19 | alembic==1.16.4 20 | annotated-types==0.7.0 21 | anyio==4.10.0 22 | argon2-cffi==25.1.0 23 | argon2-cffi-bindings==25.1.0 24 | arrow==1.3.0 25 | asttokens==3.0.0 26 | async-lru==2.0.5 27 | attrs==25.3.0 28 | babel==2.17.0 29 | beautifulsoup4==4.13.4 30 | bleach==6.2.0 31 | certifi==2025.8.3 32 | certipy==0.2.2 33 | cffi==1.17.1 34 | charset-normalizer==3.4.3 35 | comm==0.2.3 36 | cryptography==45.0.6 37 | debugpy==1.8.16 38 | decorator==5.2.1 39 | defusedxml==0.7.1 40 | entrypoints==0.4 41 | executing==2.2.0 42 | fastjsonschema==2.21.2 43 | fqdn==1.5.1 44 | frozenlist==1.7.0 45 | greenlet==3.2.4 46 | h11==0.16.0 47 | httpcore==1.0.9 48 | httpx==0.28.1 49 | idna==3.10 50 | ipykernel==6.29.5 51 | ipython==9.4.0 52 | ipython-genutils==0.2.0 53 | ipython-pygments-lexers==1.1.1 54 | ipywidgets==8.1.7 55 | isoduration==20.11.0 56 | jedi==0.19.2 57 | jinja2==3.1.6 58 | json5==0.12.1 59 | jsonpointer==3.0.0 60 | jsonschema-specifications==2025.4.1 61 | jsonschema==4.25.1 62 | jupyter-client==7.4.9 63 | jupyter-core==5.8.1 64 | jupyter-events==0.12.0 65 | jupyter-lsp==2.2.6 66 | jupyter-server==2.17.0 67 | jupyter-server-terminals==0.5.3 68 | jupyterlab-pygments==0.3.0 69 | jupyterlab-server==2.27.3 70 | lark==1.2.2 71 | mako==1.3.10 72 | markupsafe==3.0.2 73 | matplotlib-inline==0.1.7 74 | mistune==3.1.3 75 | multidict==6.6.4 76 | nbclassic==1.3.1 77 | nbclient==0.10.2 78 | nbconvert==7.16.6 79 | nbformat==5.10.4 80 | nest-asyncio==1.6.0 81 | notebook-shim==0.2.4 82 | nvidia-ml-py==12.575.51 83 | oauthlib==3.3.1 84 | packaging==25.0 85 | pamela==1.2.0 86 | pandocfilters==1.5.1 87 | parso==0.8.4 88 | pexpect==4.9.0 89 | platformdirs==4.3.8 90 | prometheus-client==0.22.1 91 | prompt-toolkit==3.0.51 92 | propcache==0.3.2 93 | psutil==7.0.0 94 | ptyprocess==0.7.0 95 | pure-eval==0.2.3 96 | pycparser==2.22 97 | pydantic==2.11.7 98 | pydantic-core==2.33.2 99 | pygments==2.19.2 100 | pynvml==12.0.0 101 | python-dateutil==2.9.0.post0 102 | python-json-logger==3.3.0 103 | pyyaml==6.0.2 104 | pyzmq==27.0.2 105 | referencing==0.36.2 106 | requests==2.32.5 107 | rfc3339-validator==0.1.4 108 | rfc3986-validator==0.1.1 109 | rfc3987-syntax==1.1.0 110 | rpds-py==0.27.0 111 | send2trash==1.8.3 112 | setuptools==80.9.0 113 | simpervisor==1.0.0 114 | six==1.17.0 115 | sniffio==1.3.1 116 | soupsieve==2.7 117 | sqlalchemy==2.0.43 118 | stack-data==0.6.3 119 | terminado==0.18.1 120 | tinycss2==1.4.0 121 | tornado==6.5.2 122 | traitlets==5.14.3 123 | types-python-dateutil==2.9.0.20250822 124 | typing-extensions==4.14.1 125 | typing-inspection==0.4.1 126 | uri-template==1.3.0 127 | urllib3==2.5.0 128 | wcwidth==0.2.13 129 | webcolors==24.11.1 130 | webencodings==0.5.1 131 | websocket-client==1.8.0 132 | yarl==1.20.1 133 | <% } -%> 134 | 135 | <% if $extra_packages { %><%= join($extra_packages, '\n') %><% } %> 136 | -------------------------------------------------------------------------------- /data/common.yaml: -------------------------------------------------------------------------------- 1 | # jupyterhub/data/common.yaml 2 | --- 3 | jupyterhub::python3::version: "3.13" 4 | 5 | jupyterhub::prefix: "/opt/jupyterhub" 6 | jupyterhub::python: "%{alias('jupyterhub::python3::version')}" 7 | jupyterhub::traefik_version: 3.1.7 8 | 9 | jupyterhub::node::prefix: "/opt/jupyterhub_node" 10 | jupyterhub::node::config::jupyter_server_config: "%{alias('jupyterhub::jupyter_notebook_config_hash')}" 11 | jupyterhub::node::install_method: "venv" 12 | jupyterhub::node::install::python: "%{alias('jupyterhub::python3::version')}" 13 | 14 | jupyterhub::kernel::install_method: venv 15 | jupyterhub::kernel::venv::prefix: "/opt/ipython_kernel" 16 | jupyterhub::kernel::venv::python: "%{alias('jupyterhub::python3::version')}" 17 | 18 | 19 | jupyterhub::uv::install::prefix: "/opt/uv" 20 | jupyterhub::uv::install::version: "0.8.13" 21 | 22 | jupyterhub::nbgitpuller::version: 1.2.2 23 | jupyterhub::ipywidgets::version: 8.1.7 24 | jupyterhub::widgetsnbextension::version: 4.0.14 25 | jupyterhub::jupyterlab_widgets::version: 3.0.15 26 | jupyterhub::notebook::version: 6.5.7 27 | jupyterhub::jupyterhub::version: 5.3.0 28 | jupyterhub::pamela::version: 1.2.0 29 | 30 | jupyterhub::batchspawner::version: 1.3.0 31 | jupyterhub::slurmformspawner::version: 2.9.2 32 | jupyterhub::wrapspawner::version: 1.0.1 33 | jupyterhub::jupyterhub_traefik_proxy::version: 2.1.0 34 | jupyterhub::jupyterlab::version: 4.4.6 35 | jupyterhub::jupyterlmod::version: 5.3.0 36 | jupyterhub::jupyterlab_nvdashboard::version: 0.13.0 37 | jupyterhub::jupyter_server_proxy::version: 4.4.0 38 | jupyterhub::jupyter_rsession_proxy::version: 2.3.0 39 | jupyterhub::jupyter_remote_desktop_proxy::version: 3.0.1 40 | jupyterhub::idle_culler::version: 1.4.0 41 | jupyterhub::oauthenticator::version: 14.2.0 42 | jupyterhub::announcement::version: 0.9.2 43 | jupyterhub::pammfauthenticator::version: 1.3.1 44 | jupyterhub::oauth2freeipa::version: 2.0.0 45 | jupyterhub::ltiauthenticator::version: 1.6.2 46 | 47 | jupyterhub::announcement::port: 8888 48 | jupyterhub::announcement::fixed_message: '' 49 | jupyterhub::announcement::lifetime_days: 7 50 | jupyterhub::announcement::persist_path: /var/run/jupyterhub/announcements.json 51 | 52 | jupyterhub::jupyterhub_config_hash: 53 | SlurmFormSpawner: 54 | ui_args: 55 | lab: 56 | name: JupyterLab 57 | notebook: 58 | name: Jupyter Notebook 59 | url: '/tree' 60 | terminal: 61 | name: Terminal 62 | url: '/terminals/1' 63 | SbatchForm: 64 | ui: 65 | choices: ['notebook', 'lab', 'terminal'] 66 | def: 'lab' 67 | 68 | jupyterhub::jupyter_notebook_config_hash: 69 | NotebookNotary: 70 | db_file: ':memory:' 71 | FileManagerMixin: 72 | use_atomic_writing: false 73 | Lmod: 74 | launcher_pins: ['desktop-websockify'] 75 | ServerProxy: 76 | servers: 77 | code-server: 78 | command: ["code-server", "--auth=none", "--disable-telemetry", "--host=127.0.0.1", "--port={port}"] 79 | timeout: 30 80 | launcher_entry: 81 | title: VS Code 82 | enabled: true 83 | openrefine: 84 | command: ["refine", "-i", "127.0.0.1", "-p", "{port}", "-x", "refine.headless=true"] 85 | timeout: 60 86 | launcher_entry: 87 | title: OpenRefine 88 | enabled: true 89 | 90 | jupyterhub::submit::additions: | 91 | # Make sure Jupyter does not store its runtime in the home directory 92 | export JUPYTER_RUNTIME_DIR=${SLURM_TMPDIR}/jupyter 93 | 94 | # Disable variable export with sbatch 95 | export SBATCH_EXPORT=NONE 96 | # Avoid steps inheriting environment export 97 | # settings from the sbatch command 98 | unset SLURM_EXPORT_ENV 99 | 100 | # Setup user pip install folder 101 | export PIP_PREFIX=${SLURM_TMPDIR} 102 | export PATH="${PIP_PREFIX}/bin":${PATH} 103 | export PYTHONPATH=${PYTHONPATH}:"/opt/jupyterhub/lib/usercustomize" 104 | 105 | # Make sure the environment-level directories does not 106 | # have priority over user-level directories for config and data. 107 | # Jupyter core is trying to be smart with virtual environments 108 | # and it is not doing the right thing in our case. 109 | export JUPYTER_PREFER_ENV_PATH=0 110 | -------------------------------------------------------------------------------- /manifests/node.pp: -------------------------------------------------------------------------------- 1 | class jupyterhub::node ( 2 | Stdlib::Absolutepath $prefix, 3 | Enum['none', 'venv'] $install_method, 4 | ) { 5 | include jupyterhub::node::config 6 | include jupyterhub::kernel 7 | 8 | if $install_method == 'venv' { 9 | include jupyterhub::node::install 10 | } 11 | } 12 | 13 | class jupyterhub::node::config ( 14 | Hash $jupyter_server_config = {} 15 | ) { 16 | ensure_resource('file', '/etc/jupyter', { 'ensure' => 'directory' }) 17 | 18 | file { 'jupyter_notebook_config.json': 19 | path => '/etc/jupyter/jupyter_notebook_config.json', 20 | content => to_json_pretty($jupyter_server_config, true), 21 | mode => '0644', 22 | require => File['/etc/jupyter'], 23 | } 24 | 25 | file { 'jupyter_server_config.json': 26 | path => '/etc/jupyter/jupyter_server_config.json', 27 | content => to_json_pretty($jupyter_server_config, true), 28 | mode => '0644', 29 | require => File['/etc/jupyter'], 30 | } 31 | } 32 | 33 | class jupyterhub::node::install ( 34 | String $python, 35 | Array[String] $packages = [], 36 | Boolean $frozen_deps = true, 37 | ) { 38 | include jupyterhub::uv::install 39 | $prefix = lookup('jupyterhub::node::prefix') 40 | 41 | $jupyterhub_version = lookup('jupyterhub::jupyterhub::version') 42 | $batchspawner_version = lookup('jupyterhub::batchspawner::version') 43 | $nbgitpuller_version = lookup('jupyterhub::nbgitpuller::version') 44 | $ipywidgets_version = lookup('jupyterhub::ipywidgets::version') 45 | $widgetsnbextension_version = lookup('jupyterhub::widgetsnbextension::version') 46 | $jupyterlab_widgets_version = lookup('jupyterhub::jupyterlab_widgets::version') 47 | $notebook_version = lookup('jupyterhub::notebook::version') 48 | $jupyterlab_version = lookup('jupyterhub::jupyterlab::version') 49 | $jupyter_server_proxy_version = lookup('jupyterhub::jupyter_server_proxy::version') 50 | $jupyterlmod_version = lookup('jupyterhub::jupyterlmod::version') 51 | $jupyterlab_nvdashboard_version = lookup('jupyterhub::jupyterlab_nvdashboard::version') 52 | $jupyter_rsession_proxy_version = lookup('jupyterhub::jupyter_rsession_proxy::version') 53 | $jupyter_remote_desktop_proxy_version = lookup('jupyterhub::jupyter_remote_desktop_proxy::version') 54 | 55 | jupyterhub::uv::venv { 'node': 56 | prefix => $prefix, 57 | python => $python, 58 | requirements => epp('jupyterhub/node-requirements.txt', { 59 | 'jupyterhub_version' => $jupyterhub_version, 60 | 'batchspawner_version' => $batchspawner_version, 61 | 'notebook_version' => $notebook_version, 62 | 'nbgitpuller_version' => $nbgitpuller_version, 63 | 'ipywidgets_version' => $ipywidgets_version, 64 | 'widgetsnbextension_version' => $widgetsnbextension_version, 65 | 'jupyterlab_widgets_version' => $jupyterlab_widgets_version, 66 | 'jupyterlab_version' => $jupyterlab_version, 67 | 'jupyter_server_proxy_version' => $jupyter_server_proxy_version, 68 | 'jupyterlmod_version' => $jupyterlmod_version, 69 | 'jupyterlab_nvdashboard_version' => $jupyterlab_nvdashboard_version, 70 | 'jupyter_rsession_proxy_version' => $jupyter_rsession_proxy_version, 71 | 'jupyter_remote_desktop_proxy_version' => $jupyter_remote_desktop_proxy_version, 72 | 'frozen_deps' => $frozen_deps, 73 | 'extra_packages' => $packages, 74 | }), 75 | } 76 | 77 | if $jupyterlmod_version and $jupyter_server_proxy_version { 78 | # disable jupyterlab-server-proxy extension 79 | ensure_resource('file', "${prefix}/etc/jupyter/labconfig/", { 'ensure' => 'directory', 'require' => Jupyterhub::Uv::Venv['node'] }) 80 | file { "${prefix}/etc/jupyter/labconfig/page_config.json": 81 | content => '{"disabledExtensions": {"@jupyterhub/jupyter-server-proxy": true}}', 82 | subscribe => Jupyterhub::Uv::Venv['node'], 83 | require => File["${prefix}/etc/jupyter/labconfig/"], 84 | } 85 | 86 | # disable jupyter-server-proxy nbextension 87 | file { "${prefix}/etc/jupyter/nbconfig/tree.d/jupyter-server-proxy.json": 88 | content => '{"load_extensions": {"jupyter_server_proxy/tree": false}}', 89 | subscribe => Jupyterhub::Uv::Venv['node'], 90 | } 91 | } 92 | 93 | file { "${prefix}/lib/usercustomize": 94 | ensure => 'directory', 95 | mode => '0755', 96 | require => Jupyterhub::Uv::Venv['node'], 97 | } 98 | 99 | file { "${prefix}/lib/usercustomize/usercustomize.py": 100 | source => 'puppet:///modules/jupyterhub/usercustomize.py', 101 | mode => '0655', 102 | require => Jupyterhub::Uv::Venv['node'], 103 | } 104 | 105 | file { "${prefix}/share/jupyter/kernels/python3/kernel.json": 106 | ensure => absent, 107 | require => Jupyterhub::Uv::Venv['node'], 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /manifests/init.pp: -------------------------------------------------------------------------------- 1 | # @summary Class configuring a JupyterHub server with SlurmFormSpawner 2 | # @param prefix Absolute path where JupyterHub will be installed 3 | # @param python Python version to be installed by uv 4 | # @param slurm_home Path to Slurm installation folder 5 | # @param bind_url Public facing URL of the whole JupyterHub application 6 | # @param spawner_class Class to use for spawning single-user servers 7 | # @param authenticator_class Class name for authenticating users. 8 | # @param idle_timeout Time in seconds after which an inactive notebook is culled 9 | # @param traefik_version Version of traefik to install on the hub instance 10 | # @param admin_groups List of user groups that can act as JupyterHub admin 11 | # @param blocked_users List of users that cannot login and that jupyterhub can't sudo as 12 | # @param jupyterhub_config_hash Custom hash merged to JupyterHub JSON main hash 13 | # @param disable_user_config Disable per-user configuration of single-user servers 14 | # @param packages List of extra packages to install in the hub virtual environment 15 | # @param prometheus_token Token that Prometheus can use to scrape JupyterHub's metrics 16 | class jupyterhub ( 17 | Stdlib::Absolutepath $prefix, 18 | String $python, 19 | String $traefik_version, 20 | Stdlib::Absolutepath $slurm_home = '/opt/software/slurm', 21 | String $bind_url = 'https://127.0.0.1:8000', 22 | String $spawner_class = 'slurmformspawner.SlurmFormSpawner', 23 | String $authenticator_class = 'pam', 24 | Integer $idle_timeout = 0, 25 | Array[String] $admin_groups = [], 26 | Array[String] $blocked_users = ['root', 'toor', 'admin', 'centos', 'slurm'], 27 | Hash $jupyterhub_config_hash = {}, 28 | Boolean $disable_user_config = false, 29 | Boolean $frozen_deps = true, 30 | Array[String] $packages = [], 31 | Optional[String] $prometheus_token = undef, 32 | ) { 33 | include jupyterhub::uv::install 34 | 35 | user { 'jupyterhub': 36 | ensure => 'present', 37 | groups => 'jupyterhub', 38 | comment => 'JupyterHub', 39 | home => '/run/jupyterhub', 40 | shell => '/sbin/nologin', 41 | system => true, 42 | } 43 | group { 'jupyterhub': 44 | ensure => 'present', 45 | } 46 | 47 | $traefik_arch = $::facts['os']['architecture'] ? { 48 | 'x86_64' => 'amd64', 49 | 'aarch64' => 'arm64', 50 | } 51 | archive { 'traefik': 52 | path => "/opt/puppetlabs/puppet/cache/puppet-archive/traefik_v${traefik_version}_linux_${traefik_arch}.tar.gz", 53 | source => "https://github.com/traefik/traefik/releases/download/v${traefik_version}/traefik_v${traefik_version}_linux_${traefik_arch}.tar.gz", 54 | extract => true, 55 | extract_path => '/usr/bin', 56 | creates => '/usr/bin/traefik', 57 | extract_command => 'tar -xf %s traefik', 58 | } 59 | 60 | file { 'jupyterhub.service': 61 | path => '/lib/systemd/system/jupyterhub.service', 62 | content => epp('jupyterhub/jupyterhub.service', 63 | { 64 | 'python3_version' => $python, 65 | 'prefix' => $prefix, 66 | 'slurm_home' => $slurm_home, 67 | } 68 | ), 69 | } 70 | 71 | file { '/etc/sudoers.d/99-jupyterhub-user': 72 | mode => '0440', 73 | content => epp('jupyterhub/99-jupyterhub-user', 74 | { 75 | 'blocked_users' => $blocked_users, 76 | 'hostname' => $facts['networking']['hostname'], 77 | 'slurm_home' => $slurm_home, 78 | } 79 | ), 80 | } 81 | 82 | file { 'jupyterhub-auth': 83 | path => '/etc/pam.d/jupyterhub-auth', 84 | source => 'puppet:///modules/jupyterhub/jupyterhub-auth', 85 | mode => '0644', 86 | } 87 | 88 | file { 'jupyterhub-login': 89 | path => '/etc/pam.d/jupyterhub-login', 90 | source => 'puppet:///modules/jupyterhub/jupyterhub-login', 91 | mode => '0644', 92 | require => File['jupyterhub-auth'], 93 | } 94 | 95 | file { ['/etc/jupyterhub', '/etc/jupyterhub/ssl', '/etc/jupyterhub/templates']: 96 | ensure => directory, 97 | } 98 | 99 | file { '/run/jupyterhub': 100 | ensure => directory, 101 | owner => 'jupyterhub', 102 | group => 'jupyterhub', 103 | mode => '0755', 104 | } 105 | 106 | file { '/usr/lib/tmpfiles.d/jupyterhub.conf': 107 | source => 'puppet:///modules/jupyterhub/jupyterhub.conf', 108 | mode => '0644', 109 | } 110 | 111 | file { '/etc/jupyterhub/templates/page.html': 112 | source => 'puppet:///modules/jupyterhub/page.html', 113 | mode => '0644', 114 | require => File['/etc/jupyterhub/templates/'], 115 | notify => Service['jupyterhub'], 116 | } 117 | 118 | $announcement_port = lookup('jupyterhub::announcement::port') 119 | $announcement_service = { 120 | 'name' => 'announcement', 121 | # TODO: activate SSL 122 | # 'url' => "https://127.0.0.1:${announcement_port}", 123 | 'url' => "http://127.0.0.1:${announcement_port}", 124 | 'command' => [ 125 | "${prefix}/bin/python", 126 | '-m', 'jupyterhub_announcement', 127 | '--AnnouncementService.config_file=/etc/jupyterhub/announcement_config.json', 128 | ], 129 | 'oauth_no_confirm' => true, 130 | } 131 | $announcement_roles = [ 132 | { 133 | 'name' => 'user', 134 | 'scopes' => ['access:services', 'self'] 135 | } 136 | ] 137 | 138 | if $idle_timeout > 0 { 139 | $idle_culler_services = [{ 140 | 'name' => 'jupyterhub-idle-culler-service', 141 | 'command' => [ 142 | "${prefix}/bin/python3", 143 | '-m', 144 | 'jupyterhub_idle_culler', 145 | "--timeout=${idle_timeout}", 146 | ], 147 | }] 148 | 149 | $idle_culler_roles = [{ 150 | 'name' => 'jupyterhub-idle-culler-role', 151 | 'scopes' => ['list:users', 'read:users:activity', 'read:servers', 'delete:servers'], 152 | 'services'=> ['jupyterhub-idle-culler-service'], 153 | }] 154 | } else { 155 | $idle_culler_services = [] 156 | $idle_culler_roles = [] 157 | } 158 | 159 | if $prometheus_token != undef { 160 | $prometheus_services = [{ 161 | 'name' => 'prometheus', 162 | 'api_token' => $prometheus_token, 163 | }] 164 | $prometheus_roles = [{ 165 | 'name' => 'metrics', 166 | 'scopes' => ['read:metrics'], 167 | 'services' => ['prometheus'], 168 | }] 169 | } else { 170 | $prometheus_services = [] 171 | $prometheus_roles = [] 172 | } 173 | 174 | $services = [$announcement_service] + $idle_culler_services + $prometheus_services 175 | $roles = $announcement_roles + $idle_culler_roles + $prometheus_roles 176 | 177 | $node_prefix = lookup('jupyterhub::node::prefix') 178 | $jupyterhub_config_base = parsejson(file('jupyterhub/jupyterhub_config.json')) 179 | $kernel_setup = lookup('jupyterhub::kernel::install_method') 180 | $kernel_prefix = lookup('jupyterhub::kernel::venv::prefix') 181 | $prologue = $kernel_setup ? { 182 | 'venv' => "export JUPYTER_PATH=${kernel_prefix}/puppet-jupyter:\${JUPYTER_PATH:-}; export VIRTUAL_ENV_DISABLE_PROMPT=1; source ${kernel_prefix}/bin/activate", 183 | 'none' => '', 184 | } 185 | $jupyterhub_config_params = { 186 | 'JupyterHub' => { 187 | 'bind_url' => $bind_url, 188 | 'authenticator_class' => $authenticator_class, 189 | 'spawner_class' => $spawner_class, 190 | 'admin_access' => Boolean(size($admin_groups) > 0), 191 | 'services' => $services, 192 | 'load_roles' => $roles, 193 | }, 194 | 'Authenticator' => { 195 | 'admin_groups' => $admin_groups, 196 | 'allow_all' => true, 197 | 'blocked_users' => $blocked_users, 198 | }, 199 | 'Spawner' => { 200 | 'disable_user_config' => $disable_user_config, 201 | 'cmd' => "${node_prefix}/bin/jupyterhub-singleuser", 202 | }, 203 | 'BatchSpawnerBase' => { 204 | 'batchspawner_singleuser_cmd' => "${node_prefix}/bin/batchspawner-singleuser", 205 | }, 206 | 'SlurmSpawner' => { 207 | 'exec_prefix' => '', 208 | 'env_keep' => [], 209 | 'batch_submit_cmd' => "sudo --preserve-env={keepvars} -u {username} ${slurm_home}/bin/sbatch --parsable", 210 | 'batch_cancel_cmd' => "sudo -u {username} ${slurm_home}/bin/scancel {job_id}", 211 | 'req_prologue' => $prologue, 212 | }, 213 | 'SlurmFormSpawner' => { 214 | 'slurm_bin_path' => "${slurm_home}/bin", 215 | }, 216 | } 217 | 218 | $jupyterhub_config = deep_merge( 219 | $jupyterhub_config_base, 220 | $jupyterhub_config_params, 221 | $jupyterhub_config_hash, 222 | ) 223 | 224 | $announcement_config = { 225 | 'AnnouncementService' => { 226 | 'fixed_message' => lookup('jupyterhub::announcement::fixed_message'), 227 | 'cookie_secret_file' => '/var/run/jupyterhub/jupyterhub_cookie_secret', 228 | 'port' => lookup('jupyterhub::announcement::port'), 229 | }, 230 | 'AnnouncementQueue' => { 231 | 'lifetime_days' => lookup('jupyterhub::announcement::lifetime_days'), 232 | 'persist_path' => lookup('jupyterhub::announcement::persist_path'), 233 | }, 234 | 'SSLContext' => { 235 | # TODO: add missing SSL CA 236 | # 'certfile' => '/etc/jupyterhub/ssl/cert.pem', 237 | # 'keyfile' => '/etc/jupyterhub/ssl/key.pem' 238 | }, 239 | } 240 | 241 | file { 'jupyterhub_config.json': 242 | path => '/etc/jupyterhub/jupyterhub_config.json', 243 | content => to_json_pretty($jupyterhub_config, true), 244 | mode => '0640', 245 | owner => 'root', 246 | group => 'jupyterhub', 247 | require => User['jupyterhub'], 248 | } 249 | 250 | file { 'announcement_config.json': 251 | path => '/etc/jupyterhub/announcement_config.json', 252 | content => to_json_pretty($announcement_config, true), 253 | mode => '0640', 254 | owner => 'root', 255 | group => 'jupyterhub', 256 | require => User['jupyterhub'], 257 | } 258 | 259 | $submit_additions = lookup('jupyterhub::submit::additions', String, undef, '') 260 | file { 'submit.sh': 261 | path => '/etc/jupyterhub/submit.sh', 262 | content => epp('jupyterhub/submit.sh', { 263 | 'prologue' => $prologue, 264 | 'additions' => $submit_additions, 265 | }), 266 | mode => '0644', 267 | } 268 | 269 | # JupyterHub virtual environment 270 | $jupyterhub_version = lookup('jupyterhub::jupyterhub::version') 271 | $batchspawner_version = lookup('jupyterhub::batchspawner::version') 272 | $jupyterhub_traefik_proxy_version = lookup('jupyterhub::jupyterhub_traefik_proxy::version') 273 | $oauthenticator_version = lookup('jupyterhub::oauthenticator::version') 274 | $ltiauthenticator_version = lookup('jupyterhub::ltiauthenticator::version') 275 | $pamela_version = lookup('jupyterhub::pamela::version') 276 | $pammfauthenticator_version = lookup('jupyterhub::pammfauthenticator::version') 277 | $oauth2freeipa_version = lookup('jupyterhub::oauth2freeipa::version') 278 | $idle_culler_version = lookup('jupyterhub::idle_culler::version') 279 | $announcement_version = lookup('jupyterhub::announcement::version') 280 | $slurmformspawner_version = lookup('jupyterhub::slurmformspawner::version') 281 | $wrapspawner_version = lookup('jupyterhub::wrapspawner::version') 282 | 283 | jupyterhub::uv::venv { 'hub': 284 | prefix => $prefix, 285 | python => $python, 286 | requirements => epp('jupyterhub/hub-requirements.txt', { 287 | 'jupyterhub_version' => $jupyterhub_version, 288 | 'batchspawner_version' => $batchspawner_version, 289 | 'slurmformspawner_version' => $slurmformspawner_version, 290 | 'wrapspawner_version' => $wrapspawner_version, 291 | 'oauthenticator_version' => $oauthenticator_version, 292 | 'ltiauthenticator_version' => $ltiauthenticator_version, 293 | 'oauth2freeipa_version' => $oauth2freeipa_version, 294 | 'pamela_version' => $pamela_version, 295 | 'pammfauthenticator_version' => $pammfauthenticator_version, 296 | 'idle_culler_version' => $idle_culler_version, 297 | 'announcement_version' => $announcement_version, 298 | 'jupyterhub_traefik_proxy_version' => $jupyterhub_traefik_proxy_version, 299 | 'frozen_deps' => $frozen_deps, 300 | 'extra_packages' => $packages, 301 | }), 302 | } 303 | 304 | exec { 'create_self_signed_sslcert': 305 | command => "openssl req -newkey rsa:4096 -nodes -keyout key.pem -x509 -days 3650 -out cert.pem -subj '/CN=${facts['networking']['fqdn']}'", 306 | cwd => '/etc/jupyterhub/ssl', 307 | creates => ['/etc/jupyterhub/ssl/key.pem', '/etc/jupyterhub/ssl/cert.pem'], 308 | path => ['/usr/bin', '/usr/sbin'], 309 | umask => '037', 310 | } 311 | 312 | file { '/etc/jupyterhub/ssl/cert.pem': 313 | mode => '0644', 314 | require => [Exec['create_self_signed_sslcert']], 315 | } 316 | 317 | file { '/etc/jupyterhub/ssl/key.pem': 318 | mode => '0640', 319 | group => 'jupyterhub', 320 | require => [Exec['create_self_signed_sslcert']], 321 | } 322 | 323 | service { 'jupyterhub': 324 | ensure => running, 325 | enable => true, 326 | require => File['submit.sh'], 327 | subscribe => [ 328 | Archive['traefik'], 329 | Jupyterhub::Uv::Venv['hub'], 330 | File['jupyterhub-login'], 331 | File['jupyterhub.service'], 332 | File['jupyterhub_config.json'], 333 | File['announcement_config.json'], 334 | File['/etc/jupyterhub/ssl/cert.pem'], 335 | File['/etc/jupyterhub/ssl/key.pem'], 336 | ], 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # puppet-jupyterhub 2 | 3 | The module installs, configures, and manages the JupyterHub service 4 | with [batchspawner](https://github.com/jupyterhub/batchspawner) as a 5 | spawner and in conjunction with the job scheduler [Slurm](https://slurm.schedmd.com/). 6 | 7 | ## Requirements 8 | 9 | - Linux 10 | - Slurm >= 17.x 11 | 12 | ### Hub 13 | 14 | - The hub ports 80 and 443 need to be opened to the users incoming network (i.e: Internet). 15 | - The hub needs to allow authentication of users through pam. 16 | - The hub must be able to talk to `slurmctld` to submit jobs on the users' behalf. 17 | - The hub port `8081` needs to be accessible from the compute node network. 18 | - The slurm binaries needs to be installed and accessible from `PATH` for the user `jupyterhub`, 19 | mainly : `squeue`, `sbatch`, `sinfo`, `sacctmgr` and `scontrol`. 20 | - The hub does not need the users to have SSH access. 21 | - The hub does not need access to the cluster filesystem. 22 | 23 | ### Compute Node 24 | 25 | - The compute nodes' tcp ephemeral port range needs to be accessible from the hub. 26 | - Optional: configure [Compute Canada Software Stack with CVMFS](https://docs.computecanada.ca/wiki/Accessing_CVMFS) 27 | 28 | 29 | ## Setup 30 | 31 | ### hub 32 | To install JuptyerHub with the default options: 33 | 34 | ```puppet 35 | include jupyterhub 36 | ``` 37 | 38 | ### compute 39 | 40 | To install the Jupyter notebook component on the compute node: 41 | 42 | ```puppet 43 | include jupyterhub::node 44 | ``` 45 | 46 | If the compute nodes cannot access Internet, configure the puppet agent to use 47 | [`http_proxy_host`](https://www.puppet.com/docs/puppet/8/configuration.html#http-proxy-host). 48 | 49 | ## Hieradata Configuration 50 | 51 | ### General options 52 | 53 | | Variable | Type | Description | Default | 54 | | -------- | :----| :-----------| ------- | 55 | | `jupyterhub::python3::version` | String | Global Python 3 version to use when creating virtual environment | refer to [data/common.yaml](data/common.yaml)| 56 | | `jupyterhub::jupyterhub::version` | String | JupyterHub package version to install | refer to [data/common.yaml](data/common.yaml) | 57 | | `jupyterhub::pip::version` | String | pip package version to install | refer to [data/common.yaml](data/common.yaml) | 58 | | `jupyterhub::notebook::version` | String | notebook package version to install | refer to [data/common.yaml](data/common.yaml) | 59 | | `jupyterhub::batchspawner::version` | String | Url to batchspawner source code release file | refer to [data/common.yaml](data/common.yaml) | 60 | | `jupyterhub::slurmformspawner::version` | String | slurmformspawner package version to install | refer to [data/common.yaml](data/common.yaml) | 61 | | `jupyterhub::wrapspawner::version` | String | wrapspawner package version to install | refer to [data/common.yaml](data/common.yaml) | 62 | | `jupyterhub::oauthenticator::version` | String | oauthenticator package version to install | refer to [data/common.yaml](data/common.yaml) | 63 | | `jupyterhub::ltiauthenticator::version` | String | ltiauthenticator package version to install | refer to [data/common.yaml](data/common.yaml) | 64 | | `jupyterhub::oauth2freeipa::version` | String | oauth2freeipa package version to install | refer to [data/common.yaml](data/common.yaml) | 65 | | `jupyterhub::pammfauthenticator::url` | String | Url to pammfauthenticator source code release file | refer to [data/common.yaml](data/common.yaml) | 66 | | `jupyterhub::jupyterhub_traefik_proxy::version` | String | jupyterhub-traefik-proxy package version to install | refer to [data/common.yaml](data/common.yaml) | 67 | | `jupyterhub::nbgitpuller::version` | String | nbgitpuller package version to install | refer to [data/common.yaml](data/common.yaml) | 68 | | `jupyterhub::ipywidgets::version` | String | ipywidgets package version to install | refer to [data/common.yaml](data/common.yaml) | 69 | | `jupyterhub::widgetsnbextension::version` | String | widgetsnbextension package version to install | refer to [data/common.yaml](data/common.yaml) | 70 | | `jupyterhub::jupyterlab_widgets::version` | String | jupyterlab_widgets package version to install | refer to [data/common.yaml](data/common.yaml) | 71 | 72 | ### Hub options 73 | 74 | | Variable | Type | Description | Default | 75 | | -------- | :----| :-----------| ------- | 76 | | `jupyterhub::prefix` | Stdlib::Absolutepath | Absolute path where JupyterHub will be installed | `/opt/jupyterhub` | 77 | | `jupyterhub::python` | String | Python version to be installed by uv | `%{alias('jupyterhub::python3::version')}` | 78 | | `jupyterhub::slurm_home` | Stdlib::Absolutepath | Path to Slurm installation folder | `/opt/software/slurm` | 79 | | `jupyterhub::bind_url` | String | Public facing URL of the whole JupyterHub application | `https://127.0.0.1:8000` | 80 | | `jupyterhub::spawner_class` | String | Class to use for spawning single-user servers | `slurmformspawner.SlurmFormSpawner` | 81 | | `jupyterhub::authenticator_class` | String | Class name for authenticating users | `pam` | 82 | | `jupyterhub::idle_timeout` | Integer | Time in seconds after which an inactive notebook is culled | `0 (no timeout)` | 83 | | `jupyterhub::traefik_version` | String | Version of traefik to install on the hub instance | '2.10.4' | 84 | | `jupyterhub::admin_groups` | Array[String] | List of user groups that can act as JupyterHub admin | `[]` | 85 | | `jupyterhub::blocked_users` | List[String] | List of users that cannot login | `['root', 'toor', 'admin', 'centos', 'slurm']` | 86 | | `jupyterhub::jupyterhub_config_hash` | Hash | Custom hash merged to JupyterHub JSON main hash | `{}` | 87 | | `jupyterhub::disable_user_config` | Boolean | Disable per-user configuration of single-user servers | `false` | 88 | | `jupyterhub::packages` | Array[String] | List of extra packages to install in the hub virtual environment | `[]` | 89 | | `jupyterhub::prometheus_token` | String | Token that Prometheus can use to scrape JupyterHub's metrics | `undef` | 90 | | `jupyterhub::frozen_deps` | Boolean | Install all unlisted dependencies versions as frozen by this module | `true` | 91 | 92 | ### Announcement options 93 | 94 | puppet-jupyterhub installs the service [jupyterhub-announcement](https://github.com/rcthomas/jupyterhub-announcement) to broadcast messages for the users once connected to the hub. 95 | 96 | | Variable | Type | Description | Default | 97 | | -------- | :----| :-----------| ------- | 98 | | `jupyterhub::announcement::port` | Integer | Localhost port the service will listen on | 8888 | 99 | | `jupyterhub::announcement::fixed_message` | String | Message that will always be displayed | '' | 100 | | `jupyterhub::announcement::lifetime_days `| Integer | Announcement duration in days | 7 | 101 | | `jupyterhub::announcement::persist_path` | String | File where current and past annoucements are stored | /var/run/jupyterhub/announcements.json | 102 | 103 | 104 | ### Compute node options 105 | 106 | | Variable | Type | Description | Default | 107 | | -------- | :----| :-----------| ------- | 108 | | `jupyterhub::node::prefix` | Stdlib::Absolutepath | Absolute path where Jupyter Notebook and jupyterhub-singleuser will be installed | `/opt/jupyterhub` | 109 | | `jupyterhub::node::config::jupyter_server_config` | Hash | control options and traitlets of Jupyter and its extensions | refer to [data/common.yaml](data/common.yaml) | 110 | | `jupyterhub::node::install_method` | Enum['none', 'venv'] | Determine if the jupyterhub node virtual environment needs to be installed by Puppet | `venv` | 111 | | `jupyterhub::node::install::python` | String | Python version to be installed by uv | `%{alias('jupyterhub::python3::version')}` | 112 | | `jupyterhub::node::install::packages` | Array[String] | List of extra packages to install in the node virtual environment | `[]` | 113 | | `jupyterhub::node::install::frozen_deps` | Boolean | Install all unlisted dependencies versions as frozen by this module | `true` | 114 | 115 | ### Kernel options 116 | 117 | | Variable | Type | Description | Default | 118 | | -------- | :----| :-----------| ------- | 119 | | `jupyterhub::kernel::install_method` | Enum['none', 'venv'] | Determine if the Python kernel is installed as a local virtual environment by Puppet | `venv` | 120 | | `jupyterhub::kernel::venv::prefix` | Stdlib::Absolutepath | Absolute path where the IPython kernel virtual environment will be installed | `/opt/ipython-kernel` | 121 | | `jupyterhub::kernel::venv::python` | Variant[String, Stdlib::Absolutepath] | Python version or path to Python binary to init virtual environment with uv | `3.12` | 122 | | `jupyterhub::kernel::venv::kernel_name` | String | Name of the kernelspec | `python3` | 123 | | `jupyterhub::kernel::venv::display_name` | String | Display name of the kernel | `Python 3` | 124 | | `jupyterhub::kernel::venv::packages` | Array[String] | Python packages to install in the default kernel | `[]` | 125 | | `jupyterhub::kernel::venv::pip_environment`| Hash[String, String] | Hash of environment variables configured before calling installing `venv::packages` | `{}` | 126 | | `jupyterhub::kernel::venv::kernel_environment`| Hash[String, String] | Hash of environment variables configured before the kernel is started | `{}` | 127 | 128 | 129 | ### SlurmFormSpawner's options 130 | 131 | To control SlurmFormSpawner options, use `jupyterhub::jupyterhub_config_hash` like this: 132 | 133 | ```yaml 134 | jupyterhub::jupyterhub_config_hash: 135 | SbatchForm: 136 | account: 137 | def: 'def-account' 138 | runtime: 139 | min: 1.0 140 | def: 2.0 141 | max: 5.0 142 | nprocs: 143 | min: 1 144 | def: 2 145 | max: 8 146 | memory: 147 | min: 1024 148 | max: 2048 149 | gpus: 150 | def: 'gpu:0' 151 | choices: ['gpu:0', 'gpu:k20:1', 'gpu:k80:1'] 152 | oversubscribe: 153 | def: false 154 | lock: true 155 | ui: 156 | def: 'lab' 157 | choices: ['lab', 'notebook', 'terminal', 'rstudio', 'code-server', 'desktop'] 158 | partition: 159 | def: 'partition1' 160 | choices: ['partition1', 'partition2', 'partition3'] 161 | SlurmFormSpawner: 162 | ui_args: 163 | notebook: 164 | name: Jupyter Notebook 165 | args: '/tree' 166 | modules: ['ipython-kernel/3.7'] 167 | lab: 168 | name: JupyterLab 169 | modules: ['ipython-kernel/3.7'] 170 | terminal: 171 | name: Terminal 172 | args: '/terminals/1' 173 | rstudio: 174 | name: RStudio 175 | args: '/rstudio' 176 | modules: ['gcc', 'rstudio-server'] 177 | code-server: 178 | name: VS Code 179 | args: '/code-server' 180 | modules: ['code-server'] 181 | desktop: 182 | name: Desktop 183 | url: '/Desktop' 184 | SlurmAPI: 185 | info_cache_ttl: 3600 # refresh sinfo cache at most every hour 186 | acct_cache_ttl: 3600 # refresh account cache at most every hour 187 | res_cache_ttl: 3600 # refresh reservation cache at most every hour 188 | ``` 189 | 190 | Refer to [slurmformspawner documentation](https://github.com/cmd-ntrf/slurmformspawner) for more details on each parameter. 191 | 192 | ### SlurmSpawner usage example 193 | 194 | [`SlurmSpawner`](https://github.com/jupyterhub/batchspawner) can be used instead of SlurmFormSpawner 195 | when job configuration with a form is not desirable: 196 | ```yaml 197 | jupyterhub::spawner_class: "batchspawner.SlurmSpawner" 198 | jupyterhub::jupyterhub_config_hash: 199 | SlurmSpawner: 200 | req_account: "def-sponsor00" 201 | req_memory: "256" 202 | req_nprocs: "1" 203 | req_runtime: "3600" 204 | req_options: "--oversubscribe" 205 | default_url: "/tree" # use nbclassic instead of lab 206 | ``` 207 | 208 | ### ProfilesSpawner usage example 209 | 210 | [`ProfilesSpawner`](https://github.com/jupyterhub/wrapspawner) can be used instead of SlurmFormSpawner 211 | when job configuration with a complete form is not desirable, but some predefined options might be: 212 | ```yaml 213 | jupyterhub::spawner_class: 'wrapspawner.ProfilesSpawner' 214 | jupyterhub::jupyterhub_config_hash: 215 | ProfilesSpawner: 216 | profiles: 217 | - ["Base", 'base', 'batchspawner.SlurmSpawner', { 'req_nprocs': '1' } ] 218 | - ["Parallel", 'parallel', 'batchspawner.SlurmSpawner', { 'req_nprocs': '2' } ] 219 | SlurmSpawner: 220 | req_account: "def-sponsor00" 221 | req_memory: "256" 222 | req_runtime: "3600" 223 | req_options: "--oversubscribe" 224 | default_url: "/tree" # use nbclassic instead of lab 225 | ``` 226 | 227 | ### OAuthenticator usage example 228 | 229 | By default, puppet-jupyterhub configures the authentication with PAM, but the oauthenticator 230 | package is readily installed. 231 | 232 | 233 | In this example, we configure JupyterHub to authenticate with GitHub and create an account in FreeIPA. 234 | ```yaml 235 | jupyterhub::authenticator_class: "ipa-github" 236 | jupyterhub::jupyterhub_config_hash: 237 | GitHubOAuthenticator: 238 | auto_login: true 239 | oauth_callback_url: "https://[your-domain]/hub/oauth_callback" 240 | client_id: "XYZ" 241 | client_secret: "DCBA-123-456" 242 | ``` 243 | 244 | ### LTIAuthenticator usage example 245 | 246 | By default, puppet-jupyterhub configures the authentication with PAM, but the ltiauthenticator 247 | package is readily installed. This allows to integrate with LTI (Learning Tools Interoperability) providers. 248 | 249 | In this example, we configure JupyterHub to authenticate with an LTI 1.1 provider 250 | ```yaml 251 | jupyterhub::authenticator_class: "ipa-lti11" 252 | jupyterhub::jupyterhub_config_hash: 253 | LTI11Authenticator: 254 | consumers: { '': ' ]} 255 | username_key: 'lis_person_sourcedid' 256 | ``` 257 | For more information about the LTI Authenticator for JupyterHub, see [its documentation for version 1.1](https://ltiauthenticator.readthedocs.io/en/latest/lti11/getting-started.html). 258 | For LTI 1.3, you would change `ipa-lti11` by `ipa-lti13` and adjust the hash according to [LTI Authenticator's documentation for version 1.3](https://ltiauthenticator.readthedocs.io/en/latest/lti13/getting-started.html). 259 | 260 | ### OpenID Connect (OIDC) usage example 261 | It is possible to configure JupyterHub to delegate authentication to an ODIC provider. The configuration might look someting like this: 262 | ```yaml 263 | jupyterhub::authenticator_class: 'oauthenticator.generic.GenericOAuthenticator' 264 | jupyterhub::jupyterhub_config_hash: 265 | GenericOAuthenticator: 266 | client_id: '' 267 | client_secret: '' 268 | authorize_url: 'https:///idp/profile/oidc/authorize' 269 | token_url: 'https:///idp/profile/oidc/token' 270 | userdata_url: 'https:///idp/profile/oidc/userinfo' 271 | oauth_callback_url: 'https:///hub/oauth_callback' 272 | username_key: 'preferred_username' 273 | scope: ['openid', ''] 274 | allowed_groups: [] 275 | claim_groups_key: '' 276 | required_groups: [] 277 | ``` 278 | 279 | In the above example, we have defined a brand new `required_groups` parameter, which we can implement via custom Python code in `/etc/jupyterhub/jupyterhub_config.py`: 280 | ```py 281 | `def require_groups( 282 | authenticator: Authenticator, handler, auth_model: dict 283 | ) -> dict | None: 284 | claim_groups_key = authenticator.config['GenericOAuthenticator']['claim_groups_key'] 285 | in_groups = auth_model.get('auth_state', {}).get('oauth_user', {}).get(claim_groups_key, []) 286 | required_groups = authenticator.config['GenericOAuthenticator']['required_groups'] 287 | for group in required_groups: 288 | if group not in in_groups: 289 | authenticator.log.warning( 290 | "Not allowing access to user %s not in group %s (groups=%s)", 291 | auth_model["name"], 292 | group, 293 | in_groups, 294 | ) 295 | return None 296 | return auth_model 297 | 298 | c.GenericOAuthenticator.post_auth_hook = require_groups 299 | ``` 300 | 301 | ### Jupyter Notebook options 302 | 303 | To control options and traitlets of Jupyter Notebook and its extensions, use `jupyterhub::node::config::jupyter_server_config` like this: 304 | ```yaml 305 | jupyterhub::node::config::jupyter_server_config: 306 | ServerProxy: 307 | servers: 308 | rstudio: 309 | command: ["rserver", "--www-port={port}", "--www-frame=same", "--www-address=127.0.0.1"] 310 | timeout: 30 311 | launcher_entry: 312 | title: RStudio 313 | code-server: 314 | command: ["code-server", "--auth=none", "--disable-telemetry", "--host=127.0.0.1", "--port={port}"] 315 | timeout: 30 316 | launcher_entry: 317 | title: VS Code 318 | openrefine: 319 | command: ["refine"] 320 | timeout: 30 321 | launcher_entry: 322 | title: OpenRefine 323 | ``` 324 | 325 | ### Submit addition option 326 | | Variable | Type | Description | 327 | | -------- | :----| :-----------| 328 | | `jupyterhub::submit::additions` | String | bash command(s) that should be added to submit.sh | 329 | 330 | Adds the following by default: 331 | ```sh 332 | # Make sure Jupyter does not store its runtime in the home directory 333 | export JUPYTER_RUNTIME_DIR=${SLURM_TMPDIR}/jupyter 334 | 335 | # Disable variable export with sbatch 336 | export SBATCH_EXPORT=NONE 337 | # Avoid steps inheriting environment export 338 | # settings from the sbatch command 339 | unset SLURM_EXPORT_ENV 340 | 341 | # Setup user pip install folder 342 | export PIP_PREFIX=${SLURM_TMPDIR} 343 | export PATH="${PIP_PREFIX}/bin":${PATH} 344 | export PYTHONPATH=${PYTHONPATH}:"/opt/jupyterhub/lib/usercustomize" 345 | 346 | # Make sure the environment-level directories does not 347 | # have priority over user-level directories for config and data. 348 | # Jupyter core is trying to be smart with virtual environments 349 | # and it is not doing the right thing in our case. 350 | export JUPYTER_PREFER_ENV_PATH=0 351 | ``` 352 | --------------------------------------------------------------------------------