Skip to content

Commit 1b4f03d

Browse files
committed
Merge branches 'travis_jh9.0.1', 'run_command_deadlock', 'run_command_debug', 'keepvars_extra', 'spawner_detail', 'prologue_epilogue' and 'exec_prefix_template_separately' into dev
7 parents c6b34a9 + a23b3d4 + 9a00bee + 359fc35 + afbf5d3 + e956082 + bf48b92 commit 1b4f03d

File tree

4 files changed

+142
-16
lines changed

4 files changed

+142
-16
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ This package formerly included WrapSpawner and ProfilesSpawner, which provide me
2424

2525
## Batch Spawners
2626

27+
For information on the specific spawners, see [SPAWNERS.md](SPAWNERS.md).
28+
2729
### Overview
2830

2931
This file contains an abstraction layer for batch job queueing systems (`BatchSpawnerBase`), and implements
@@ -82,6 +84,15 @@ to run Jupyter notebooks on an academic supercomputer cluster.
8284
c.TorqueSpawner.state_exechost_exp = r'int-\1.mesabi.xyz.edu'
8385
```
8486

87+
### Security
88+
89+
Unless otherwise stated for a specific spawner, assume that spawners
90+
*do* evaluate shell environment for users and thus the [security
91+
requriemnts of JupyterHub security for untrusted
92+
users](https://jupyterhub.readthedocs.io/en/stable/reference/websecurity.html)
93+
are not fulfilled.
94+
95+
8596
## Provide different configurations of BatchSpawner
8697

8798
### Overview

SPAWNERS.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Notes on specific spawners
2+
3+
## `TorqueSpawner`
4+
5+
Maintainers:
6+
7+
8+
## `MoabSpawner`
9+
10+
Subclass of TorqueSpawner
11+
12+
Maintainers:
13+
14+
15+
## `SlurmSpawner`
16+
17+
Maintainers: @rkdarst
18+
19+
This spawner enforces the environment if `srun` is used, which is the
20+
default. If you *do* want user environment to be used, set
21+
`req_srun=''`.
22+
23+
24+
## `GridengineSpawner`
25+
26+
Maintainers:
27+
28+
29+
## `CondorSpawner`
30+
31+
Maintainers:
32+
33+
34+
## `LsfSpawner`
35+
36+
Maintainers:
37+
38+
39+
# Checklist for making spawners
40+
41+
- Does your spawner read shell environment before starting?
42+
43+
- Does your spawner send SIGTERM to the jupyterhub-singleuser process
44+
before SIGKILL? It should, so that the process can terminate
45+
gracefully. If you don't see the script end (e.g. you can add `echo
46+
"terminated gracefully"` to the end of your script and see it), you
47+
should check. PR #75 might help here, ask the poster to finalize
48+
it.
49+

batchspawner/batchspawner.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ def _req_homedir_default(self):
143143
def _req_keepvars_default(self):
144144
return ','.join(self.get_env().keys())
145145

146+
req_keepvars_extra = Unicode(
147+
help="Extra environment variables which should be configured, "
148+
"added to the defaults in keepvars, "
149+
"comma separated list.")
150+
151+
146152
batch_script = Unicode('', \
147153
help="Template for job submission script. Traits on this class named like req_xyz "
148154
"will be substituted in the template for {xyz} using string.Formatter. "
@@ -164,6 +170,8 @@ def get_req_subvars(self):
164170
subvars = {}
165171
for t in reqlist:
166172
subvars[t[4:]] = getattr(self, t)
173+
if subvars.get('keepvars_extra'):
174+
subvars['keepvars'] += ',' + subvars['keepvars_extra']
167175
return subvars
168176

169177
batch_submit_cmd = Unicode('', \
@@ -189,17 +197,20 @@ def run_command(self, cmd, input=None, env=None):
189197
# Apparently harmless
190198
pass
191199
proc.stdin.close()
192-
out = yield proc.stdout.read_until_close()
193-
eout = yield proc.stderr.read_until_close()
200+
out, eout = yield [proc.stdout.read_until_close(),
201+
proc.stderr.read_until_close()]
194202
proc.stdout.close()
195203
proc.stderr.close()
196204
eout = eout.decode().strip()
197205
try:
198206
err = yield proc.wait_for_exit()
199207
except CalledProcessError:
200208
self.log.error("Subprocess returned exitcode %s" % proc.returncode)
209+
self.log.error('Stdout:')
210+
self.log.error(out)
211+
self.log.error('Stderr:')
201212
self.log.error(eout)
202-
raise RuntimeError(eout)
213+
raise RuntimeError('{} exit status {}: {}'.format(cmd, proc.returncode, eout))
203214
if err != 0:
204215
return err # exit error?
205216
else:
@@ -215,8 +226,8 @@ def _get_batch_script(self, **subvars):
215226
@gen.coroutine
216227
def submit_batch_script(self):
217228
subvars = self.get_req_subvars()
218-
cmd = self.exec_prefix + ' ' + self.batch_submit_cmd
219-
cmd = format_template(cmd, **subvars)
229+
cmd = ' '.join((format_template(self.exec_prefix, **subvars),
230+
format_template(self.batch_submit_cmd, **subvars)))
220231
subvars['cmd'] = self.cmd_formatted_for_batch()
221232
if hasattr(self, 'user_options'):
222233
subvars.update(self.user_options)
@@ -246,8 +257,8 @@ def read_job_state(self):
246257
return self.job_status
247258
subvars = self.get_req_subvars()
248259
subvars['job_id'] = self.job_id
249-
cmd = self.exec_prefix + ' ' + self.batch_query_cmd
250-
cmd = format_template(cmd, **subvars)
260+
cmd = ' '.join((format_template(self.exec_prefix, **subvars),
261+
format_template(self.batch_query_cmd, **subvars)))
251262
self.log.debug('Spawner querying job: ' + cmd)
252263
try:
253264
out = yield self.run_command(cmd)
@@ -266,8 +277,8 @@ def read_job_state(self):
266277
def cancel_batch_job(self):
267278
subvars = self.get_req_subvars()
268279
subvars['job_id'] = self.job_id
269-
cmd = self.exec_prefix + ' ' + self.batch_cancel_cmd
270-
cmd = format_template(cmd, **subvars)
280+
cmd = ' '.join((format_template(self.exec_prefix, **subvars),
281+
format_template(self.batch_cancel_cmd, **subvars)))
271282
self.log.info('Cancelling job ' + self.job_id + ': ' + cmd)
272283
yield self.run_command(cmd)
273284

@@ -460,7 +471,9 @@ class TorqueSpawner(BatchSpawnerRegexStates):
460471
#PBS -v {keepvars}
461472
#PBS {options}
462473
474+
{prologue}
463475
{cmd}
476+
{epilogue}
464477
""").tag(config=True)
465478

466479
# outputs job id string
@@ -582,7 +595,9 @@ class GridengineSpawner(BatchSpawnerBase):
582595
#$ -v {keepvars}
583596
#$ {options}
584597
598+
{prologue}
585599
{cmd}
600+
{epilogue}
586601
""").tag(config=True)
587602

588603
# outputs job id string
@@ -668,7 +683,9 @@ class LsfSpawner(BatchSpawnerBase):
668683
#BSUB -o {homedir}/.jupyterhub.lsf.out
669684
#BSUB -e {homedir}/.jupyterhub.lsf.err
670685
686+
{prologue}
671687
{cmd}
688+
{epilogue}
672689
''').tag(config=True)
673690

674691

batchspawner/tests/test_spawners.py

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -279,9 +279,11 @@ def test_torque(db, io_loop):
279279
'req_nprocs': '5',
280280
'req_memory': '5678',
281281
'req_options': 'some_option_asdf',
282+
'req_prologue': 'PROLOGUE',
283+
'req_epilogue': 'EPILOGUE',
282284
}
283285
batch_script_re_list = [
284-
re.compile(r'singleuser_command'),
286+
re.compile(r'^PROLOGUE.*^singleuser_command.*^EPILOGUE', re.S|re.M),
285287
re.compile(r'mem=5678'),
286288
re.compile(r'ppn=5'),
287289
re.compile(r'^#PBS some_option_asdf', re.M),
@@ -305,9 +307,11 @@ def test_moab(db, io_loop):
305307
'req_nprocs': '5',
306308
'req_memory': '5678',
307309
'req_options': 'some_option_asdf',
310+
'req_prologue': 'PROLOGUE',
311+
'req_epilogue': 'EPILOGUE',
308312
}
309313
batch_script_re_list = [
310-
re.compile(r'singleuser_command'),
314+
re.compile(r'^PROLOGUE.*^singleuser_command.*^EPILOGUE', re.S|re.M),
311315
re.compile(r'mem=5678'),
312316
re.compile(r'ppn=5'),
313317
re.compile(r'^#PBS some_option_asdf', re.M),
@@ -332,23 +336,41 @@ def test_slurm(db, io_loop):
332336
'req_nprocs': '5',
333337
'req_memory': '5678',
334338
'req_options': 'some_option_asdf',
339+
'req_prologue': 'PROLOGUE',
340+
'req_epilogue': 'EPILOGUE',
335341
}
336342
batch_script_re_list = [
337-
re.compile(r'srun .* singleuser_command', re.X|re.M),
343+
re.compile(r'PROLOGUE.*srun singleuser_command.*EPILOGUE', re.S),
338344
re.compile(r'^#SBATCH \s+ --cpus-per-task=5', re.X|re.M),
339345
re.compile(r'^#SBATCH \s+ --time=3-05:10:10', re.X|re.M),
340346
re.compile(r'^#SBATCH \s+ some_option_asdf', re.X|re.M),
341347
]
342-
script = [
348+
from .. import SlurmSpawner
349+
run_spawner_script(db, io_loop, SlurmSpawner, normal_slurm_script,
350+
batch_script_re_list=batch_script_re_list,
351+
spawner_kwargs=spawner_kwargs)
352+
# We tend to use slurm as our typical example job. These allow quick
353+
# Slurm examples.
354+
normal_slurm_script = [
343355
(re.compile(r'sudo.*sbatch'), str(testjob)),
344356
(re.compile(r'sudo.*squeue'), 'PENDING '), # pending
345357
(re.compile(r'sudo.*squeue'), 'RUNNING '+testhost), # running
346358
(re.compile(r'sudo.*squeue'), 'RUNNING '+testhost),
347359
(re.compile(r'sudo.*scancel'), 'STOP'),
348360
(re.compile(r'sudo.*squeue'), ''),
349361
]
350-
from .. import SlurmSpawner
351-
run_spawner_script(db, io_loop, SlurmSpawner, script,
362+
from .. import SlurmSpawner
363+
def run_typical_slurm_spawner(db, io_loop,
364+
spawner=SlurmSpawner,
365+
script=normal_slurm_script,
366+
batch_script_re_list=None,
367+
spawner_kwargs={}):
368+
"""Run a full slurm job with default (overrideable) parameters.
369+
370+
This is useful, for example, for changing options and testing effect
371+
of batch scripts.
372+
"""
373+
return run_spawner_script(db, io_loop, spawner, script,
352374
batch_script_re_list=batch_script_re_list,
353375
spawner_kwargs=spawner_kwargs)
354376

@@ -407,9 +429,11 @@ def test_lfs(db, io_loop):
407429
'req_memory': '5678',
408430
'req_options': 'some_option_asdf',
409431
'req_queue': 'some_queue',
432+
'req_prologue': 'PROLOGUE',
433+
'req_epilogue': 'EPILOGUE',
410434
}
411435
batch_script_re_list = [
412-
re.compile(r'^singleuser_command', re.M),
436+
re.compile(r'^PROLOGUE.*^singleuser_command.*^EPILOGUE', re.S|re.M),
413437
re.compile(r'#BSUB\s+-q\s+some_queue', re.M),
414438
]
415439
script = [
@@ -424,3 +448,28 @@ def test_lfs(db, io_loop):
424448
run_spawner_script(db, io_loop, LsfSpawner, script,
425449
batch_script_re_list=batch_script_re_list,
426450
spawner_kwargs=spawner_kwargs)
451+
452+
453+
def test_keepvars(db, io_loop):
454+
# req_keepvars
455+
spawner_kwargs = {
456+
'req_keepvars': 'ABCDE',
457+
}
458+
batch_script_re_list = [
459+
re.compile(r'--export=ABCDE', re.X|re.M),
460+
]
461+
run_typical_slurm_spawner(db, io_loop,
462+
spawner_kwargs=spawner_kwargs,
463+
batch_script_re_list=batch_script_re_list)
464+
465+
# req_keepvars AND req_keepvars together
466+
spawner_kwargs = {
467+
'req_keepvars': 'ABCDE',
468+
'req_keepvars_extra': 'XYZ',
469+
}
470+
batch_script_re_list = [
471+
re.compile(r'--export=ABCDE,XYZ', re.X|re.M),
472+
]
473+
run_typical_slurm_spawner(db, io_loop,
474+
spawner_kwargs=spawner_kwargs,
475+
batch_script_re_list=batch_script_re_list)

0 commit comments

Comments
 (0)