|
4 | 4 |
|
5 | 5 | from dockerspawner import SystemUserSpawner
|
6 | 6 | from jupyterhub.auth import PAMAuthenticator
|
7 |
| -from jupyter_client.localinterfaces import public_ips |
8 | 7 | from tljh.hooks import hookimpl
|
9 | 8 | from tljh.systemd import check_service_active
|
10 |
| -from traitlets import default |
| 9 | +from tljh_repo2docker import TLJHDockerSpawner |
| 10 | +from traitlets import default, Unicode |
11 | 11 |
|
12 |
| -from .builder import DEFAULT_CPU_LIMIT, DEFAULT_MEMORY_LIMIT |
13 |
| -from .images import list_images, client |
14 | 12 |
|
15 |
| -VOLUMES_PATH = "/home" |
16 |
| -SHARED_DATA_PATH = "/srv/data" |
| 13 | +class PlasmaBioSpawner(SystemUserSpawner, TLJHDockerSpawner): |
17 | 14 |
|
18 |
| -# Default CPU period |
19 |
| -# See: https://docs.docker.com/config/containers/resource_constraints/#limit-a-containers-access-to-memory#configure-the-default-cfs-scheduler |
20 |
| -CPU_PERIOD = 100_000 |
| 15 | + base_path = Unicode("/home", config=True, help="The base path for the user volumes") |
21 | 16 |
|
| 17 | + shared_data_path = Unicode( |
| 18 | + "/srv/data", config=True, help="The path to the shared data folder" |
| 19 | + ) |
22 | 20 |
|
23 |
| -# See: https://github.com/jupyterhub/jupyterhub/tree/master/examples/bootstrap-script#example-1---create-a-user-directory |
24 |
| -def create_pre_spawn_hook(base_path): |
25 |
| - def pre_spawn_hook(spawner): |
| 21 | + def start(self): |
| 22 | + # set the image limits |
| 23 | + super().set_limits() |
26 | 24 |
|
27 |
| - # create user directory on the host if it does not exist |
28 |
| - username = spawner.user.name |
29 |
| - imagename = spawner.user_options.get("image") |
| 25 | + # escape the image name |
| 26 | + username = self.user.name |
| 27 | + imagename = self.user_options.get("image") |
30 | 28 | imagename_escaped = imagename.replace(":", "-").replace("/", "-")
|
31 | 29 |
|
32 |
| - volume_path = os.path.join(base_path, username, imagename_escaped) |
| 30 | + # create the user directory on the host if it does not exist |
| 31 | + volume_path = os.path.join(self.base_path, username, imagename_escaped) |
33 | 32 | os.makedirs(volume_path, exist_ok=True)
|
34 | 33 |
|
35 | 34 | # the escaped image name is used to create a new folder in the user home directory
|
36 |
| - spawner.host_homedir_format_string = f"{base_path}/{username}" |
| 35 | + self.host_homedir_format_string = f"{self.base_path}/{username}" |
37 | 36 | # pass the image name to the Docker container
|
38 |
| - spawner.environment = {"USER_IMAGE": imagename_escaped} |
39 |
| - |
40 |
| - # set the image limits |
41 |
| - image = client.images.get(imagename) |
42 |
| - mem_limit = image.labels.get("plasmabio.mem_limit", None) |
43 |
| - cpu_limit = image.labels.get("plasmabio.cpu_limit", None) |
44 |
| - spawner.mem_limit = mem_limit or spawner.mem_limit |
45 |
| - spawner.cpu_limit = float(cpu_limit) if cpu_limit else spawner.cpu_limit |
46 |
| - spawner.extra_host_config = { |
47 |
| - "cpu_period": CPU_PERIOD, |
48 |
| - "cpu_quota": int(float(CPU_PERIOD) * spawner.cpu_limit), |
| 37 | + self.environment = {"USER_IMAGE": imagename_escaped} |
| 38 | + |
| 39 | + # mount volumes |
| 40 | + self.volumes = { |
| 41 | + os.path.join( |
| 42 | + os.path.dirname(__file__), "entrypoint", "entrypoint.sh" |
| 43 | + ): "/usr/local/bin/repo2docker-entrypoint", |
| 44 | + self.shared_data_path: {"bind": "/srv/data", "mode": "ro"}, |
49 | 45 | }
|
50 | 46 |
|
51 |
| - return pre_spawn_hook |
52 |
| - |
53 |
| - |
54 |
| -def options_form(spawner): |
55 |
| - """ |
56 |
| - Override the default form to handle the case when there is only |
57 |
| - one image. |
58 |
| - """ |
59 |
| - images = spawner.image_whitelist(spawner) |
60 |
| - option_t = '<option value="{image}" {selected}>{image}</option>' |
61 |
| - options = [ |
62 |
| - option_t.format( |
63 |
| - image=image, selected="selected" if image == spawner.image else "" |
64 |
| - ) |
65 |
| - for image in images |
66 |
| - ] |
67 |
| - return """ |
68 |
| - <label for="image">Select an image:</label> |
69 |
| - <select class="form-control" name="image" required autofocus> |
70 |
| - {options} |
71 |
| - </select> |
72 |
| - """.format( |
73 |
| - options=options |
74 |
| - ) |
75 |
| - |
76 |
| - |
77 |
| -def image_whitelist(spawner): |
78 |
| - """ |
79 |
| - Retrieve the list of available images |
80 |
| - """ |
81 |
| - images = list_images() |
82 |
| - return {image["image_name"]: image["image_name"] for image in images} |
| 47 | + return super().start() |
83 | 48 |
|
84 | 49 |
|
85 |
| -@hookimpl |
| 50 | +@hookimpl(trylast=True) |
86 | 51 | def tljh_custom_jupyterhub_config(c):
|
87 | 52 | # hub
|
88 |
| - c.JupyterHub.hub_ip = public_ips()[0] |
89 | 53 | c.JupyterHub.cleanup_servers = False
|
90 | 54 | c.JupyterHub.authenticator_class = PAMAuthenticator
|
91 |
| - c.JupyterHub.spawner_class = SystemUserSpawner |
| 55 | + c.JupyterHub.spawner_class = PlasmaBioSpawner |
92 | 56 | c.JupyterHub.allow_named_servers = True
|
93 | 57 | c.JupyterHub.named_server_limit_per_user = 2
|
| 58 | + c.JupyterHub.template_paths.insert( |
| 59 | + 0, os.path.join(os.path.dirname(__file__), "templates") |
| 60 | + ) |
94 | 61 |
|
95 | 62 | # spawner
|
| 63 | + # update name template for named servers |
| 64 | + c.PlasmaBioSpawner.name_template = "{prefix}-{username}-{servername}" |
96 | 65 | # increase the timeout to be able to pull larger Docker images
|
97 |
| - c.SystemUserSpawner.start_timeout = 120 |
98 |
| - c.SystemUserSpawner.pull_policy = "Never" |
99 |
| - c.SystemUserSpawner.name_template = "{prefix}-{username}-{servername}" |
100 |
| - c.SystemUserSpawner.default_url = "/lab" |
| 66 | + c.PlasmaBioSpawner.start_timeout = 120 |
| 67 | + c.PlasmaBioSpawner.pull_policy = "Never" |
| 68 | + c.PlasmaBioSpawner.remove = True |
| 69 | + c.PlasmaBioSpawner.default_url = "/lab" |
101 | 70 | # TODO: change back to jupyterhub-singleuser
|
102 |
| - c.SystemUserSpawner.cmd = ["/srv/conda/envs/notebook/bin/jupyterhub-singleuser"] |
103 |
| - c.SystemUserSpawner.volumes = { |
104 |
| - os.path.join( |
105 |
| - os.path.dirname(__file__), "entrypoint", "entrypoint.sh" |
106 |
| - ): "/usr/local/bin/repo2docker-entrypoint", |
107 |
| - SHARED_DATA_PATH: {"bind": "/srv/data", "mode": "ro"}, |
108 |
| - } |
109 |
| - |
| 71 | + c.PlasmaBioSpawner.cmd = ["/srv/conda/envs/notebook/bin/jupyterhub-singleuser"] |
110 | 72 | # set the default cpu and memory limits
|
111 |
| - c.SystemUserSpawner.mem_limit = DEFAULT_MEMORY_LIMIT |
112 |
| - c.SystemUserSpawner.cpu_limit = float(DEFAULT_CPU_LIMIT) |
113 |
| - c.SystemUserSpawner.args = ["--ResourceUseDisplay.track_cpu_percent=True"] |
114 |
| - |
115 |
| - c.SystemUserSpawner.pre_spawn_hook = create_pre_spawn_hook(VOLUMES_PATH) |
116 |
| - c.SystemUserSpawner.remove = True |
117 |
| - c.SystemUserSpawner.image_whitelist = image_whitelist |
118 |
| - c.SystemUserSpawner.options_form = options_form |
119 |
| - |
120 |
| - c.JupyterHub.template_paths = ( |
121 |
| - os.path.join(os.path.dirname(__file__), "templates"), |
122 |
| - ) |
123 |
| - |
124 |
| - # register the service to manage the user images |
125 |
| - c.JupyterHub.services.append( |
126 |
| - { |
127 |
| - "name": "environments", |
128 |
| - "admin": True, |
129 |
| - "url": "http://127.0.0.1:9988", |
130 |
| - "command": [sys.executable, "-m", "tljh_plasmabio.images"], |
131 |
| - } |
132 |
| - ) |
| 73 | + c.PlasmaBioSpawner.args = ["--ResourceUseDisplay.track_cpu_percent=True"] |
133 | 74 |
|
134 | 75 | # register Cockpit as a service if active
|
135 | 76 | if check_service_active("cockpit"):
|
136 | 77 | c.JupyterHub.services.append(
|
137 | 78 | {"name": "cockpit", "url": "http://0.0.0.0:9090",},
|
138 | 79 | )
|
139 |
| - |
140 |
| - |
141 |
| -@hookimpl |
142 |
| -def tljh_extra_hub_pip_packages(): |
143 |
| - return ["dockerspawner", "jupyter_client"] |
0 commit comments