Added Nbgrader

This commit is contained in:
2025-07-15 18:40:59 +02:00
parent a6db921bea
commit d7e7733166
25 changed files with 1354 additions and 17 deletions

48
jupyterhub/Dockerfile Normal file
View File

@@ -0,0 +1,48 @@
FROM quay.io/jupyterhub/jupyterhub:latest
# Install system dependencies
USER root
RUN apt-get update && apt-get install -y \
curl \
gnupg \
iproute2 \
iputils-ping \
netcat \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Install Docker CLI (needed for DockerSpawner)
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu focal stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \
&& apt-get update \
&& apt-get install -y docker-ce-cli \
&& rm -rf /var/lib/apt/lists/*
# Install Required packages
RUN pip install --no-cache-dir \
dockerspawner \
jupyterhub-nativeauthenticator \
jupyterhub-dummyauthenticator \
nbgrader \
psycopg2-binary
RUN mkdir -p /srv/nbgrader/exchange
RUN chmod -R 777 /srv/nbgrader/exchange
RUN mkdir -p /srv/nbgrader/courses
RUN chown -R 1000:100 /srv/nbgrader
#RUN jupyter nbextension install --sys-prefix --py nbgrader --overwrite
#RUN jupyter nbextension enable --sys-prefix --py nbgrader
#RUN jupyter serverextension enable --sys-prefix --py nbgrader
# Set Workdir
WORKDIR /srv/jupyterhub
# Generate Cookie Secret
RUN openssl rand -hex 32 > /srv/jupyterhub/cookie_secret
RUN chmod 600 /srv/jupyterhub/cookie_secret
#ENV JUPYTERHUB_AUTH_TOKEN=$(openssl rand -hex 32)
EXPOSE 8000
CMD ["jupyterhub", "-f", "/srv/jupyterhub/jupyterhub_config.py"]

15
jupyterhub/compose.yml Normal file
View File

@@ -0,0 +1,15 @@
services:
jupyterhub:
build: .
container_name: jupyterhub
restart: unless-stopped
ports:
- 8000:8000
environment:
- JUPYTERHUB_LOG_LEVEL=DEBUG
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./templates:/srv/jupyterhub/templates:ro
- ./jupyterhub_config.py:/srv/jupyterhub/jupyterhub_config.py
- ./nbgrader_config.py:/srv/nbgrader/courses/nbgrader_config.py
- ../logs/jupyterhub:/var/log/jupyterhub

9
jupyterhub/conf_test.py Normal file
View File

@@ -0,0 +1,9 @@
c = get_config()
c.JupyterHub.authenticator_class = "dummy"
c.JupyterHub.spawner_class = "docker"
c.JupyterHub.hub_ip = '0.0.0.0'
c.JupyterHub.hub_connect_ip = 'jupyterhub'
c.DockerSpawner.image = 'jupyter/base-notebook'
c.DockerSpawner.network_name = 'stack_default'
c.DockerSpawner.remove = True

View File

@@ -0,0 +1,227 @@
import os
import sys
from dockerspawner import DockerSpawner
from jupyterhub.utils import random_port
from nativeauthenticator import NativeAuthenticator
#from nbgrader.auth import JupyterHubAuthPlugin
from traitlets import Unicode, Bool, Int, Float
import secrets
import string
from datetime import datetime, timedelta
from typing import Optional
def generate_api_token(
length: int = 32,
prefix: Optional[str] = None,
) -> dict:
"""
Generate a secure random API token with optional prefix and expiration.
Args:
length: Length of the token (default: 32)
prefix: Optional prefix for the token (e.g., 'hub_')
expiration_days: Optional expiration in days from now
Returns:
- token: The generated token
"""
# Generate cryptographically secure random string
alphabet = string.ascii_letters + string.digits
token = ''.join(secrets.choice(alphabet) for _ in range(length))
# Add prefix if specified
if prefix:
token = f"{prefix}{token}"
return token
import subprocess
def is_service_available_cmd(host, port):
"""Check service using system commands"""
try:
subprocess.run(
['nc', '-z', host, str(port)],
check=True,
timeout=3,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
return True
except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
return False
# Configuration file for JupyterHub
c = get_config()
# Base URL configuration (The new way, dont use ip/port/bind_url attributes it throws a warning and is deprecated)
port = int(os.environ.get('JUPYTERHUB_PORT', '8000'))
base_url = os.environ.get('JUPYTERHUB_BASE_URL', '/jupyter/')
c.JupyterHub.bind_url = f"http://0.0.0.0:{port}{base_url}"
# Database configuration
c.JupyterHub.db_url = f"postgresql://{os.environ['POSTGRES_USER']}:{os.environ['POSTGRES_PASSWORD']}@stack-{os.environ['POSTGRES_HOST']}:5432/{os.environ['POSTGRES_DB']}"
#c.JupyterHub.db_url = 'sqlite:///jupyterhub.sqlite'
# Authenticator configuration - NativeAuthenticator
c.JupyterHub.authenticator_class = NativeAuthenticator
# Native Authenticator settings
#c.NativeAuthenticator.create_system_users = True
c.NativeAuthenticator.minimum_password_length = int(os.environ.get('NATIVE_AUTH_MIN_PASSWORD_LENGTH', '8'))
c.NativeAuthenticator.check_common_password = True
c.NativeAuthenticator.enable_signup = True
c.NativeAuthenticator.ask_email_on_signup = False
c.NativeAuthenticator.allow_2fa = False
# Admin configuration
admin_user = os.environ.get('JUPYTERHUB_ADMIN_USER', 'admin')
c.Authenticator.admin_users = {admin_user, 'DerGrumpf'}
c.Authenticator.any_allow_config = True
c.Authenticator.allow_all = True
# Custom logo and templates
c.JupyterHub.logo_file = '/srv/jupyterhub/templates/logo.png'
c.JupyterHub.template_paths = ['/etc/jupyterhub/templates']
# Hub environment for containers
c.JupyterHub.spawner_class = DockerSpawner
c.Spawner.ip = '0.0.0.0'
c.DockerSpawner.image = os.environ.get('NOTEBOOK_IMAGE', 'jupyter/scipy-notebook:latest')
'''network_name = os.environ.get('DOCKER_NETWORK_NAME', 'stack_default')
c.DockerSpawner.network_name = network_name
c.DockerSpawner.extra_host_config = {
'network_mode': network_name
}
c.DockerSpawner.remove = True
c.DockerSpawner.debug = True
c.DockerSpawner.default_url = os.environ.get('SPAWNER_DEFAULT_URL', '/lab')
c.DockerSpawner.use_internal_ip = True
# Resources
c.DockerSpawner.mem_limit = os.environ.get('NOTEBOOK_MEMORY_LIMIT', '500M')
c.DockerSpawner.cpu_limit = float(os.environ.get('NOTEBOOK_CPU_LIMIT', '1.0'))
# Volume
c.DockerSpawner.volumes = {
'/var/run/docker.sock': '/var/run/docker.sock'
# './data/jupyter/users/{username}': '/home/jovyan/work',
# './data/jupyter/nbgrader/exchange': '/srv/nbgrader/exchange',
# './data/jupyter/nbgrader/courses': '/srv/nbgrader/courses',
}
c.DockerSpawner.notebook_dir = '/home/jovyan/work'
# Container Environment
c.DockerSpawner.environment = {
'GRANT_SUDO': '0',
'JUPYTER_ENABLE_LAB': os.environ.get('ENABLE_LAB', '1'),
'JUPYTERHUB_SINGLEUSER_APP': 'jupyter_server.serverapp.ServerApp'
}
#c.DockerSpawner.hub_ip_connect = os.environ.get('HUB_IP', 'jupyterhub')
#c.DockerSpawner.hub_port_connect = 8081
def pre_spawn_hook(spawner):
"""Create user directories before spawning"""
username = spawner.user.name
user_dir = f"./data/jupyter/users/{username}"
import os
import stat
if not os.path.exists(user_dir):
os.makedirs(user_dir, mode=0o755, exist_ok=True)
os.chown(user_dir, 1000, 1000)
# TODO Nbgrader dirs
c.DockerSpawner.pre_spawn_hook = pre_spawn_hook
'''
'''
# Services configuration for NBGrader
c.JupyterHub.services = [
{
'name': 'nbgrader-formgrader',
'url': 'http://127.0.0.1:9999',
'command': [
'jupyter-nbgrader',
'--port=9999',
'--no-browser',
'--log-level=INFO'
],
'cwd': '/srv/jupyterhub/courses',
'user': 'root',
'environment': {
'PYTHONPATH': '/srv/nbgrader',
'NBGRADER_CONFIG_FILE': '/srv/jupyterhub/nbgrader/nbgrader_config.py'
}
}
]
'''
'''
# NBGrader configuration
c.JupyterHub.load_groups = {
'nbgrader-instructors': ['instructor'],
'nbgrader-students': []
}
'''
'''
# NBGrader Config
c.JupyterHub.services = [
{
'name': 'nbgrader-formgrader',
'url': 'http://127.0.0.1:9999',
'api_token': generate_api_token(prefix='nbgrader'),
'command': [
'python', '-m', 'nbgrader', 'formgrader',
#'--port=9999',
#'--no-browser',
'--log-level=INFO',
#'--base_url=/jupyter/services/nbgrader-formgrader',
'--debug'
],
'environment': {
'JUPYTERHUB_SERVICE_URL': 'http://127.0.0.1:8081/jupyter/hub/api',
'NBGRADER_CONFIG_FILE': '/srv/nbgrader/nbgrader_config.py',
'PYTHONPATH': '/srv/nbgrader'
},
'cwd': '/srv/nbgrader/courses',
'user': 'root'
}
]
'''
# Security settings
c.JupyterHub.cookie_secret_file = '/srv/jupyterhub/cookie_secret'
c.ConfigurableHTTPProxy.auth_token = os.environ.get('JUPYTERHUB_AUTH_TOKEN', '')
# Logging
c.JupyterHub.log_level = os.environ.get('LOG_LEVEL', 'INFO')
# Shutdown settings
c.JupyterHub.shutdown_on_logout = False
c.JupyterHub.cleanup_servers = True
# Timeout settings
c.JupyterHub.active_server_limit = 0
c.JupyterHub.concurrent_spawn_limit = 10
# Allow named servers
c.JupyterHub.allow_named_servers = True
c.JupyterHub.named_server_limit_per_user = 3
print("JupyterHub configuration loaded successfully!")
print(f"Base URL: {c.JupyterHub.base_url}")
print(f"Bind URL: {c.JupyterHub.bind_url}")
print(f"Database URL: {c.JupyterHub.db_url}")
print(f"Docker Network: {c.DockerSpawner.network_name}")
print(f"Docker Image: {c.DockerSpawner.image}")
print("Postgres available", is_service_available_cmd("postgres", 5432))
with open("/srv/jupyterhub/cookie_secret") as f:
print(f.readlines())

View File

@@ -0,0 +1,30 @@
# /srv/nbgrader/nbgrader_config.py
c = get_config()
# 1. Course Configuration
c.CourseDirectory.course_id = "intro-to-python" # Change to your course name
c.CourseDirectory.root = '/srv/nbgrader/courses'
# 2. Exchange Directory (for submitting/collecting assignments)
c.Exchange.root = '/srv/nbgrader/exchange'
c.Exchange.path_includes_course = True
# 3. JupyterHub Integration
#c.NbGrader.hub_url = 'http://jupyterhub:8081/hub/api'
#c.NbGrader.hubapi_token = 'your-hub-token' # Generate with: openssl rand -hex 32
# 4. Database Configuration
c.StudentAssignmentNotebook.database_url = 'sqlite:////srv/nbgrader/nbgrader.sqlite'
# 5. Assignment Policies
c.ExecutePreprocessor.timeout = 300 # 5 minutes timeout per cell
c.ClearSolutions.code_stub = "# YOUR CODE HERE"
c.ClearSolutions.text_stub = "YOUR ANSWER HERE"
# 6. Formgrader UI Settings
c.FormgradeApp.port = 9999
c.FormgradeApp.authenticator_class = 'nbgrader.auth.hubauth.HubAuth'
c.FormgradeApp.ip = '0.0.0.0'
# 7. Permissions
c.CourseDirectory.groupshared = True

View File

@@ -0,0 +1,30 @@
# /srv/nbgrader/nbgrader_config.py
c = get_config()
# 1. Course Configuration
c.CourseDirectory.course_id = "intro-to-python" # Change to your course name
c.CourseDirectory.root = '/srv/nbgrader/courses'
# 2. Exchange Directory (for submitting/collecting assignments)
c.Exchange.root = '/srv/nbgrader/exchange'
c.Exchange.path_includes_course = True
# 3. JupyterHub Integration
#c.NbGrader.hub_url = 'http://jupyterhub:8081/hub/api'
#c.NbGrader.hubapi_token = 'your-hub-token' # Generate with: openssl rand -hex 32
# 4. Database Configuration
c.StudentAssignmentNotebook.database_url = 'sqlite:////srv/nbgrader/nbgrader.sqlite'
# 5. Assignment Policies
c.ExecutePreprocessor.timeout = 300 # 5 minutes timeout per cell
c.ClearSolutions.code_stub = "# YOUR CODE HERE"
c.ClearSolutions.text_stub = "YOUR ANSWER HERE"
# 6. Formgrader UI Settings
c.FormgradeApp.port = 9999
c.FormgradeApp.authenticator_class = 'nbgrader.auth.hubauth.HubAuth'
c.FormgradeApp.ip = '0.0.0.0'
# 7. Permissions
c.CourseDirectory.groupshared = True

View File

@@ -0,0 +1,74 @@
{% extends "templates/page.html" %}
{% block login %}
<div class="container login-container">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="login-panel panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Welcome to JupyterHub</h3>
</div>
<div class="panel-body">
<form action="{{authenticator_login_url}}" method="post" role="form">
<div class="form-group">
<label for="username_input">User</label>
<input
id="username_input"
type="text"
class="form-control"
name="username"
autocapitalize="off"
autocorrect="off"
required
/>
</div>
<div class="form-group">
<label for="password_input">Password</label>
<input
id="password_input"
type="password"
class="form-control"
name="password"
required
/>
</div>
<div class="form-group">
<input
type="submit"
id="login_submit"
class="btn btn-jupyter btn-lg btn-block"
value="Sign In"
/>
</div>
{% if login_error %}
<div class="alert alert-danger" role="alert">
{{login_error}}
</div>
{% endif %}
</form>
{% if authenticator.enable_signup %}
<div class="text-center">
<p>Don't have an account? <a href="{{authenticator_signup_url}}">Sign up</a></p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock login %}
<style>
.btn-jupyter {
background-color: #FFFFFF;
color: white;
border-color: #F37626;
}
.btn-jupyter:hover {
background-color: #D85F1C;
border-color: #D85F1C;
}
.login-container {
margin-top: 50px;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="utf-8">
<title>JupyterHub</title>
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/jupyter/hub/static/css/style.min.css?v=90495ca2cd6745c4b19a42dfd4b244ac5ca697ae76bf6f58a465da54045d2e0032f25207e2ebe4df838e4d7bd40c183228f28bbacc2456fe706797438809f749" type="text/css">
<link rel="icon" href="/jupyter/hub/static/favicon.ico?v=fde5757cd3892b979919d3b1faa88a410f28829feb5ba22b6cf069f2c6c98675fceef90f932e49b510e74d65c681d5846b943e7f7cc1b41867422f0481085c1f" type="image/x-icon">
<script src="/jupyter/hub/static/components/bootstrap/dist/js/bootstrap.bundle.min.js?v=ecf8bfa2d7656db091f8b9d6f85ecfc057120c93ae5090773b1b441db838bd232fcef26375ee0fa35bf8051f4675cf5a5cd50d155518f922b9d70593f161741a" type="text/javascript" charset="utf-8"></script>
<script src="/jupyter/hub/static/components/requirejs/require.js?v=1ff44af658602d913b22fca97c78f98945f47e76dacf9800f32f35350f05e9acda6dc710b8501579076f3980de02f02c97f5994ce1a9864c21865a42262d79ec" type="text/javascript" charset="utf-8"></script>
<script src="/jupyter/hub/static/components/jquery/dist/jquery.min.js?v=bf6089ed4698cb8270a8b0c8ad9508ff886a7a842278e98064d5c1790ca3a36d5d69d9f047ef196882554fc104da2c88eb5395f1ee8cf0f3f6ff8869408350fe" type="text/javascript" charset="utf-8"></script>
<script src="/jupyter/hub/static/js/darkmode.js?v=2fd9a7d11ad78df9351fed40ab35eab52e1e6a3d516f188b652120e6faf57b8e387a30aae8f52a6fb51563d06d04545c7005da0b77a98c21b0bd28f6d1cdfa11" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
require.config({
urlArgs: "v=20250711124117",
baseUrl: '/jupyter/hub/static/js',
paths: {
components: '../components',
jquery: '../components/jquery/dist/jquery.min',
moment: "../components/moment/moment",
},
});
window.jhdata = {
base_url: "/jupyter/hub/",
prefix: "/jupyter/",
admin_access: false,
options_form: false,
xsrf_token: "MnwxOjB8MTA6MTc1MjIzNzY4Nnw1Ol94c3JmfDY4OlRtOXVaVHA1TFdsUGJHbHhVa1pSTlZCbWFrbEdjWGhYU1dKUFNVSnVURTlVVkdsTllUQkJYMXBJWmpadGJFUjNQUT09fDlkOTc3NzE2YjVkNGVhZjZjYzE5NWFmMGI3MTE3ZjNjNzVlM2IzYmNmM2M3YWU4YjQ3ZTQ5YmVlZTk5MjYxZWQ",
};
</script>
<meta name="description" content="JupyterHub">
<meta name="keywords" content="Jupyter, JupyterHub">
</head>
<body>
<noscript>
<div id ='noscript'>Jupyterhub requires JavaScript.</div>
</noscript>
Hello
</body>
</html>

View File

@@ -0,0 +1,82 @@
{% extends "templates/page.html" %}
{% block login %}
<div class="container login-container">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<div class="login-panel panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Create Account</h3>
</div>
<div class="panel-body">
<form action="{{authenticator_signup_url}}" method="post" role="form">
<div class="form-group">
<label for="username_input">Username</label>
<input
id="username_input"
type="text"
class="form-control"
name="username"
autocapitalize="off"
autocorrect="off"
required
/>
</div>
<div class="form-group">
<label for="password_input">Password (min. 8 characters)</label>
<input
id="password_input"
type="password"
class="form-control"
name="password"
required
/>
</div>
<div class="form-group">
<label for="password_confirm">Confirm Password</label>
<input
id="password_confirm"
type="password"
class="form-control"
name="password_confirmation"
required
/>
</div>
<div class="form-group">
<input
type="submit"
id="signup_submit"
class="btn btn-jupyter btn-lg btn-block"
value="Create Account"
/>
</div>
{% if signup_error %}
<div class="alert alert-danger" role="alert">
{{signup_error}}
</div>
{% endif %}
</form>
<div class="text-center">
<p>Already have an account? <a href="{{authenticator_login_url}}">Sign in</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock login %}
<style>
.btn-jupyter {
background-color: #F37626;
color: white;
border-color: #F37626;
}
.btn-jupyter:hover {
background-color: #D85F1C;
border-color: #D85F1C;
}
.login-container {
margin-top: 50px;
}
</style>