1
1
import os
2
2
3
3
from aiodocker import Docker
4
+ from aiodocker .exceptions import DockerError
4
5
from dockerspawner import DockerSpawner
6
+ from docker .errors import APIError
7
+ from docker .types import Mount
5
8
from jinja2 import Environment , BaseLoader
6
9
from jupyter_client .localinterfaces import public_ips
7
10
from jupyterhub .handlers .static import CacheControlStaticFilesHandler
10
13
from tljh .configurer import load_config
11
14
from traitlets import Unicode
12
15
from traitlets .config import Configurable
16
+ from tornado import web
13
17
14
18
from .builder import BuildHandler
19
+ from .launcher import LaunchHandler
15
20
from .docker import list_images
16
21
from .images import ImagesHandler
17
22
from .logs import LogsHandler
23
+ from .token import TokenStore
24
+
18
25
19
26
# Default CPU period
20
27
# See: https://docs.docker.com/config/containers/resource_constraints/#limit-a-containers-access-to-memory#configure-the-default-cfs-scheduler
@@ -92,6 +99,22 @@ class SpawnerMixin(Configurable):
92
99
""" ,
93
100
)
94
101
102
+ rdmfs_base_path = Unicode (
103
+ config = True ,
104
+ help = """
105
+ A base path for RDMFS.
106
+ """ ,
107
+ )
108
+
109
+ token_store_path = Unicode (
110
+ config = True ,
111
+ help = """
112
+ A dbpath of token_store.
113
+ """ ,
114
+ )
115
+
116
+ extra_mounts = None
117
+
95
118
async def list_images (self ):
96
119
"""
97
120
Return the list of available images
@@ -155,19 +178,171 @@ async def set_limits(self):
155
178
}
156
179
)
157
180
181
+ async def set_extra_mounts (self ):
182
+ """
183
+ Prepare volume binds for GRDM
184
+ """
185
+ imagename = self .user_options .get ("image" )
186
+ async with Docker () as docker :
187
+ image = await docker .images .inspect (imagename )
188
+
189
+ provider_prefix = image ["ContainerConfig" ]["Labels" ].get (
190
+ "tljh_repo2docker.opt.provider" , None
191
+ )
192
+ if provider_prefix != 'rdm' :
193
+ return
194
+ await self ._set_rdm_mounts (image )
195
+
196
+ async def _set_rdm_mounts (self , image ):
197
+ repo = image ["ContainerConfig" ]["Labels" ].get (
198
+ "tljh_repo2docker.opt.repo" , None
199
+ )
200
+ token_store = TokenStore (dbpath = self .token_store_path )
201
+ repo_token = token_store .get (self .user , repo )
202
+ if repo_token is None :
203
+ raise web .HTTPError (
204
+ 400 ,
205
+ "No repo_token for: %s" % (repo ),
206
+ )
207
+ self .log .info ("Preparing RDMFS... " + 'name=' + repr (self .user .name ) + ', repo=' + repr (repo ))
208
+ mount_path = os .path .join (self .rdmfs_base_path , self .container_name )
209
+ if not os .path .exists (mount_path ):
210
+ os .makedirs (mount_path )
211
+ self .extra_mounts = [
212
+ dict (type = 'bind' , source = mount_path , target = '/mnt' , propagation = 'rshared' ),
213
+ ]
214
+ rdmfs_id = await self .get_rdmfs_object ()
215
+ if rdmfs_id is not None :
216
+ await self .remove_object_by_id (rdmfs_id )
217
+ rdmfs_id = await self .create_rdmfs_object ({
218
+ 'RDM_NODE_ID' : image ["ContainerConfig" ]["Labels" ].get (
219
+ "tljh_repo2docker.opt.user.rdm_node_id" , None
220
+ ),
221
+ 'RDM_API_URL' : image ["ContainerConfig" ]["Labels" ].get (
222
+ "tljh_repo2docker.opt.user.rdm_api_url" , None
223
+ ),
224
+ 'RDM_TOKEN' : repo_token ,
225
+ 'MOUNT_PATH' : '/mnt/rdm' ,
226
+ })
227
+ await self .start_object_by_id (rdmfs_id )
228
+
229
+ async def get_rdmfs_object (self ):
230
+ object_name = self .object_name + '_rdmfs'
231
+ self .log .debug ("Getting %s '%s'" , self .object_type , object_name )
232
+ try :
233
+ async with Docker () as docker :
234
+ obj = await docker .containers .get (object_name )
235
+ return obj .id
236
+ except DockerError as e :
237
+ if e .status == 404 :
238
+ self .log .info (
239
+ "%s '%s' is gone" , self .object_type .title (), object_name
240
+ )
241
+ elif e .status == 500 :
242
+ self .log .info (
243
+ "%s '%s' is on unhealthy node" ,
244
+ self .object_type .title (),
245
+ object_name ,
246
+ )
247
+ else :
248
+ raise
249
+ return None
250
+
251
+ async def create_rdmfs_object (self , env ):
252
+ host_config = dict (
253
+ Mounts = [
254
+ {
255
+ "Type" : "bind" ,
256
+ "Source" : m ['source' ],
257
+ "Target" : "/mnt" ,
258
+ "ReadOnly" : False ,
259
+ "BindOptions" : {
260
+ "Propagation" : "rshared" ,
261
+ },
262
+ }
263
+ for m in (self .extra_mounts or [])
264
+ ],
265
+ Privileged = True ,
266
+ )
267
+ create_kwargs = dict (
268
+ Image = 'gcr.io/nii-ap-ops/rdmfs:20211221' ,
269
+ Env = [f'{ k } ={ v } ' for k , v in env .items ()],
270
+ AutoRemove = True ,
271
+ HostConfig = host_config ,
272
+ )
273
+ async with Docker () as docker :
274
+ obj = await docker .containers .create (
275
+ create_kwargs ,
276
+ name = self .container_name + '_rdmfs' ,
277
+ )
278
+ return obj .id
279
+
280
+ async def start_object_by_id (self , object_id ):
281
+ async with Docker () as docker :
282
+ obj = await docker .containers .get (object_id )
283
+ await obj .start ()
284
+
285
+ async def remove_object_by_id (self , object_id ):
286
+ self .log .info ("Removing %s %s" , self .object_type , object_id )
287
+ try :
288
+ async with Docker () as docker :
289
+ obj = await docker .containers .get (object_id )
290
+ desc = await obj .show ()
291
+ if 'State' in desc and desc ['State' ]['Running' ]:
292
+ self .log .info ('terminating...' )
293
+ exec = await obj .exec (["/bin/sh" ,"-c" ,"xattr -w command terminate /mnt/rdm" ])
294
+ result = await exec .start (detach = True )
295
+ self .log .info ('terminated: {}' .format (result ))
296
+ else :
297
+ self .log .info ('deleting...' )
298
+ await obj .delete ()
299
+ except DockerError as e :
300
+ if e .status == 409 :
301
+ self .log .debug (
302
+ "Already removing %s: %s" , self .object_type , object_id
303
+ )
304
+ elif e .status == 404 :
305
+ self .log .debug (
306
+ "Already removed %s: %s" , self .object_type , object_id
307
+ )
308
+ else :
309
+ raise
310
+
158
311
159
312
class Repo2DockerSpawner (SpawnerMixin , DockerSpawner ):
160
313
"""
161
314
A custom spawner for using local Docker images built with tljh-repo2docker.
162
315
"""
163
316
317
+ @property
318
+ def mount_binds (self ):
319
+ base_mount_binds = super ().mount_binds .copy ()
320
+ if self .extra_mounts is None :
321
+ return base_mount_binds
322
+ base_mount_binds += [Mount (** m ) for m in self .extra_mounts ]
323
+ return base_mount_binds
324
+
164
325
async def start (self , * args , ** kwargs ):
165
326
await self .set_limits ()
327
+ await self .set_extra_mounts ()
166
328
return await super ().start (* args , ** kwargs )
167
329
330
+ async def stop (self , * args , ** kwargs ):
331
+ await super ().stop (* args , ** kwargs )
332
+ rdmfs_id = await self .get_rdmfs_object ()
333
+ if rdmfs_id is None :
334
+ return
335
+ await self .remove_object_by_id (rdmfs_id )
336
+
168
337
169
338
@hookimpl
170
339
def tljh_custom_jupyterhub_config (c ):
340
+ from binderhub .repoproviders import (
341
+ GitHubRepoProvider , GitRepoProvider , GitLabRepoProvider , GistRepoProvider ,
342
+ ZenodoProvider , FigshareProvider , HydroshareProvider , DataverseProvider ,
343
+ RDMProvider , WEKO3Provider ,
344
+ )
345
+
171
346
# hub
172
347
c .JupyterHub .hub_ip = public_ips ()[0 ]
173
348
c .JupyterHub .cleanup_servers = False
@@ -178,10 +353,14 @@ def tljh_custom_jupyterhub_config(c):
178
353
0 , os .path .join (os .path .dirname (__file__ ), "templates" )
179
354
)
180
355
356
+ token_store_path = '/opt/tljh/state/repo2docker.sqlite'
357
+
181
358
# spawner
182
359
c .DockerSpawner .cmd = ["jupyterhub-singleuser" ]
183
360
c .DockerSpawner .pull_policy = "Never"
184
361
c .DockerSpawner .remove = True
362
+ c .Repo2DockerSpawner .rdmfs_base_path = '/opt/tljh/repo2docker/volumes'
363
+ c .Repo2DockerSpawner .token_store_path = token_store_path
185
364
186
365
# fetch limits from the TLJH config
187
366
tljh_config = load_config ()
@@ -193,12 +372,47 @@ def tljh_custom_jupyterhub_config(c):
193
372
{"default_cpu_limit" : cpu_limit , "default_mem_limit" : mem_limit }
194
373
)
195
374
375
+ repo_providers = {
376
+ 'gh' : GitHubRepoProvider ,
377
+ 'gist' : GistRepoProvider ,
378
+ 'git' : GitRepoProvider ,
379
+ 'gl' : GitLabRepoProvider ,
380
+ 'zenodo' : ZenodoProvider ,
381
+ 'figshare' : FigshareProvider ,
382
+ 'hydroshare' : HydroshareProvider ,
383
+ 'dataverse' : DataverseProvider ,
384
+ 'rdm' : RDMProvider ,
385
+ 'weko3' : WEKO3Provider ,
386
+ }
387
+ c .RDMProvider .hosts = [
388
+ {
389
+ 'hostname' : ["https://osf.io/" ],
390
+ 'api' : "https://api.osf.io/v2/"
391
+ },
392
+ {
393
+ 'hostname' : ["https://bh.rdm.yzwlab.com/" ],
394
+ 'api' : "https://api.bh.rdm.yzwlab.com/v2/" ,
395
+ },
396
+ {
397
+ 'hostname' : ["https://rcos.rdm.nii.ac.jp" ],
398
+ 'api' : "https://api.rcos.rdm.nii.ac.jp/v2/" ,
399
+ },
400
+ ]
401
+
196
402
# register the handlers to manage the user images
197
403
c .JupyterHub .extra_handlers .extend (
198
404
[
199
405
(r"environments" , ImagesHandler ),
200
406
(r"api/environments" , BuildHandler ),
201
407
(r"api/environments/([^/]+)/logs" , LogsHandler ),
408
+ (
409
+ r"build/([^/]+)/[^/]+/[^/]+" ,
410
+ LaunchHandler ,
411
+ {
412
+ "repo_providers" : repo_providers ,
413
+ "token_store_path" : token_store_path ,
414
+ },
415
+ ),
202
416
(
203
417
r"environments-static/(.*)" ,
204
418
CacheControlStaticFilesHandler ,
@@ -210,4 +424,9 @@ def tljh_custom_jupyterhub_config(c):
210
424
211
425
@hookimpl
212
426
def tljh_extra_hub_pip_packages ():
213
- return ["dockerspawner~=0.11" , "jupyter_client~=6.1" , "aiodocker~=0.19" ]
427
+ return [
428
+ "dockerspawner~=12.1" ,
429
+ "jupyter_client~=6.1" ,
430
+ "aiodocker~=0.19" ,
431
+ "git+https://github.com/RCOSDP/CS-binderhub.git" ,
432
+ ]
0 commit comments