Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jupyterhub deployment #5

Merged
merged 6 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ COPY metadynminer/ /tmp/build/metadynminer/

RUN pip3 install /tmp/build

COPY python_metadynminer.ipynb /home/jan8be
COPY python_metadynminer.ipynb /usr/local/lib
COPY copy-notebook.sh /usr/local/bin/before-notebook.d

RUN mamba install --yes ipywidgets ipympl

30 changes: 30 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
image=cerit.io/ljocha/metadynminer
tag=latest

ns=metadynminer-ns

build:
docker build -t ${image}:${tag} .
docker push ${image}:${tag}

restart: uninstall install


install:
helm install metadynminer -n ${ns} jupyterhub/jupyterhub \
-f helm/values.yaml \
--set hub.config.GenericOAuthenticator.client_secret=${shell cat helm/client_secret} \
--set-file hub.extraConfig.form-0=helm/form-0.py \
--set-file hub.extraConfig.pre-spawn-hook=helm/pre-spawn-hook.py \
--set hub.config.notebookImage=${image}:${tag} \


uninstall:
helm uninstall metadynminer -n ${ns}

log:
kubectl -n ${ns} logs -f $(shell kubectl -n metadynminer-ns get pods | grep hub | cut -f1 -d' ')


usage:
kubectl -n ${ns} exec $(shell kubectl -n metadynminer-ns get pods | grep hub | cut -f1 -d' ') -- cat /srv/jupyterhub/usage.log
5 changes: 5 additions & 0 deletions copy-notebook.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash

if [ ! -f $HOME/python_metadynminer.ipynb ]; then
cp /usr/local/lib/python_metadynminer.ipynb $HOME
fi
31 changes: 31 additions & 0 deletions helm/form-0.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from traitlets import default, Unicode
from tornado import gen
from kubespawner import KubeSpawner

class SillySpawner(KubeSpawner):
form_template = Unicode("""
<h3>Home</h3>
<div id="phomeDiv" style="display: block;">
<input type="checkbox" id="delhome" name="delhome" value="delete">
<label for="delhome">Erase if home exists</label><br/>
<p style="background-color:orange;">Take care of checking this button, it removes whole home directory and previous data will be lost. Use in case only when notebook is broken so it does not start, in other cases, remove data from terminal.</p>
</div>
""",)

option_template = Unicode("""
<option value="{item}">{item}</option>""",
config = True, help = "Template for html form options."
)


async def get_options_form(self):
return self.form_template


c.JupyterHub.spawner_class = SillySpawner
c.MappingKernelManager.cull_idle_timeout = 259200
c.MappingKernelManager.cull_connected = False
c.MappingKernelManager.cull_busy = False
c.NotebookApp.shutdown_no_activity_timeout = 259200

c.Authenticator.allowed_users = set()
231 changes: 231 additions & 0 deletions helm/pre-spawn-hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import asyncio
import kubernetes_asyncio
from kubernetes_asyncio import config, client
import datetime

#nsprefix = 'mtdminer-'
#nssuffix = '-prod-ns'
ns = 'metadynminer-ns'
usagelog = '/srv/jupyterhub/usage.log'

from kubernetes_asyncio.client import (
V1ObjectMeta,
V1Secret,
V1PersistentVolume,
V1PersistentVolumeClaim,
V1ResourceRequirements,
V1LabelSelector,
V1CSIPersistentVolumeSource,
V1PersistentVolumeSpec,
V1PersistentVolumeClaimSpec,
V1Namespace,
V1ServiceAccount,
V1RoleBinding,
V1RoleRef,
V1Subject,
V1ClusterRole,
V1PolicyRule,
ApiException,
)

async def check_pvc(home_pvc_name, namespace):
async with kubernetes_asyncio.client.ApiClient() as api_client:
v1 = kubernetes_asyncio.client.CoreV1Api(api_client)
pvcs = await v1.list_namespaced_persistent_volume_claim(namespace)
for claim in pvcs.items:
if claim.metadata.name == home_pvc_name:
return claim
return None

