Skip to content
Snippets Groups Projects
Commit 80580885 authored by František Dvořák's avatar František Dvořák
Browse files

Experiments with JupyterHub - central hub using gateways and full fledged hub (WIP)

parent bd07dc74
No related branches found
No related tags found
No related merge requests found
---
proxy:
service:
type: NodePort
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: "nginx"
kubernetes.io/tls-acme: "true"
hosts:
- eosc.zcu.cz
tls:
- hosts:
- eosc.zcu.cz
secretName: acme-tls-central
singleuser:
# storage:
# capacity: 20Gi
# dynamic:
# pvcNameTemplate: claim-{userid}{servername}
# volumeNameTemplate: vol-{userid}{servername}
# storageAccessModes: ["ReadWriteMany"]
lifecycleHooks:
postStart:
exec: { "command": ["/bin/sh", "-c", "mkdir -p /home/jovyan/.notebookCheckpoints"] }
memory:
limit: 6G
guarantee: 128M
cpu:
limit: 2
guarantee: .02
defaultUrl: "/lab"
image:
name: elyra/nb2kg
tag: dev
extraEnv:
# KERNEL_USERNAME: jovyan
KG_AUTH_TOKEN: "{{ gateways_token['cesnet-mcc'] }}"
KG_URL: "https://gateway-cesnet.eosc.zcu.cz"
# KG_HTTP_USER: jovyan
KG_REQUEST_TIMEOUT: 60
hub:
# services:
# status:
# url: "http://status-web/"
# admin: true
image:
name: valtri/hub
tag: "sha-0800b46" # jupyter-3.x
config:
Authenticator:
enable_auth_state: true
admin_users:
- 529a87e5ce04cd5ddd7161734d02df0e2199a11452430803e714cb1309cc3907@egi.eu
- 025166931789a0f57793a6092726c2ad89387a4cc167e7c63c5d85fc91021d18@egi.eu
- 7ce47695f1e7fc91a1156e672f4a47576559938cdbe840355e2429e3a05b4ff8@egi.eu
# fdvorak2 @ aai.egi.eu
- 52cc7599bd1553c9d63e34e4c90b7e84d44967490c28bb4c53fe97b0c881d677@egi.eu
# fdvorak2 @ aai-dev.egi.eu
- c481e0a85e1ae0a5a1480a63e62295ca2f9ac652244947995bd4a0210fbcb77c@egi.eu
# jhradil3 @ aai-dev.egi.eu
- 240c0594fe34ac26cffd82fd0ad85f29d9ad9dfbb46febb05ed42db0bff594d1@egi.eu
# keep in sync with:
# - cesnet/playbooks/templates/binder.yaml
# - documentation/content/en/users/dev-env/notebooks/_index.md
allowed_groups:
- urn:mace:egi.eu:group:vo.access.egi.eu:role=member#aai.egi.eu
- urn:mace:egi.eu:group:vo.notebooks.egi.eu:role=member#aai.egi.eu
- urn:mace:egi.eu:www.egi.eu:fedcloud-users:member@egi.eu
- urn:mace:egi.eu:www.egi.eu:techsolutions:member@egi.eu
# changed 2022-10
- urn:mace:egi.eu:group:fedcloud-users#sso.egi.eu
- urn:mace:egi.eu:group:supplier-notebooks#sso.egi.eu
- urn:mace:egi.eu:group:techsolutions#sso.egi.eu
- urn:mace:egi.eu:group:notebooks-support#sso.egi.eu
auto_login: true
claim_groups_key: "eduperson_entitlement"
EGICheckinAuthenticator:
checkin_host: "{{ secret['checkin_host'] }}"
authorize_url: "https://{{ secret['checkin_host'] }}/auth/realms/egi/protocol/openid-connect/auth"
token_url: "https://{{ secret['checkin_host'] }}/auth/realms/egi/protocol/openid-connect/token"
userdata_url: "https://{{ secret['checkin_host'] }}/auth/realms/egi/protocol/openid-connect/userinfo"
client_id: "{{ secret['client_id'] }}"
client_secret: "{{ secret['client_secret'] }}"
oauth_callback_url: "https://eosc.zcu.cz/hub/oauth_callback"
scope: ["openid", "profile", "email", "offline_access", "eduperson_scoped_affiliation", "eduperson_entitlement"]
username_key: "preferred_username" # 'sub' too long for persistent volumes
JupyterHub:
admin_access: true
authenticate_prometheus: false
authenticator_class: egi_notebooks_hub.egiauthenticator.EGICheckinAuthenticator
# spawner_class
# c.B2DropSpawner.args = ["--FileCheckpoints.checkpoint_dir='/home/jovyan/.notebookCheckpoints'"]
extraConfig:
nb2kg: |-
config = '/etc/jupyter/jupyter_notebook_config.py'
c.Spawner.cmd = ['jupyter-labhub']
templatePaths:
- /egi-notebooks-hub/templates
---
proxy:
service:
type: NodePort
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: "nginx"
kubernetes.io/tls-acme: "true"
hosts:
- fullhub.eosc.zcu.cz
tls:
- hosts:
- fullhub.eosc.zcu.cz
secretName: acme-tls-fullhub
singleuser:
# keep resource limits in sync with:
# - documentation/content/en/users/dev-env/notebooks/_index.md
# - documentation/content/en/users/dev-env/notebooks/data/_index.md
# - profileList
storage:
capacity: 20Gi
dynamic:
pvcNameTemplate: claim-{userid}{servername}
volumeNameTemplate: vol-{userid}{servername}
storageAccessModes: ["ReadWriteMany"]
extraVolumes:
- name: b2drop
# sizeLimit problematic in this environment,
# not needed for remote mounts
empty_dir:
lifecycleHooks:
postStart:
exec: { "command": ["/bin/sh", "-c", "ln -snf /mnt/b2drop $HOME/b2drop; mkdir -p /home/jovyan/.notebookCheckpoints"] }
memory:
limit: 6G
guarantee: 128M
cpu:
limit: 2
guarantee: .02
defaultUrl: "/lab"
image:
name: valtri/single-user
tag: "jupyter-4"
profileList:
- display_name: Default EGI environment - 6 GB RAM / 2 core
description: >
The Default notebook environment includes Python, R, Julia and Octave kernels
default: true
kubespawner_override:
args:
- "--CondaKernelSpecManager.env_filter='/opt/conda$'"
hub:
# services:
# status:
# url: "http://status-web/"
# admin: true
image:
name: valtri/hub
tag: "sha-0800b46" # jupyter-3.x
config:
Authenticator:
enable_auth_state: true
admin_users:
- 529a87e5ce04cd5ddd7161734d02df0e2199a11452430803e714cb1309cc3907@egi.eu
- 025166931789a0f57793a6092726c2ad89387a4cc167e7c63c5d85fc91021d18@egi.eu
- 7ce47695f1e7fc91a1156e672f4a47576559938cdbe840355e2429e3a05b4ff8@egi.eu
# fdvorak2 @ aai.egi.eu
- 52cc7599bd1553c9d63e34e4c90b7e84d44967490c28bb4c53fe97b0c881d677@egi.eu
# fdvorak2 @ aai-dev.egi.eu
- c481e0a85e1ae0a5a1480a63e62295ca2f9ac652244947995bd4a0210fbcb77c@egi.eu
# jhradil3 @ aai-dev.egi.eu
- 240c0594fe34ac26cffd82fd0ad85f29d9ad9dfbb46febb05ed42db0bff594d1@egi.eu
# keep in sync with:
# - cesnet/playbooks/templates/binder.yaml
# - documentation/content/en/users/dev-env/notebooks/_index.md
allowed_groups:
- urn:mace:egi.eu:group:vo.access.egi.eu:role=member#aai.egi.eu
- urn:mace:egi.eu:group:vo.notebooks.egi.eu:role=member#aai.egi.eu
- urn:mace:egi.eu:www.egi.eu:fedcloud-users:member@egi.eu
- urn:mace:egi.eu:www.egi.eu:techsolutions:member@egi.eu
# changed 2022-10
- urn:mace:egi.eu:group:fedcloud-users#sso.egi.eu
- urn:mace:egi.eu:group:supplier-notebooks#sso.egi.eu
- urn:mace:egi.eu:group:techsolutions#sso.egi.eu
- urn:mace:egi.eu:group:notebooks-support#sso.egi.eu
auto_login: true
claim_groups_key: "eduperson_entitlement"
EGICheckinAuthenticator:
checkin_host: "{{ secret['checkin_host'] }}"
authorize_url: "https://{{ secret['checkin_host'] }}/auth/realms/egi/protocol/openid-connect/auth"
token_url: "https://{{ secret['checkin_host'] }}/auth/realms/egi/protocol/openid-connect/token"
userdata_url: "https://{{ secret['checkin_host'] }}/auth/realms/egi/protocol/openid-connect/userinfo"
client_id: "{{ secret['client_id'] }}"
client_secret: "{{ secret['client_secret'] }}"
oauth_callback_url: "https://fullhub.eosc.zcu.cz/hub/oauth_callback"
scope: ["openid", "profile", "email", "offline_access", "eduperson_scoped_affiliation", "eduperson_entitlement"]
username_key: "sub"
JupyterHub:
admin_access: true
authenticate_prometheus: false
authenticator_class: egi_notebooks_hub.egiauthenticator.EGICheckinAuthenticator
# spawner_class: (in egi-notebooks-b2drop)
extraConfig:
egi-notebooks-welcome: |-
from egi_notebooks_hub.welcome import WelcomeHandler
c.JupyterHub.default_url = "/welcome"
c.JupyterHub.extra_handlers = [(r'/welcome', WelcomeHandler)]
egi-notebooks-b2drop: |-
{% raw %}
import base64
from jinja2 import BaseLoader
from jinja2 import Environment
from egi_notebooks_hub.onedata import OnedataSpawner
from kubernetes_asyncio.client.rest import ApiException
class B2DropSpawner(OnedataSpawner):
async def auth_state_hook(self, spawner, auth_state):
await super().auth_state_hook(spawner, auth_state)
self.b2drop_ready = False
self.b2drop_user = ""
self.b2drop_pwd = ""
try:
secret = await self.api.read_namespaced_secret(self.token_secret_name, self.namespace)
except ApiException:
return
if secret and secret.data:
self.b2drop_user = base64.b64decode(secret.data.get("b2drop-user", "")).decode()
self.b2drop_pwd = base64.b64decode(secret.data.get("b2drop-pwd", "")).decode()
self.b2drop_ready = (self.b2drop_user and self.b2drop_pwd)
def _render_options_form(self, profile_list):
# old:self._profile_list = self._init_profile_list(profile_list)
self._profile_list = self._get_initialized_profile_list(profile_list)
profile_form_template = Environment(loader=BaseLoader).from_string(
self.profile_form_template
)
return profile_form_template.render(profile_list=self._profile_list, b2drop_ready=self.b2drop_ready, b2drop_user=self.b2drop_user, b2drop_pwd=self.b2drop_pwd)
async def pre_spawn_hook(self, spawner):
await super(B2DropSpawner, self).pre_spawn_hook(spawner)
b2drop_user = self.user_options.get("b2drop-user", "")
b2drop_pwd = self.user_options.get("b2drop-pwd", "")
b2drop_remember = self.user_options.get("b2drop-remember", None)
if not (b2drop_user and b2drop_pwd):
secret = await self.api.read_namespaced_secret(self.token_secret_name, self.namespace)
if secret and secret.data:
b2drop_user = base64.b64decode(secret.data.get("b2drop-user", "")).decode()
b2drop_pwd = base64.b64decode(secret.data.get("b2drop-pwd", "")).decode()
if b2drop_user and b2drop_pwd:
volume_mounts = [
{"mountPath": "/b2drop:shared", "name": "b2drop"},
]
spawner.extra_containers.append(
{
"name": "b2drop",
"image": "eginotebooks/webdav-sidecar:sha-e5e8df2",
"env": [
{"name": "WEBDAV_URL", "value": "https://b2drop.eudat.eu/remote.php/webdav"},
{"name": "WEBDAV_PWD", "value": b2drop_pwd},
{"name": "WEBDAV_USER", "value": b2drop_user},
{"name": "MOUNT_PATH", "value": "/b2drop"},
],
"resources": self.sidecar_resources,
# "command": cmd,
"securityContext": {
"runAsUser": 0,
"privileged": True,
"capabilities": {"add": ["SYS_ADMIN"]},
},
"volumeMounts": volume_mounts,
"lifecycle": {
"preStop": {
"exec": {"command": ["umount", "-l", "/b2drop"]}
},
},
}
)
if b2drop_remember:
await self._update_secret({"b2drop-user": b2drop_user,
"b2drop-pwd": b2drop_pwd})
else:
await self._update_secret({"b2drop-user": "", "b2drop-pwd": ""})
def options_from_form(self, formdata):
data = super(B2DropSpawner, self)._options_from_form(formdata)
data.update({'b2drop-user': formdata.get('b2drop-user', [None])[0],
'b2drop-remember': formdata.get('b2drop-remember', [None])[0],
'b2drop-pwd': formdata.get('b2drop-pwd', [None])[0]})
return data
c.JupyterHub.spawner_class = B2DropSpawner
c.B2DropSpawner.http_timeout = 60
c.B2DropSpawner.args = ["--FileCheckpoints.checkpoint_dir='/home/jovyan/.notebookCheckpoints'"]
c.B2DropSpawner.profile_form_template = """
<style>
/*
.profile divs holds two div tags: one for a radio button, and one
for the profile's content.
*/
#kubespawner-profiles-list .profile {
display: flex;
flex-direction: row;
font-weight: normal;
border-bottom: 1px solid #ccc;
padding-bottom: 12px;
}
#kubespawner-profiles-list .profile .radio {
padding: 12px;
}
/* .option divs holds a label and a select tag */
#kubespawner-profiles-list .profile .option {
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 12px;
}
#kubespawner-profiles-list .profile .option label {
font-weight: normal;
margin-right: 8px;
min-width: 96px;
}
</style>
<div class='form-group' id='kubespawner-profiles-list'>
{%- for profile in profile_list %}
{#- Wrap everything in a <label> so clicking anywhere selects the option #}
<label for='profile-item-{{ profile.slug }}' class='profile'>
<div class='radio'>
<input type='radio' name='profile' id='profile-item-{{ profile.slug }}' value='{{ profile.slug }}' {% if profile.default %}checked{% endif %} />
</div>
<div>
<h3>{{ profile.display_name }}</h3>
{%- if profile.description %}
<p>{{ profile.description }}</p>
{%- endif %}
{%- if profile.profile_options %}
<div>
{%- for k, option in profile.profile_options.items() %}
<div class='option'>
<label for='profile-option-{{profile.slug}}-{{k}}'>{{option.display_name}}</label>
<select name="profile-option-{{profile.slug}}-{{k}}" class="form-control">
{%- for k, choice in option['choices'].items() %}
<option value="{{ k }}" {% if choice.default %}selected{%endif %}>{{ choice.display_name }}</option>
{%- endfor %}
</select>
</div>
{%- endfor %}
</div>
{%- endif %}
</div>
</label>
{%- endfor %}
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title">
<a class="collabpsed" role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseOne" aria-expanded="false" aria-controls="collapseOne">
B2DROP connection
</a>
{%if b2drop_ready %}<span class="label label-success">Already configured!</span>{% endif %}
</h4>
</div>
<div id="collapseOne" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingOne">
<div class="panel-body">
<div class='form-group'>
<label for="b2drop-user" class="form-label">B2DROP app Username</label>
<input type="text" class="form-control" name="b2drop-user" id="b2drop-user" aria-describedby="b2drop-user-help" value="{{ b2drop_user }}">
<div id="b2drop-user-help" class="form-text">Create new app password at <a href="https://b2drop.eudat.eu/settings/user/security">B2DROP security configuration</a></div>
</div>
<div class='form-group'>
<label for="b2drop-pwd" class="form-label">B2DROP app Password</label>
<input type="password" class="form-control" name="b2drop-pwd" id="b2drop-pwd" value="{{ b2drop_pwd }}">
</div>
<div class='form-group'>
<input type="checkbox" id="b2drop-remember" name="b2drop-remember" {%if b2drop_ready %}checked{% endif %}>
<label class="form-check-label" for="from-check-input">Remember B2DROP credentials</label>
</div>
</div>
</div>
</div>
</div>
"""
{% endraw %}
templatePaths:
- /egi-notebooks-hub/templates
extraFiles:
login.html:
mountPath: /egi-notebooks-hub/templates/login.html
stringData: |-
{% raw %}
{% extends "egi-login.html" %}
{% block main_intro %}
<h1><img alt="Notebooks Logo" src="{{ static_url('images/egi-icon-notebooks.svg') }}"
height="100">Notebooks</h1>
<p>
Notebooks is an environment based on <a href="http://jupyter.org/">Jupyter</a> and
the <a href="https://www.egi.eu/services/cloud-compute/">EGI cloud service</a> that
offers a browser-based, scalable tool for interactive data analysis. The Notebooks
environment provides users with notebooks where they can combine text, mathematics,
computations and rich media output.
</p>
<p>
Access requires a valid <a href="https://docs.egi.eu/users/check-in/signup">EGI account</a>
and <a href="https://docs.egi.eu/users/dev-env/notebooks/#notebooks-for-researchers">
enrolling to one of the supported VOs</a>.
</p>
<p>
Default environment provides 2 CPU cores, 6 GB RAM and 20GB of personal storage space per user
</p>
{% endblock main_intro %}
{% endraw %}
---
- name: Notebooks deployments
hosts: master
become: true
tasks:
- name: Configure helm repo
shell: |-
helm repo add jupyterhub https://jupyterhub.github.io/helm-chart/
helm repo update
when: "'jupyterhub' not in ansible_local.helm_repos | map(attribute='name') | list"
- name: Get Secrets from Vault for notebooks
vars:
name: "{{ item | basename | splitext | first }}"
set_fact:
secrets: "{{ secrets|default({}) | combine({name: lookup('community.hashi_vault.hashi_vault', vault_mount_point + '/deployment-' + name,
token_validate=false)}) }}"
with_fileglob:
- "../deployments/*.yaml"
- name: Get Secrets from Vault for gateway
set_fact:
gateways_token: "{{ {'cesnet-mcc': lookup('community.hashi_vault.hashi_vault', vault_mount_point + '/gateway_authtoken:value',
token_validate=false)} }}"
# - name: Debug Deployments Secrets
# debug:
# msg: "{{ item.key }} = {{ item.value }}"
# loop: "{{ secrets | dict2items }}"
# - name: Debug Gateway Secrets
# debug:
# msg: "{{ item.key }} = {{ item.value }}"
# loop: "{{ gateways_token | dict2items }}"
- name: Copy config file to master
vars:
name: "{{ item | basename | splitext | first }}"
secret: "{{ secrets['central'] }}"
gateways_token: "{{ gateways_token }}"
template:
src: "{{ item }}"
dest: "/tmp/{{ item | basename }}"
mode: 0600
with_fileglob:
- "../deployments/*.yaml"
- name: Deploy/upgrade notebook instance
vars:
name: "{{ item | basename | splitext | first }}"
version: "3.2.1" # app 4.0.2 (2023-11-27)
shell: |-
helm status --namespace {{ name }} {{ name }}
if [ $? -ne 0 ]; then
helm install --create-namespace --namespace {{ name }} \
-f /tmp/{{ item | basename }} --version {{ version }} --timeout 2h \
{{ name }} jupyterhub/jupyterhub
else
helm upgrade --version {{ version }} -f /tmp/{{ item | basename }} --timeout 2h \
--namespace {{ name }} {{ name }} jupyterhub/jupyterhub
fi
environment:
KUBECONFIG: /etc/kubernetes/admin.conf
PATH: /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
when: true
with_fileglob:
- "../deployments/*.yaml"
- name: Configure secrets management for the hub
vars:
name: "{{ item | basename | splitext | first }}"
shell: |-
kubectl apply -f - << EOF
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: hub-secrets
namespace: {{ name }}
rules:
- apiGroups: [""] # "" indicates the core API group
resources: ["secrets"]
verbs: ["get", "watch", "list", "create", "delete", "patch", "update"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: hub-secrets
namespace: {{ name }}
subjects:
- kind: ServiceAccount
name: hub
namespace: {{ name }}
roleRef:
kind: Role
name: hub-secrets
apiGroup: rbac.authorization.k8s.io
EOF
environment:
KUBECONFIG: /etc/kubernetes/admin.conf
PATH: /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
when: true
with_fileglob:
- "../deployments/*.yaml"
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment