20
20
21
21
import xml .etree .ElementTree as ET
22
22
23
+ from jinja2 import Template
24
+
23
25
from tornado import gen
24
26
from tornado .process import Subprocess
25
27
from subprocess import CalledProcessError
34
36
from jupyterhub .spawner import set_user_setuid
35
37
import jupyterhub
36
38
37
- @gen .coroutine
38
- def run_command (cmd , input = None , env = None ):
39
- proc = Subprocess (cmd , shell = True , env = env , stdin = Subprocess .STREAM , stdout = Subprocess .STREAM ,stderr = Subprocess .STREAM )
40
- inbytes = None
41
- if input :
42
- inbytes = input .encode ()
43
- try :
44
- yield proc .stdin .write (inbytes )
45
- except StreamClosedError as exp :
46
- # Apparently harmless
47
- pass
48
- proc .stdin .close ()
49
- out = yield proc .stdout .read_until_close ()
50
- eout = yield proc .stderr .read_until_close ()
51
- proc .stdout .close ()
52
- proc .stderr .close ()
53
- eout = eout .decode ().strip ()
54
- try :
55
- err = yield proc .wait_for_exit ()
56
- except CalledProcessError :
57
- #self.log.error("Subprocess returned exitcode %s" % proc.returncode)
58
- #self.log.error(eout)
59
- raise RuntimeError (eout )
60
- if err != 0 :
61
- return err # exit error?
62
- else :
63
- out = out .decode ().strip ()
64
- return out
39
+
40
+ def format_template (template , * args , ** kwargs ):
41
+ """Format a template, either using jinja2 or str.format().
42
+
43
+ Use jinja2 if the template is a jinja2.Template, or contains '{{' or
44
+ '{%'. Otherwise, use str.format() for backwards compatability with
45
+ old scripts (but you can't mix them).
46
+ """
47
+ if isinstance (template , Template ):
48
+ return template .render (* args , ** kwargs )
49
+ elif '{{' in template or '{%' in template :
50
+ return Template (template ).render (* args , ** kwargs )
51
+ return template .format (* args , ** kwargs )
52
+
65
53
66
54
class BatchSpawnerBase (Spawner ):
67
55
"""Base class for spawners using resource manager batch job submission mechanisms
@@ -90,6 +78,10 @@ class BatchSpawnerBase(Spawner):
90
78
# override default server ip since batch jobs normally running remotely
91
79
ip = Unicode ("0.0.0.0" , help = "Address for singleuser server to listen at" ).tag (config = True )
92
80
81
+ exec_prefix = Unicode ('sudo -E -u {username}' , \
82
+ help = "Standard executon prefix (e.g. the default sudo -E -u {username})"
83
+ ).tag (config = True )
84
+
93
85
# all these req_foo traits will be available as substvars for templated strings
94
86
req_queue = Unicode ('' , \
95
87
help = "Queue name to submit job to resource manager"
@@ -127,6 +119,14 @@ class BatchSpawnerBase(Spawner):
127
119
help = "Other options to include into job submission script"
128
120
).tag (config = True )
129
121
122
+ req_prologue = Unicode ('' , \
123
+ help = "Script to run before single user server starts."
124
+ ).tag (config = True )
125
+
126
+ req_epilogue = Unicode ('' , \
127
+ help = "Script to run after single user server ends."
128
+ ).tag (config = True )
129
+
130
130
req_username = Unicode ()
131
131
@default ('req_username' )
132
132
def _req_username_default (self ):
@@ -177,17 +177,53 @@ def parse_job_id(self, output):
177
177
def cmd_formatted_for_batch (self ):
178
178
return ' ' .join (self .cmd + self .get_args ())
179
179
180
+ @gen .coroutine
181
+ def run_command (self , cmd , input = None , env = None ):
182
+ proc = Subprocess (cmd , shell = True , env = env , stdin = Subprocess .STREAM , stdout = Subprocess .STREAM ,stderr = Subprocess .STREAM )
183
+ inbytes = None
184
+ if input :
185
+ inbytes = input .encode ()
186
+ try :
187
+ yield proc .stdin .write (inbytes )
188
+ except StreamClosedError as exp :
189
+ # Apparently harmless
190
+ pass
191
+ proc .stdin .close ()
192
+ out = yield proc .stdout .read_until_close ()
193
+ eout = yield proc .stderr .read_until_close ()
194
+ proc .stdout .close ()
195
+ proc .stderr .close ()
196
+ eout = eout .decode ().strip ()
197
+ try :
198
+ err = yield proc .wait_for_exit ()
199
+ except CalledProcessError :
200
+ self .log .error ("Subprocess returned exitcode %s" % proc .returncode )
201
+ self .log .error (eout )
202
+ raise RuntimeError (eout )
203
+ if err != 0 :
204
+ return err # exit error?
205
+ else :
206
+ out = out .decode ().strip ()
207
+ return out
208
+
209
+ @gen .coroutine
210
+ def _get_batch_script (self , ** subvars ):
211
+ """Format batch script from vars"""
212
+ # Colud be overridden by subclasses, but mainly useful for testing
213
+ return format_template (self .batch_script , ** subvars )
214
+
180
215
@gen .coroutine
181
216
def submit_batch_script (self ):
182
217
subvars = self .get_req_subvars ()
183
- cmd = self .batch_submit_cmd .format (** subvars )
218
+ cmd = self .exec_prefix + ' ' + self .batch_submit_cmd
219
+ cmd = format_template (cmd , ** subvars )
184
220
subvars ['cmd' ] = self .cmd_formatted_for_batch ()
185
221
if hasattr (self , 'user_options' ):
186
222
subvars .update (self .user_options )
187
- script = self .batch_script . format (** subvars )
223
+ script = yield self ._get_batch_script (** subvars )
188
224
self .log .info ('Spawner submitting job using ' + cmd )
189
225
self .log .info ('Spawner submitted script:\n ' + script )
190
- out = yield run_command (cmd , input = script , env = self .get_env ())
226
+ out = yield self . run_command (cmd , input = script , env = self .get_env ())
191
227
try :
192
228
self .log .info ('Job submitted. cmd: ' + cmd + ' output: ' + out )
193
229
self .job_id = self .parse_job_id (out )
@@ -210,10 +246,11 @@ def read_job_state(self):
210
246
return self .job_status
211
247
subvars = self .get_req_subvars ()
212
248
subvars ['job_id' ] = self .job_id
213
- cmd = self .batch_query_cmd .format (** subvars )
249
+ cmd = self .exec_prefix + ' ' + self .batch_query_cmd
250
+ cmd = format_template (cmd , ** subvars )
214
251
self .log .debug ('Spawner querying job: ' + cmd )
215
252
try :
216
- out = yield run_command (cmd )
253
+ out = yield self . run_command (cmd )
217
254
self .job_status = out
218
255
except Exception as e :
219
256
self .log .error ('Error querying job ' + self .job_id )
@@ -229,9 +266,10 @@ def read_job_state(self):
229
266
def cancel_batch_job (self ):
230
267
subvars = self .get_req_subvars ()
231
268
subvars ['job_id' ] = self .job_id
232
- cmd = self .batch_cancel_cmd .format (** subvars )
269
+ cmd = self .exec_prefix + ' ' + self .batch_cancel_cmd
270
+ cmd = format_template (cmd , ** subvars )
233
271
self .log .info ('Cancelling job ' + self .job_id + ': ' + cmd )
234
- yield run_command (cmd )
272
+ yield self . run_command (cmd )
235
273
236
274
def load_state (self , state ):
237
275
"""load job_id from state"""
@@ -423,21 +461,21 @@ class TorqueSpawner(BatchSpawnerRegexStates):
423
461
""" ).tag (config = True )
424
462
425
463
# outputs job id string
426
- batch_submit_cmd = Unicode ('sudo -E -u {username} qsub' ).tag (config = True )
464
+ batch_submit_cmd = Unicode ('qsub' ).tag (config = True )
427
465
# outputs job data XML string
428
- batch_query_cmd = Unicode ('sudo -E -u {username} qstat -x {job_id}' ).tag (config = True )
429
- batch_cancel_cmd = Unicode ('sudo -E -u {username} qdel {job_id}' ).tag (config = True )
466
+ batch_query_cmd = Unicode ('qstat -x {job_id}' ).tag (config = True )
467
+ batch_cancel_cmd = Unicode ('qdel {job_id}' ).tag (config = True )
430
468
# search XML string for job_state - [QH] = pending, R = running, [CE] = done
431
469
state_pending_re = Unicode (r'<job_state>[QH]</job_state>' ).tag (config = True )
432
470
state_running_re = Unicode (r'<job_state>R</job_state>' ).tag (config = True )
433
471
state_exechost_re = Unicode (r'<exec_host>((?:[\w_-]+\.?)+)/\d+' ).tag (config = True )
434
472
435
473
class MoabSpawner (TorqueSpawner ):
436
474
# outputs job id string
437
- batch_submit_cmd = Unicode ('sudo -E -u {username} msub' ).tag (config = True )
475
+ batch_submit_cmd = Unicode ('msub' ).tag (config = True )
438
476
# outputs job data XML string
439
- batch_query_cmd = Unicode ('sudo -E -u {username} mdiag -j {job_id} --xml' ).tag (config = True )
440
- batch_cancel_cmd = Unicode ('sudo -E -u {username} mjobctl -c {job_id}' ).tag (config = True )
477
+ batch_query_cmd = Unicode ('mdiag -j {job_id} --xml' ).tag (config = True )
478
+ batch_cancel_cmd = Unicode ('mjobctl -c {job_id}' ).tag (config = True )
441
479
state_pending_re = Unicode (r'State="Idle"' ).tag (config = True )
442
480
state_running_re = Unicode (r'State="Running"' ).tag (config = True )
443
481
state_exechost_re = Unicode (r'AllocNodeList="([^\r\n\t\f :"]*)' ).tag (config = True )
@@ -476,24 +514,29 @@ class SlurmSpawner(UserEnvMixin,BatchSpawnerRegexStates):
476
514
).tag (config = True )
477
515
478
516
batch_script = Unicode ("""#!/bin/bash
479
- #SBATCH --partition={partition}
480
- #SBATCH --time={runtime}
481
- #SBATCH --output={homedir}/jupyterhub_slurmspawner_%j.log
517
+ #SBATCH --output={{homedir}}/jupyterhub_slurmspawner_%j.log
482
518
#SBATCH --job-name=spawner-jupyterhub
483
- #SBATCH --workdir={homedir}
484
- #SBATCH --mem={memory}
485
- #SBATCH --export={keepvars}
519
+ #SBATCH --workdir={{homedir}}
520
+ #SBATCH --export={{keepvars}}
486
521
#SBATCH --get-user-env=L
487
- #SBATCH {options}
488
-
522
+ {% if partition %}#SBATCH --partition={{partition}}
523
+ {% endif %}{% if runtime %}#SBATCH --time={{runtime}}
524
+ {% endif %}{% if memory %}#SBATCH --mem={{memory}}
525
+ {% endif %}{% if nprocs %}#SBATCH --cpus-per-task={{nprocs}}
526
+ {% endif %}{% if options %}#SBATCH {{options}}{% endif %}
527
+
528
+ trap 'echo SIGTERM received' TERM
529
+ {{prologue}}
489
530
which jupyterhub-singleuser
490
- {cmd}
531
+ srun {{cmd}}
532
+ echo "jupyterhub-singleuser ended gracefully"
533
+ {{epilogue}}
491
534
""" ).tag (config = True )
492
535
# outputs line like "Submitted batch job 209"
493
- batch_submit_cmd = Unicode ('sudo -E -u {username} sbatch --parsable' ).tag (config = True )
536
+ batch_submit_cmd = Unicode ('sbatch --parsable' ).tag (config = True )
494
537
# outputs status and exec node like "RUNNING hostname"
495
- batch_query_cmd = Unicode ("sudo -E -u {username} squeue -h -j {job_id} -o '%T %B'" ).tag (config = True ) #
496
- batch_cancel_cmd = Unicode ('sudo -E -u {username} scancel {job_id}' ).tag (config = True )
538
+ batch_query_cmd = Unicode ("squeue -h -j {job_id} -o '%T %B'" ).tag (config = True ) #
539
+ batch_cancel_cmd = Unicode ('scancel {job_id}' ).tag (config = True )
497
540
# use long-form states: PENDING, CONFIGURING = pending
498
541
# RUNNING, COMPLETING = running
499
542
state_pending_re = Unicode (r'^(?:PENDING|CONFIGURING)' ).tag (config = True )
@@ -535,10 +578,10 @@ class GridengineSpawner(BatchSpawnerBase):
535
578
""" ).tag (config = True )
536
579
537
580
# outputs job id string
538
- batch_submit_cmd = Unicode ('sudo -E -u {username} qsub' ).tag (config = True )
581
+ batch_submit_cmd = Unicode ('qsub' ).tag (config = True )
539
582
# outputs job data XML string
540
- batch_query_cmd = Unicode ('sudo -E -u {username} qstat -xml' ).tag (config = True )
541
- batch_cancel_cmd = Unicode ('sudo -E -u {username} qdel {job_id}' ).tag (config = True )
583
+ batch_query_cmd = Unicode ('qstat -xml' ).tag (config = True )
584
+ batch_cancel_cmd = Unicode ('qdel {job_id}' ).tag (config = True )
542
585
543
586
def parse_job_id (self , output ):
544
587
return output .split (' ' )[2 ]
@@ -585,10 +628,10 @@ class CondorSpawner(UserEnvMixin,BatchSpawnerRegexStates):
585
628
""" ).tag (config = True )
586
629
587
630
# outputs job id string
588
- batch_submit_cmd = Unicode ('sudo -E -u {username} condor_submit' ).tag (config = True )
631
+ batch_submit_cmd = Unicode ('condor_submit' ).tag (config = True )
589
632
# outputs job data XML string
590
633
batch_query_cmd = Unicode ('condor_q {job_id} -format "%s, " JobStatus -format "%s" RemoteHost -format "\n " True' ).tag (config = True )
591
- batch_cancel_cmd = Unicode ('sudo -E -u {username} condor_rm {job_id}' ).tag (config = True )
634
+ batch_cancel_cmd = Unicode ('condor_rm {job_id}' ).tag (config = True )
592
635
# job status: 1 = pending, 2 = running
593
636
state_pending_re = Unicode (r'^1,' ).tag (config = True )
594
637
state_running_re = Unicode (r'^2,' ).tag (config = True )
@@ -610,20 +653,20 @@ class LsfSpawner(BatchSpawnerBase):
610
653
'''A Spawner that uses IBM's Platform Load Sharing Facility (LSF) to launch notebooks.'''
611
654
612
655
batch_script = Unicode ('''#!/bin/sh
613
- #BSUB -R "select[type==any]" # Allow spawning on non-uniform hardware
614
- #BSUB -R "span[hosts=1]" # Only spawn job on one server
615
- #BSUB -q {queue}
616
- #BSUB -J spawner-jupyterhub
617
- #BSUB -o {homedir}/.jupyterhub.lsf.out
618
- #BSUB -e {homedir}/.jupyterhub.lsf.err
656
+ #BSUB -R "select[type==any]" # Allow spawning on non-uniform hardware
657
+ #BSUB -R "span[hosts=1]" # Only spawn job on one server
658
+ #BSUB -q {queue}
659
+ #BSUB -J spawner-jupyterhub
660
+ #BSUB -o {homedir}/.jupyterhub.lsf.out
661
+ #BSUB -e {homedir}/.jupyterhub.lsf.err
619
662
620
- {cmd}
621
- ''' ).tag (config = True )
663
+ {cmd}
664
+ ''' ).tag (config = True )
622
665
623
666
624
- batch_submit_cmd = Unicode ('sudo -E -u {username} bsub' ).tag (config = True )
625
- batch_query_cmd = Unicode ('sudo -E -u {username} bjobs -a -noheader -o "STAT EXEC_HOST" {job_id}' ).tag (config = True )
626
- batch_cancel_cmd = Unicode ('sudo -E -u {username} bkill {job_id}' ).tag (config = True )
667
+ batch_submit_cmd = Unicode ('bsub' ).tag (config = True )
668
+ batch_query_cmd = Unicode ('bjobs -a -noheader -o "STAT EXEC_HOST" {job_id}' ).tag (config = True )
669
+ batch_cancel_cmd = Unicode ('bkill {job_id}' ).tag (config = True )
627
670
628
671
def get_env (self ):
629
672
env = super ().get_env ()
0 commit comments