async def delete_pvc(namespace, pvc):
async with kubernetes_asyncio.client.ApiClient() as api_client:
v1 = kubernetes_asyncio.client.CoreV1Api(api_client)
await v1.delete_namespaced_persistent_volume_claim(name=pvc, namespace=namespace)
await asyncio.sleep(1)

async def create_pvc(home_pvc_name, home_pv_name, namespace, storage_class, capacity):
pvc = V1PersistentVolumeClaim()
pvc.api_version = "v1"
pvc.kind = "PersistentVolumeClaim"
pvc.metadata = V1ObjectMeta()
pvc.metadata.name = home_pvc_name
pvc.spec = V1PersistentVolumeClaimSpec()
pvc.spec.access_modes = ['ReadWriteMany']
pvc.spec.resources = V1ResourceRequirements()
pvc.spec.resources.requests = {"storage": capacity}
pvc.spec.storage_class_name = storage_class
if storage_class != "nfs-csi":
pvc.spec.selector = V1LabelSelector()
pvc.spec.selector.match_labels = {"name": home_pv_name}
try:
async with kubernetes_asyncio.client.ApiClient() as api_client:
v1 = kubernetes_asyncio.client.CoreV1Api(api_client)
x = await v1.create_namespaced_persistent_volume_claim(namespace, pvc)
await asyncio.sleep(1)
except ApiException as e:
if re.search("object is being deleted:", e.body):
raise web.HTTPError(401, "Can't delete PVC {}, please contact administrator!".format(home_pvc_name))
return False
return True

def add_volume(spawner_vol_list, volume, volname):
volume_exists = False
for vol in spawner_vol_list:
if "name" in vol and vol["name"] == volname:
volume_exists = True
if not volume_exists:
spawner_vol_list.append(volume)

def mount(spawner, pv, pvc, mountpath):
volume = {"name": pv, "persistentVolumeClaim": {"claimName": pvc}}
volume_mount = {"mountPath": mountpath, "name": pv}
if len(spawner.volumes) == 0:
spawner.volumes = [volume]
else:
add_volume(spawner.volumes, volume, pv)
if len(spawner.volume_mounts) == 0:
spawner.volume_mounts = [volume_mount]
else:
add_volume(spawner.volume_mounts, volume_mount, pvc)

async def mount_persistent_hub_home(spawner, username, namespace):
hub_home_name = username + "-home-default"

if spawner.user_options.get('delhome') == "delete":
pvc = await check_pvc(hub_home_name, namespace)
if pvc:
await delete_pvc(namespace, hub_home_name)
await create_pvc(hub_home_name, hub_home_name + "-pv", namespace, "nfs-csi", "10Gi")
else:
pvc = await check_pvc(hub_home_name, namespace)
if not pvc:
await create_pvc(hub_home_name, hub_home_name + "-pv", namespace, "nfs-csi", "10Gi")

mount(spawner, hub_home_name + "-pv", hub_home_name, "/home/jovyan")

async def check_ns(user_ns):
async with kubernetes_asyncio.client.ApiClient() as api_client:
v1 = kubernetes_asyncio.client.CoreV1Api(api_client)
nss = await v1.list_namespace(watch=False)
for ns in nss.items:
if ns.metadata.name == user_ns:
ann = ns.metadata.annotations.get("field.cattle.io/projectId")
if not ann or ann != 'c-m-qvndqhf6:p-wt9xp':
return True, False
return True, True
return False, False


async def create_ns(username, original):
namespace = nsprefix + username + nssuffix
exists, ann = await check_ns(namespace)
if not exists:
ns = V1Namespace()
ns.metadata = V1ObjectMeta(name=namespace,
annotations={'field.cattle.io/projectId': 'c-m-qvndqhf6:p-wt9xp', 'user': original},
labels={'hub.jupyter.org/network-access-hub': 'true'})

async with kubernetes_asyncio.client.ApiClient() as api_client:
v1 = kubernetes_asyncio.client.CoreV1Api(api_client)
await v1.create_namespace(body=ns)
await asyncio.sleep(1)
return namespace
if exists and not ann:
raise web.HTTPError(401, "Non-labelled namespace error! Please contact administrator at [email protected].")
return namespace

