From 96f3d546b052ecaa2482534dc87c4e9a28fdb7c7 Mon Sep 17 00:00:00 2001 From: stellarshenson Date: Sun, 9 Nov 2025 22:39:59 +0100 Subject: [PATCH] fix: protect jupyterhub_config.py from import errors Fixed NameError when custom_handlers.py imports from jupyterhub_config.py by protecting all c.* assignments with 'if c is not None:' checks. Changes: - Wrapped c = get_config() in try-except to handle module import context - Defined DOCKER_SPAWNER_VOLUMES as module-level constant - Modified get_user_volume_suffixes() to accept volumes dict parameter - USER_VOLUME_SUFFIXES calculated from constant (importable without c defined) - Protected all c.* configuration assignments with 'if c is not None:' guards This allows custom_handlers.py to import USER_VOLUME_SUFFIXES without triggering "NameError: name 'get_config' is not defined" when the config file is imported as a module rather than loaded by JupyterHub. Volume management now works correctly - configuration can be safely imported by handlers to validate volume names against USER_VOLUME_SUFFIXES. --- config/jupyterhub_config.py | 236 +++++++++++++++++++----------------- 1 file changed, 125 insertions(+), 111 deletions(-) diff --git a/config/jupyterhub_config.py b/config/jupyterhub_config.py index 73a1066..c25f8be 100644 --- a/config/jupyterhub_config.py +++ b/config/jupyterhub_config.py @@ -8,7 +8,12 @@ import requests import nativeauthenticator import docker # for gpu autodetection -c = get_config() +# Only call get_config() when running as JupyterHub config (not when imported as module) +try: + c = get_config() +except NameError: + # Being imported as a module, not loaded by JupyterHub + c = None # NVIDIA GPU auto-detection def detect_nvidia(nvidia_autodetect_image='nvidia/cuda:12.9.1-base-ubuntu24.04'): @@ -58,61 +63,57 @@ if ENABLE_GPU_SUPPORT == 2: if NVIDIA_DETECTED: ENABLE_GPU_SUPPORT = 1 # means - gpu enabled else: ENABLE_GPU_SUPPORT = 0 # means - disable -# ensure that we are using SSL, it should be enabled by default -if ENABLE_JUPYTERHUB_SSL == 1: - c.JupyterHub.ssl_cert = '/mnt/certs/server.crt' - c.JupyterHub.ssl_key = '/mnt/certs/server.key' +# Apply JupyterHub configuration (only when loaded by JupyterHub, not when imported) +if c is not None: + # ensure that we are using SSL, it should be enabled by default + if ENABLE_JUPYTERHUB_SSL == 1: + c.JupyterHub.ssl_cert = '/mnt/certs/server.crt' + c.JupyterHub.ssl_key = '/mnt/certs/server.key' -# we use dockerspawner -c.JupyterHub.spawner_class = "dockerspawner.DockerSpawner" + # we use dockerspawner + c.JupyterHub.spawner_class = "dockerspawner.DockerSpawner" -# default env variables passed to the spawned containers -c.DockerSpawner.environment = { - 'TF_CPP_MIN_LOG_LEVEL':TF_CPP_MIN_LOG_LEVEL, # tensorflow logging level: 3 - err only - 'TENSORBOARD_LOGDIR':'/tmp/tensorboard', - 'MLFLOW_TRACKING_URI': 'http://localhost:5000', - 'MLFLOW_PORT':5000, - 'MLFLOW_HOST':'0.0.0.0', # new 3.5 mlflow launched with guinicorn requires this - 'MLFLOW_WORKERS':1, - 'ENABLE_SERVICE_MLFLOW': ENABLE_SERVICE_MLFLOW, - 'ENABLE_SERVICE_GLANCES': ENABLE_SERVICE_GLANCES, - 'ENABLE_SERVICE_TENSORBOARD': ENABLE_SERVICE_TENSORBOARD, - 'ENABLE_GPU_SUPPORT': ENABLE_GPU_SUPPORT, - 'ENABLE_GPUSTAT': ENABLE_GPU_SUPPORT, - 'NVIDIA_DETECTED': NVIDIA_DETECTED, -} - -# configure access to GPU if possible -if ENABLE_GPU_SUPPORT == 1: - c.DockerSpawner.extra_host_config = { - 'device_requests': [ - { - 'Driver': 'nvidia', - 'Count': -1, - 'Capabilities': [['gpu']] - } - ] + # default env variables passed to the spawned containers + c.DockerSpawner.environment = { + 'TF_CPP_MIN_LOG_LEVEL':TF_CPP_MIN_LOG_LEVEL, # tensorflow logging level: 3 - err only + 'TENSORBOARD_LOGDIR':'/tmp/tensorboard', + 'MLFLOW_TRACKING_URI': 'http://localhost:5000', + 'MLFLOW_PORT':5000, + 'MLFLOW_HOST':'0.0.0.0', # new 3.5 mlflow launched with guinicorn requires this + 'MLFLOW_WORKERS':1, + 'ENABLE_SERVICE_MLFLOW': ENABLE_SERVICE_MLFLOW, + 'ENABLE_SERVICE_GLANCES': ENABLE_SERVICE_GLANCES, + 'ENABLE_SERVICE_TENSORBOARD': ENABLE_SERVICE_TENSORBOARD, + 'ENABLE_GPU_SUPPORT': ENABLE_GPU_SUPPORT, + 'ENABLE_GPUSTAT': ENABLE_GPU_SUPPORT, + 'NVIDIA_DETECTED': NVIDIA_DETECTED, } -# spawn containers from this image -c.DockerSpawner.image = os.environ["DOCKER_NOTEBOOK_IMAGE"] + # configure access to GPU if possible + if ENABLE_GPU_SUPPORT == 1: + c.DockerSpawner.extra_host_config = { + 'device_requests': [ + { + 'Driver': 'nvidia', + 'Count': -1, + 'Capabilities': [['gpu']] + } + ] + } -# networking congfiguration -c.DockerSpawner.use_internal_ip = True -c.DockerSpawner.network_name = NETWORK_NAME + # spawn containers from this image + c.DockerSpawner.image = os.environ["DOCKER_NOTEBOOK_IMAGE"] -# prevent auto-spawn for admin users -# Redirect admin to admin panel instead -c.JupyterHub.default_url = JUPYTERHUB_BASE_URL + '/hub/home' + # networking congfiguration + c.DockerSpawner.use_internal_ip = True + c.DockerSpawner.network_name = NETWORK_NAME -# Force container user -c.DockerSpawner.notebook_dir = DOCKER_NOTEBOOK_DIR + # prevent auto-spawn for admin users + # Redirect admin to admin panel instead + c.JupyterHub.default_url = JUPYTERHUB_BASE_URL + '/hub/home' -# Set container name prefix -c.DockerSpawner.name_template = "jupyterlab-{username}" - -# User mounts in the spawned container -c.DockerSpawner.volumes = { +# User mounts in the spawned container (defined as constant for import by handlers) +DOCKER_SPAWNER_VOLUMES = { "jupyterlab-{username}_home": "/home", "jupyterlab-{username}_workspace": DOCKER_NOTEBOOK_DIR, "jupyterlab-{username}_cache": "/home/lab/.cache", @@ -120,23 +121,34 @@ c.DockerSpawner.volumes = { } # Helper function to extract user-specific volume suffixes -def get_user_volume_suffixes(): - """Extract volume suffixes from DockerSpawner.volumes that follow jupyterlab-{username}_ pattern""" +def get_user_volume_suffixes(volumes_dict): + """Extract volume suffixes from volumes dict that follow jupyterlab-{username}_ pattern""" suffixes = [] - for volume_name in c.DockerSpawner.volumes.keys(): + for volume_name in volumes_dict.keys(): # Match pattern: jupyterlab-{username}_ if volume_name.startswith("jupyterlab-{username}_"): suffix = volume_name.replace("jupyterlab-{username}_", "") suffixes.append(suffix) return suffixes -# Store user volume suffixes for use in templates and handlers -USER_VOLUME_SUFFIXES = get_user_volume_suffixes() +# Store user volume suffixes for use in templates and handlers (importable by custom_handlers.py) +USER_VOLUME_SUFFIXES = get_user_volume_suffixes(DOCKER_SPAWNER_VOLUMES) -# Make volume suffixes available to templates -c.JupyterHub.template_vars = { - 'user_volume_suffixes': USER_VOLUME_SUFFIXES -} +# Apply configuration only when running as JupyterHub config +if c is not None: + # Force container user + c.DockerSpawner.notebook_dir = DOCKER_NOTEBOOK_DIR + + # Set container name prefix + c.DockerSpawner.name_template = "jupyterlab-{username}" + + # Set volumes from constant + c.DockerSpawner.volumes = DOCKER_SPAWNER_VOLUMES + + # Make volume suffixes available to templates + c.JupyterHub.template_vars = { + 'user_volume_suffixes': USER_VOLUME_SUFFIXES + } # Built-in groups that cannot be deleted (auto-recreated if missing) BUILTIN_GROUPS = ['docker-privileged'] @@ -167,72 +179,74 @@ async def pre_spawn_hook(spawner): # Ensure docker.sock is not mounted for non-privileged users spawner.volumes.pop('/var/run/docker.sock', None) -c.DockerSpawner.pre_spawn_hook = pre_spawn_hook +# Apply remaining configuration (only when loaded by JupyterHub, not when imported) +if c is not None: + c.DockerSpawner.pre_spawn_hook = pre_spawn_hook -# Ensure containers can accept proxy connections -c.DockerSpawner.args = [ - '--ServerApp.allow_origin=*', - '--ServerApp.disable_check_xsrf=True' -] + # Ensure containers can accept proxy connections + c.DockerSpawner.args = [ + '--ServerApp.allow_origin=*', + '--ServerApp.disable_check_xsrf=True' + ] -# update internal routing for spawned containers -c.JupyterHub.hub_connect_url = 'http://jupyterhub:8080' + JUPYTERHUB_BASE_URL + '/hub' + # update internal routing for spawned containers + c.JupyterHub.hub_connect_url = 'http://jupyterhub:8080' + JUPYTERHUB_BASE_URL + '/hub' -# remove containers once they are stopped -c.DockerSpawner.remove = True + # remove containers once they are stopped + c.DockerSpawner.remove = True -# for debugging arguments passed to spawned containers -c.DockerSpawner.debug = False + # for debugging arguments passed to spawned containers + c.DockerSpawner.debug = False -# user containers will access hub by container name on the Docker network -c.JupyterHub.hub_ip = "jupyterhub" -c.JupyterHub.hub_port = 8080 -c.JupyterHub.base_url = JUPYTERHUB_BASE_URL + '/' + # user containers will access hub by container name on the Docker network + c.JupyterHub.hub_ip = "jupyterhub" + c.JupyterHub.hub_port = 8080 + c.JupyterHub.base_url = JUPYTERHUB_BASE_URL + '/' -# persist hub data on volume mounted inside container -c.JupyterHub.cookie_secret_file = "/data/jupyterhub_cookie_secret" -c.JupyterHub.db_url = "sqlite:////data/jupyterhub.sqlite" + # persist hub data on volume mounted inside container + c.JupyterHub.cookie_secret_file = "/data/jupyterhub_cookie_secret" + c.JupyterHub.db_url = "sqlite:////data/jupyterhub.sqlite" -# authenticate users with Native Authenticator -# enable UI for native authenticator -c.JupyterHub.authenticator_class = 'native' + # authenticate users with Native Authenticator + # enable UI for native authenticator + c.JupyterHub.authenticator_class = 'native' -# Template paths - must include default JupyterHub templates -import jupyterhub -c.JupyterHub.template_paths = [ - "/srv/jupyterhub/templates/", # Custom templates (highest priority) - f"{os.path.dirname(nativeauthenticator.__file__)}/templates/", # NativeAuthenticator templates - f"{os.path.dirname(jupyterhub.__file__)}/templates" # Default JupyterHub templates -] + # Template paths - must include default JupyterHub templates + import jupyterhub + c.JupyterHub.template_paths = [ + "/srv/jupyterhub/templates/", # Custom templates (highest priority) + f"{os.path.dirname(nativeauthenticator.__file__)}/templates/", # NativeAuthenticator templates + f"{os.path.dirname(jupyterhub.__file__)}/templates" # Default JupyterHub templates + ] -# allow anyone to sign-up without approval -# allow all signed-up users to login -c.NativeAuthenticator.open_signup = False -c.NativeAuthenticator.enable_signup = True -c.Authenticator.allow_all = True + # allow anyone to sign-up without approval + # allow all signed-up users to login + c.NativeAuthenticator.open_signup = False + c.NativeAuthenticator.enable_signup = True + c.Authenticator.allow_all = True -# allowed admins -c.Authenticator.admin_users = [JUPYTERHUB_ADMIN] -c.JupyterHub.admin_access = True + # allowed admins + c.Authenticator.admin_users = [JUPYTERHUB_ADMIN] + c.JupyterHub.admin_access = True -# Custom API handlers for volume management and server control -import sys -sys.path.insert(0, '/srv/jupyterhub') -sys.path.insert(0, '/start-platform.d') -sys.path.insert(0, '/') + # Custom API handlers for volume management and server control + import sys + sys.path.insert(0, '/srv/jupyterhub') + sys.path.insert(0, '/start-platform.d') + sys.path.insert(0, '/') -from custom_handlers import ( - ManageVolumesHandler, - RestartServerHandler, - NotificationsPageHandler, - BroadcastNotificationHandler -) + from custom_handlers import ( + ManageVolumesHandler, + RestartServerHandler, + NotificationsPageHandler, + BroadcastNotificationHandler + ) -c.JupyterHub.extra_handlers = [ - (r'/api/users/([^/]+)/manage-volumes', ManageVolumesHandler), - (r'/api/users/([^/]+)/restart-server', RestartServerHandler), - (r'/api/notifications/broadcast', BroadcastNotificationHandler), - (r'/notifications', NotificationsPageHandler), -] + c.JupyterHub.extra_handlers = [ + (r'/api/users/([^/]+)/manage-volumes', ManageVolumesHandler), + (r'/api/users/([^/]+)/restart-server', RestartServerHandler), + (r'/api/notifications/broadcast', BroadcastNotificationHandler), + (r'/notifications', NotificationsPageHandler), + ] # EOF