async def check_sa(user_sa, namespace):
async with kubernetes_asyncio.client.ApiClient() as api_client:
v1 = kubernetes_asyncio.client.CoreV1Api(api_client)
sas = await v1.list_namespaced_service_account(namespace=namespace)
for sa in sas.items:
if sa.metadata.name == user_sa:
return True
return False


async def create_sa(username, namespace):
sa_name = "sa-" + username
exists = await check_sa(sa_name, namespace)
if not exists:
sa = V1ServiceAccount()
sa.metadata = V1ObjectMeta(name=sa_name)
async with kubernetes_asyncio.client.ApiClient() as api_client:
v1 = kubernetes_asyncio.client.CoreV1Api(api_client)
await v1.create_namespaced_service_account(namespace=namespace, body=sa)
await asyncio.sleep(1)
return sa_name


async def check_rb(namespace):
async with kubernetes_asyncio.client.ApiClient() as api_client:
v1 = kubernetes_asyncio.client.RbacAuthorizationV1Api(api_client)
rbs = await v1.list_namespaced_role_binding(namespace=namespace)
for rb in rbs.items:
if rb.metadata.name == "hub-resources-access-binding":
return True
return False


async def create_rb(sa_name, namespace):
try:
rb = V1RoleBinding(role_ref=V1RoleRef(api_group="rbac.authorization.k8s.io", kind="ClusterRole",
name="hub-resources-access"))
rb.metadata = V1ObjectMeta(name="hub-resources-access-binding", namespace=namespace)
rb.subjects = [V1Subject(kind="ServiceAccount", name=sa_name)]
async with kubernetes_asyncio.client.ApiClient() as api_client:
v1 = kubernetes_asyncio.client.RbacAuthorizationV1Api(api_client)
await v1.create_namespaced_role_binding(namespace=namespace, body=rb)
await asyncio.sleep(1)
except kubernetes_asyncio.client.exceptions.ApiException as e:
pass # buzz off



async def bootstrap_pre_spawn(spawner):
config.load_incluster_config()
namespace = spawner.namespace
username = spawner.user.name
original = username
if "-" in username:
username = username.replace("-", "-2d")
if "_" in username:
username = username.replace("_", "-5f")

# spawner.environment = {"JUPYTERHUB_API_URL": "http://hub.gmxhub-ns.svc.cluster.local:8081/hub/api",
# "JUPYTERHUB_ACTIVITY_URL": "http://hub.gmxhub-ns.svc.cluster.local:8081/hub/api/users/"+username+"/activity"}

# ns = await create_ns(username, original)
# sa = await create_sa(username, ns)
# sa = 'hub'
# await create_rb(sa, ns)

await mount_persistent_hub_home(spawner, username, ns)

# spawner.args += [ '--port=8888', '--ip=0.0.0.0', f'--NotebookApp.base_url=/user/{username}/' ]
# spawner.args += [ '--port=8888', '--ip=0.0.0.0' ]

with open(usagelog,"a") as l:
l.write(datetime.datetime.now(datetime.timezone.utc).strftime('%c %z:') + username + '\n')

# gpu = spawner.user_options.get('gpu')
# cpu = spawner.user_options.get('cpu')
# mem = spawner.user_options.get('mem')
# image = spawner.user_options.get('container_image')

# spawner.image = 'ljocha/gromacs-hub'
spawner.image = get_config('hub.config.notebookImage')
spawner.cpu_limit = 1.
spawner.cpu_guarantee = .2
spawner.mem_limit = '8G'
spawner.mem_guarantee = '4G'
spawner.container_security_context = {"capabilities": {"drop": ["ALL"]}}

c.KubeSpawner.pre_spawn_hook = bootstrap_pre_spawn
#c.KubeSpawner.enable_user_namespaces = True
#c.KubeSpawner.user_namespace_template = nsprefix + "{username}" + nssuffix
#c.KubeSpawner.enable_user_namespaces = False
#c.KubeSpawner.user_namespace_template = "jupyterhub-{username}-prod-ns"
c.KubeSpawner.automount_service_account_token = False
#c.KubeSpawner.service_account = "sa-{username}"
# c.KubeSpawner.service_account = "metadynminer-jobs"
Loading
Loading