From 5201449daf3471e5406ad34fe692be809a1bb0e5 Mon Sep 17 00:00:00 2001 From: Gwen Date: Fri, 15 Jun 2018 17:02:36 +0200 Subject: [PATCH 01/26] Add reservation option. --- batchspawner/batchspawner.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/batchspawner/batchspawner.py b/batchspawner/batchspawner.py index f8081f45..b0135999 100644 --- a/batchspawner/batchspawner.py +++ b/batchspawner/batchspawner.py @@ -513,6 +513,10 @@ class SlurmSpawner(UserEnvMixin,BatchSpawnerRegexStates): help="QoS name to submit job to resource manager" ).tag(config=True) + req_reservation = Unicode('', \ + help="Reservation name to submit to resource manager" + ).tag(config=True) + batch_script = Unicode("""#!/bin/bash #SBATCH --output={{homedir}}/jupyterhub_slurmspawner_%j.log #SBATCH --job-name=spawner-jupyterhub From 370af5bdc5c6b5a9a7666bdc89083b6f126ede00 Mon Sep 17 00:00:00 2001 From: Petraea Date: Fri, 15 Jun 2018 17:55:26 +0200 Subject: [PATCH 02/26] Fix spacing. --- batchspawner/batchspawner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/batchspawner/batchspawner.py b/batchspawner/batchspawner.py index b0135999..4c7c716a 100644 --- a/batchspawner/batchspawner.py +++ b/batchspawner/batchspawner.py @@ -513,7 +513,7 @@ class SlurmSpawner(UserEnvMixin,BatchSpawnerRegexStates): help="QoS name to submit job to resource manager" ).tag(config=True) - req_reservation = Unicode('', \ + req_reservation = Unicode('', \ help="Reservation name to submit to resource manager" ).tag(config=True) From bf48b92201355e0ef63e96cf23f3e2f56dc7b781 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Mon, 18 Jun 2018 23:27:05 +0300 Subject: [PATCH 03/26] Update exec_prefix handling: template this separate from batch command. - It would be possible to have exec_prefix use string formatting and the command itself use jinja2 formatting, and when combined the string formatting would fail. To handle this, template them separately and then concatenate. --- batchspawner/batchspawner.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/batchspawner/batchspawner.py b/batchspawner/batchspawner.py index f8081f45..9799e705 100644 --- a/batchspawner/batchspawner.py +++ b/batchspawner/batchspawner.py @@ -215,8 +215,8 @@ def _get_batch_script(self, **subvars): @gen.coroutine def submit_batch_script(self): subvars = self.get_req_subvars() - cmd = self.exec_prefix + ' ' + self.batch_submit_cmd - cmd = format_template(cmd, **subvars) + cmd = ' '.join((format_template(self.exec_prefix, **subvars), + format_template(self.batch_submit_cmd, **subvars))) subvars['cmd'] = self.cmd_formatted_for_batch() if hasattr(self, 'user_options'): subvars.update(self.user_options) @@ -246,8 +246,8 @@ def read_job_state(self): return self.job_status subvars = self.get_req_subvars() subvars['job_id'] = self.job_id - cmd = self.exec_prefix + ' ' + self.batch_query_cmd - cmd = format_template(cmd, **subvars) + cmd = ' '.join((format_template(self.exec_prefix, **subvars), + format_template(self.batch_query_cmd, **subvars))) self.log.debug('Spawner querying job: ' + cmd) try: out = yield self.run_command(cmd) @@ -266,8 +266,8 @@ def read_job_state(self): def cancel_batch_job(self): subvars = self.get_req_subvars() subvars['job_id'] = self.job_id - cmd = self.exec_prefix + ' ' + self.batch_cancel_cmd - cmd = format_template(cmd, **subvars) + cmd = ' '.join((format_template(self.exec_prefix, **subvars), + format_template(self.batch_cancel_cmd, **subvars))) self.log.info('Cancelling job ' + self.job_id + ': ' + cmd) yield self.run_command(cmd) From e956082cc4c2dbdfb9e6d70f4d51781d551fa393 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Wed, 20 Jun 2018 17:59:34 +0300 Subject: [PATCH 04/26] Add prologue and epilogue options to other spawners - These options first appeared in dff4482 for SlurmSpawner - Add tests for this option - Not currently implemented for CondorSpawner, since it uses a different method using `bash -c`. It could be added there, but someone with local knowledge should do that. --- batchspawner/batchspawner.py | 6 ++++++ batchspawner/tests/test_spawners.py | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/batchspawner/batchspawner.py b/batchspawner/batchspawner.py index ac2a68f9..85df391d 100644 --- a/batchspawner/batchspawner.py +++ b/batchspawner/batchspawner.py @@ -460,7 +460,9 @@ class TorqueSpawner(BatchSpawnerRegexStates): #PBS -v {keepvars} #PBS {options} +{prologue} {cmd} +{epilogue} """).tag(config=True) # outputs job id string @@ -582,7 +584,9 @@ class GridengineSpawner(BatchSpawnerBase): #$ -v {keepvars} #$ {options} +{prologue} {cmd} +{epilogue} """).tag(config=True) # outputs job id string @@ -668,7 +672,9 @@ class LsfSpawner(BatchSpawnerBase): #BSUB -o {homedir}/.jupyterhub.lsf.out #BSUB -e {homedir}/.jupyterhub.lsf.err +{prologue} {cmd} +{epilogue} ''').tag(config=True) diff --git a/batchspawner/tests/test_spawners.py b/batchspawner/tests/test_spawners.py index b45abc9b..085b4c80 100644 --- a/batchspawner/tests/test_spawners.py +++ b/batchspawner/tests/test_spawners.py @@ -279,9 +279,11 @@ def test_torque(db, io_loop): 'req_nprocs': '5', 'req_memory': '5678', 'req_options': 'some_option_asdf', + 'req_prologue': 'PROLOGUE', + 'req_epilogue': 'EPILOGUE', } batch_script_re_list = [ - re.compile(r'singleuser_command'), + re.compile(r'^PROLOGUE.*^singleuser_command.*^EPILOGUE', re.S|re.M), re.compile(r'mem=5678'), re.compile(r'ppn=5'), re.compile(r'^#PBS some_option_asdf', re.M), @@ -305,9 +307,11 @@ def test_moab(db, io_loop): 'req_nprocs': '5', 'req_memory': '5678', 'req_options': 'some_option_asdf', + 'req_prologue': 'PROLOGUE', + 'req_epilogue': 'EPILOGUE', } batch_script_re_list = [ - re.compile(r'singleuser_command'), + re.compile(r'^PROLOGUE.*^singleuser_command.*^EPILOGUE', re.S|re.M), re.compile(r'mem=5678'), re.compile(r'ppn=5'), re.compile(r'^#PBS some_option_asdf', re.M), @@ -332,9 +336,11 @@ def test_slurm(db, io_loop): 'req_nprocs': '5', 'req_memory': '5678', 'req_options': 'some_option_asdf', + 'req_prologue': 'PROLOGUE', + 'req_epilogue': 'EPILOGUE', } batch_script_re_list = [ - re.compile(r'srun .* singleuser_command', re.X|re.M), + re.compile(r'PROLOGUE.*srun singleuser_command.*EPILOGUE', re.S), re.compile(r'^#SBATCH \s+ --cpus-per-task=5', re.X|re.M), re.compile(r'^#SBATCH \s+ --time=3-05:10:10', re.X|re.M), re.compile(r'^#SBATCH \s+ some_option_asdf', re.X|re.M), @@ -407,9 +413,11 @@ def test_lfs(db, io_loop): 'req_memory': '5678', 'req_options': 'some_option_asdf', 'req_queue': 'some_queue', + 'req_prologue': 'PROLOGUE', + 'req_epilogue': 'EPILOGUE', } batch_script_re_list = [ - re.compile(r'^singleuser_command', re.M), + re.compile(r'^PROLOGUE.*^singleuser_command.*^EPILOGUE', re.S|re.M), re.compile(r'#BSUB\s+-q\s+some_queue', re.M), ] script = [ From afbf5d3269e2f096c9862be941193ede07a9135b Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Wed, 20 Jun 2018 17:37:02 +0300 Subject: [PATCH 05/26] Add SPAWNERS.md, a list of specific info on spawners --- README.md | 11 +++++++++++ SPAWNERS.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 SPAWNERS.md diff --git a/README.md b/README.md index 4205400a..2cb0cb57 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ This package formerly included WrapSpawner and ProfilesSpawner, which provide me ## Batch Spawners +For information on the specific spawners, see [SPAWNERS.md](SPAWNERS.md). + ### Overview 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. c.TorqueSpawner.state_exechost_exp = r'int-\1.mesabi.xyz.edu' ``` +### Security + +Unless otherwise stated for a specific spawner, assume that spawners +*do* evaluate shell environment for users and thus the [security +requriemnts of JupyterHub security for untrusted +users](https://jupyterhub.readthedocs.io/en/stable/reference/websecurity.html) +are not fulfilled. + + ## Provide different configurations of BatchSpawner ### Overview diff --git a/SPAWNERS.md b/SPAWNERS.md new file mode 100644 index 00000000..ec7fad09 --- /dev/null +++ b/SPAWNERS.md @@ -0,0 +1,49 @@ +# Notes on specific spawners + +## `TorqueSpawner` + +Maintainers: + + +## `MoabSpawner` + +Subclass of TorqueSpawner + +Maintainers: + + +## `SlurmSpawner` + +Maintainers: @rkdarst + +This spawner enforces the environment if `srun` is used, which is the +default. If you *do* want user environment to be used, set +`req_srun=''`. + + +## `GridengineSpawner` + +Maintainers: + + +## `CondorSpawner` + +Maintainers: + + +## `LsfSpawner` + +Maintainers: + + +# Checklist for making spawners + +- Does your spawner read shell environment before starting? + +- Does your spawner send SIGTERM to the jupyterhub-singleuser process + before SIGKILL? It should, so that the process can terminate + gracefully. If you don't see the script end (e.g. you can add `echo + "terminated gracefully"` to the end of your script and see it), you + should check. PR #75 might help here, ask the poster to finalize + it. + From 72ac758f6d2edf3cd4e5a83301a0ac310c898f31 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Wed, 20 Jun 2018 23:56:09 +0300 Subject: [PATCH 06/26] Add keepvars_extra to whitelist more environment. - Existing keepvars will whitelist environment variables, but is all-or-nothing: you can't append to existing required environemnt variables that JupyterHub requires. - This allows a comma separated list of the same format as req_keepvars that gets appended to keepvars, with one extra comma between them. --- batchspawner/batchspawner.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/batchspawner/batchspawner.py b/batchspawner/batchspawner.py index ac2a68f9..3dfcc584 100644 --- a/batchspawner/batchspawner.py +++ b/batchspawner/batchspawner.py @@ -143,6 +143,12 @@ def _req_homedir_default(self): def _req_keepvars_default(self): return ','.join(self.get_env().keys()) + req_keepvars_extra = Unicode( + help="Extra environment variables which should be configured, " + "added to the defaults in keepvars, " + "comma separated list.") + + batch_script = Unicode('', \ help="Template for job submission script. Traits on this class named like req_xyz " "will be substituted in the template for {xyz} using string.Formatter. " @@ -164,6 +170,8 @@ def get_req_subvars(self): subvars = {} for t in reqlist: subvars[t[4:]] = getattr(self, t) + if subvars.get('keepvars_extra'): + subvars['keepvars'] += ',' + subvars['keepvars_extra'] return subvars batch_submit_cmd = Unicode('', \ From 359fc35067ac5ea6de824f7d6e67d88a4fb68402 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Thu, 21 Jun 2018 16:48:25 +0300 Subject: [PATCH 07/26] Add tests for req_keepvars and req_keepvars_extra --- batchspawner/tests/test_spawners.py | 47 +++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/batchspawner/tests/test_spawners.py b/batchspawner/tests/test_spawners.py index b45abc9b..1ba13ff1 100644 --- a/batchspawner/tests/test_spawners.py +++ b/batchspawner/tests/test_spawners.py @@ -339,7 +339,13 @@ def test_slurm(db, io_loop): re.compile(r'^#SBATCH \s+ --time=3-05:10:10', re.X|re.M), re.compile(r'^#SBATCH \s+ some_option_asdf', re.X|re.M), ] - script = [ + from .. import SlurmSpawner + run_spawner_script(db, io_loop, SlurmSpawner, normal_slurm_script, + batch_script_re_list=batch_script_re_list, + spawner_kwargs=spawner_kwargs) +# We tend to use slurm as our typical example job. These allow quick +# Slurm examples. +normal_slurm_script = [ (re.compile(r'sudo.*sbatch'), str(testjob)), (re.compile(r'sudo.*squeue'), 'PENDING '), # pending (re.compile(r'sudo.*squeue'), 'RUNNING '+testhost), # running @@ -347,8 +353,18 @@ def test_slurm(db, io_loop): (re.compile(r'sudo.*scancel'), 'STOP'), (re.compile(r'sudo.*squeue'), ''), ] - from .. import SlurmSpawner - run_spawner_script(db, io_loop, SlurmSpawner, script, +from .. import SlurmSpawner +def run_typical_slurm_spawner(db, io_loop, + spawner=SlurmSpawner, + script=normal_slurm_script, + batch_script_re_list=None, + spawner_kwargs={}): + """Run a full slurm job with default (overrideable) parameters. + + This is useful, for example, for changing options and testing effect + of batch scripts. + """ + return run_spawner_script(db, io_loop, spawner, script, batch_script_re_list=batch_script_re_list, spawner_kwargs=spawner_kwargs) @@ -424,3 +440,28 @@ def test_lfs(db, io_loop): run_spawner_script(db, io_loop, LsfSpawner, script, batch_script_re_list=batch_script_re_list, spawner_kwargs=spawner_kwargs) + + +def test_keepvars(db, io_loop): + # req_keepvars + spawner_kwargs = { + 'req_keepvars': 'ABCDE', + } + batch_script_re_list = [ + re.compile(r'--export=ABCDE', re.X|re.M), + ] + run_typical_slurm_spawner(db, io_loop, + spawner_kwargs=spawner_kwargs, + batch_script_re_list=batch_script_re_list) + + # req_keepvars AND req_keepvars together + spawner_kwargs = { + 'req_keepvars': 'ABCDE', + 'req_keepvars_extra': 'XYZ', + } + batch_script_re_list = [ + re.compile(r'--export=ABCDE,XYZ', re.X|re.M), + ] + run_typical_slurm_spawner(db, io_loop, + spawner_kwargs=spawner_kwargs, + batch_script_re_list=batch_script_re_list) From 05a747ec9067e690a076829ed3f8097d120e8fef Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Thu, 5 Jul 2018 12:52:05 -0400 Subject: [PATCH 08/26] ENH (minor): bool() instead of True/False, import, a bit more consistency with spacing --- batchspawner/batchspawner.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/batchspawner/batchspawner.py b/batchspawner/batchspawner.py index ac2a68f9..af5a35ea 100644 --- a/batchspawner/batchspawner.py +++ b/batchspawner/batchspawner.py @@ -17,6 +17,7 @@ """ import pwd import os +import re import xml.etree.ElementTree as ET @@ -46,7 +47,7 @@ def format_template(template, *args, **kwargs): """ if isinstance(template, Template): return template.render(*args, **kwargs) - elif '{{' in template or '{%' in template: + elif '{{' in template or '{%' in template: return Template(template).render(*args, **kwargs) return template.format(*args, **kwargs) @@ -334,7 +335,7 @@ def start(self): if self.user and self.user.server and self.user.server.port: self.port = self.user.server.port self.db.commit() - elif (jupyterhub.version_info < (0,7) and not self.user.server.port) or \ + elif (jupyterhub.version_info < (0,7) and not self.user.server.port) or \ (jupyterhub.version_info >= (0,7) and not self.port): self.port = random_port() self.db.commit() @@ -356,8 +357,8 @@ def start(self): else: self.log.warn('Job ' + self.job_id + ' neither pending nor running.\n' + self.job_status) - raise RuntimeError('The Jupyter batch job has disappeared ' - ' while pending in the queue or died immediately ' + raise RuntimeError('The Jupyter batch job has disappeared' + ' while pending in the queue or died immediately' ' after starting.') yield gen.sleep(self.startup_poll_interval) @@ -394,7 +395,6 @@ def stop(self, now=False): self.job_id, self.current_ip, self.port) ) -import re class BatchSpawnerRegexStates(BatchSpawnerBase): """Subclass of BatchSpawnerBase that uses config-supplied regular expressions @@ -429,15 +429,11 @@ class BatchSpawnerRegexStates(BatchSpawnerBase): def state_ispending(self): assert self.state_pending_re, "Misconfigured: define state_running_re" - if self.job_status and re.search(self.state_pending_re, self.job_status): - return True - else: return False + return bool(self.job_status and re.search(self.state_pending_re, self.job_status)) def state_isrunning(self): assert self.state_running_re, "Misconfigured: define state_running_re" - if self.job_status and re.search(self.state_running_re, self.job_status): - return True - else: return False + return bool(self.job_status and re.search(self.state_running_re, self.job_status)) def state_gethost(self): assert self.state_exechost_re, "Misconfigured: define state_exechost_re" @@ -450,6 +446,7 @@ def state_gethost(self): else: return match.expand(self.state_exechost_exp) + class TorqueSpawner(BatchSpawnerRegexStates): batch_script = Unicode("""#!/bin/sh #PBS -q {queue}@{host} @@ -473,6 +470,7 @@ class TorqueSpawner(BatchSpawnerRegexStates): state_running_re = Unicode(r'R').tag(config=True) state_exechost_re = Unicode(r'((?:[\w_-]+\.?)+)/\d+').tag(config=True) + class MoabSpawner(TorqueSpawner): # outputs job id string batch_submit_cmd = Unicode('msub').tag(config=True) @@ -483,6 +481,7 @@ class MoabSpawner(TorqueSpawner): state_running_re = Unicode(r'State="Running"').tag(config=True) state_exechost_re = Unicode(r'AllocNodeList="([^\r\n\t\f :"]*)').tag(config=True) + class UserEnvMixin: """Mixin class that computes values for USER, SHELL and HOME in the environment passed to the job submission subprocess in case the batch system needs these for the batch script.""" @@ -504,6 +503,7 @@ def get_env(self): env = self.user_env(env) return env + class SlurmSpawner(UserEnvMixin,BatchSpawnerRegexStates): """A Spawner that just uses Popen to start local processes.""" @@ -561,6 +561,7 @@ def parse_job_id(self, output): raise e return id + class MultiSlurmSpawner(SlurmSpawner): '''When slurm has been compiled with --enable-multiple-slurmd, the administrator sets the name of the slurmd instance via the slurmd -N @@ -573,6 +574,7 @@ def state_gethost(self): host = SlurmSpawner.state_gethost(self) return self.daemon_resolver.get(host, host) + class GridengineSpawner(BatchSpawnerBase): batch_script = Unicode("""#!/bin/bash #$ -j yes @@ -620,6 +622,7 @@ def state_gethost(self): self.log.error("Spawner unable to match host addr in job {0} with status {1}".format(self.job_id, self.job_status)) return + class CondorSpawner(UserEnvMixin,BatchSpawnerRegexStates): batch_script = Unicode(""" Executable = /bin/sh @@ -657,6 +660,7 @@ def parse_job_id(self, output): def cmd_formatted_for_batch(self): return super(CondorSpawner,self).cmd_formatted_for_batch().replace('"','""').replace("'","''") + class LsfSpawner(BatchSpawnerBase): '''A Spawner that uses IBM's Platform Load Sharing Facility (LSF) to launch notebooks.''' @@ -701,7 +705,6 @@ def state_isrunning(self): if self.job_status: return self.job_status.split(' ')[0].upper() == 'RUN' - def state_gethost(self): if self.job_status: return self.job_status.split(' ')[1].strip() From 9165b0e71f468e584568eed763fe7ceb87fe1eff Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Thu, 5 Jul 2018 13:08:10 -0400 Subject: [PATCH 09/26] RF: consistency for SlurmSpawner - removed dated __doc__ and placed batch_script first --- batchspawner/batchspawner.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/batchspawner/batchspawner.py b/batchspawner/batchspawner.py index af5a35ea..17157e11 100644 --- a/batchspawner/batchspawner.py +++ b/batchspawner/batchspawner.py @@ -505,22 +505,6 @@ def get_env(self): class SlurmSpawner(UserEnvMixin,BatchSpawnerRegexStates): - """A Spawner that just uses Popen to start local processes.""" - - # all these req_foo traits will be available as substvars for templated strings - req_cluster = Unicode('', \ - help="Cluster name to submit job to resource manager" - ).tag(config=True) - - req_qos = Unicode('', \ - help="QoS name to submit job to resource manager" - ).tag(config=True) - - req_srun = Unicode('srun', - help="Job step wrapper, default 'srun'. Set to '' you do not want " - "to run in job step (affects environment handling)" - ).tag(config=True) - batch_script = Unicode("""#!/bin/bash #SBATCH --output={{homedir}}/jupyterhub_slurmspawner_%j.log #SBATCH --job-name=spawner-jupyterhub @@ -540,6 +524,21 @@ class SlurmSpawner(UserEnvMixin,BatchSpawnerRegexStates): echo "jupyterhub-singleuser ended gracefully" {{epilogue}} """).tag(config=True) + + # all these req_foo traits will be available as substvars for templated strings + req_cluster = Unicode('', \ + help="Cluster name to submit job to resource manager" + ).tag(config=True) + + req_qos = Unicode('', \ + help="QoS name to submit job to resource manager" + ).tag(config=True) + + req_srun = Unicode('srun', + help="Job step wrapper, default 'srun'. Set to '' you do not want " + "to run in job step (affects environment handling)" + ).tag(config=True) + # outputs line like "Submitted batch job 209" batch_submit_cmd = Unicode('sbatch --parsable').tag(config=True) # outputs status and exec node like "RUNNING hostname" From 35f0919fd9a3101de32fb1590cf5b922e1039873 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Thu, 5 Jul 2018 13:11:10 -0400 Subject: [PATCH 10/26] RF(minor): remove not needed \ for newline continuation within function call signature For consistency with jupyterhub itself --- batchspawner/batchspawner.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/batchspawner/batchspawner.py b/batchspawner/batchspawner.py index 17157e11..ac33c542 100644 --- a/batchspawner/batchspawner.py +++ b/batchspawner/batchspawner.py @@ -79,52 +79,52 @@ class BatchSpawnerBase(Spawner): # override default server ip since batch jobs normally running remotely ip = Unicode("0.0.0.0", help="Address for singleuser server to listen at").tag(config=True) - exec_prefix = Unicode('sudo -E -u {username}', \ + exec_prefix = Unicode('sudo -E -u {username}', help="Standard executon prefix (e.g. the default sudo -E -u {username})" ).tag(config=True) # all these req_foo traits will be available as substvars for templated strings - req_queue = Unicode('', \ + req_queue = Unicode('', help="Queue name to submit job to resource manager" ).tag(config=True) - req_host = Unicode('', \ + req_host = Unicode('', help="Host name of batch server to submit job to resource manager" ).tag(config=True) - req_memory = Unicode('', \ + req_memory = Unicode('', help="Memory to request from resource manager" ).tag(config=True) - req_nprocs = Unicode('', \ + req_nprocs = Unicode('', help="Number of processors to request from resource manager" ).tag(config=True) - req_ngpus = Unicode('', \ + req_ngpus = Unicode('', help="Number of GPUs to request from resource manager" ).tag(config=True) - req_runtime = Unicode('', \ + req_runtime = Unicode('', help="Length of time for submitted job to run" ).tag(config=True) - req_partition = Unicode('', \ + req_partition = Unicode('', help="Partition name to submit job to resource manager" ).tag(config=True) - req_account = Unicode('', \ + req_account = Unicode('', help="Account name string to pass to the resource manager" ).tag(config=True) - req_options = Unicode('', \ + req_options = Unicode('', help="Other options to include into job submission script" ).tag(config=True) - req_prologue = Unicode('', \ + req_prologue = Unicode('', help="Script to run before single user server starts." ).tag(config=True) - req_epilogue = Unicode('', \ + req_epilogue = Unicode('', help="Script to run after single user server ends." ).tag(config=True) @@ -144,7 +144,7 @@ def _req_homedir_default(self): def _req_keepvars_default(self): return ','.join(self.get_env().keys()) - batch_script = Unicode('', \ + batch_script = Unicode('', help="Template for job submission script. Traits on this class named like req_xyz " "will be substituted in the template for {xyz} using string.Formatter. " "Must include {cmd} which will be replaced with the jupyterhub-singleuser command line." @@ -167,7 +167,7 @@ def get_req_subvars(self): subvars[t[4:]] = getattr(self, t) return subvars - batch_submit_cmd = Unicode('', \ + batch_submit_cmd = Unicode('', help="Command to run to submit batch scripts. Formatted using req_xyz traits as {xyz}." ).tag(config=True) @@ -234,7 +234,7 @@ def submit_batch_script(self): return self.job_id # Override if your batch system needs something more elaborate to read the job status - batch_query_cmd = Unicode('', \ + batch_query_cmd = Unicode('', help="Command to run to read job status. Formatted using req_xyz traits as {xyz} " "and self.job_id as {job_id}." ).tag(config=True) @@ -325,7 +325,7 @@ def poll(self): self.clear_state() return 1 - startup_poll_interval = Float(0.5, \ + startup_poll_interval = Float(0.5, help="Polling interval (seconds) to check job state during startup" ).tag(config=True) @@ -526,11 +526,11 @@ class SlurmSpawner(UserEnvMixin,BatchSpawnerRegexStates): """).tag(config=True) # all these req_foo traits will be available as substvars for templated strings - req_cluster = Unicode('', \ + req_cluster = Unicode('', help="Cluster name to submit job to resource manager" ).tag(config=True) - req_qos = Unicode('', \ + req_qos = Unicode('', help="QoS name to submit job to resource manager" ).tag(config=True) From 6546ca931362f3d2a850d049f9cb64439d80bbf5 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Mon, 9 Jul 2018 09:31:11 -0400 Subject: [PATCH 11/26] RF: revert back to if ... return True else return False --- batchspawner/batchspawner.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/batchspawner/batchspawner.py b/batchspawner/batchspawner.py index ac33c542..282d573a 100644 --- a/batchspawner/batchspawner.py +++ b/batchspawner/batchspawner.py @@ -429,11 +429,17 @@ class BatchSpawnerRegexStates(BatchSpawnerBase): def state_ispending(self): assert self.state_pending_re, "Misconfigured: define state_running_re" - return bool(self.job_status and re.search(self.state_pending_re, self.job_status)) + if self.job_status and re.search(self.state_pending_re, self.job_status): + return True + else: + return False def state_isrunning(self): assert self.state_running_re, "Misconfigured: define state_running_re" - return bool(self.job_status and re.search(self.state_running_re, self.job_status)) + if self.job_status and re.search(self.state_running_re, self.job_status): + return True + else: + return False def state_gethost(self): assert self.state_exechost_re, "Misconfigured: define state_exechost_re" From 5a9d6e86d7568d6830092390bb2a81fe5a742133 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Mon, 9 Jul 2018 09:34:44 -0400 Subject: [PATCH 12/26] RF: another removal of trailing \ --- batchspawner/batchspawner.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/batchspawner/batchspawner.py b/batchspawner/batchspawner.py index 282d573a..8acd101a 100644 --- a/batchspawner/batchspawner.py +++ b/batchspawner/batchspawner.py @@ -335,8 +335,9 @@ def start(self): if self.user and self.user.server and self.user.server.port: self.port = self.user.server.port self.db.commit() - elif (jupyterhub.version_info < (0,7) and not self.user.server.port) or \ - (jupyterhub.version_info >= (0,7) and not self.port): + elif (jupyterhub.version_info < (0,7) and not self.user.server.port) or ( + jupyterhub.version_info >= (0,7) and not self.port + ): self.port = random_port() self.db.commit() job = yield self.submit_batch_script() From 9a00bee372a37db0e210e635537ad07f9de893d6 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Wed, 20 Jun 2018 16:10:59 +0300 Subject: [PATCH 13/26] run_command: Increase debugging output in case of error - Thanks to @krinsman in #90. --- batchspawner/batchspawner.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/batchspawner/batchspawner.py b/batchspawner/batchspawner.py index ac2a68f9..3b71f4a1 100644 --- a/batchspawner/batchspawner.py +++ b/batchspawner/batchspawner.py @@ -198,8 +198,11 @@ def run_command(self, cmd, input=None, env=None): err = yield proc.wait_for_exit() except CalledProcessError: self.log.error("Subprocess returned exitcode %s" % proc.returncode) + self.log.error('Stdout:') + self.log.error(out) + self.log.error('Stderr:') self.log.error(eout) - raise RuntimeError(eout) + raise RuntimeError('{} exit status {}: {}'.format(cmd, proc.returncode, eout)) if err != 0: return err # exit error? else: From a23b3d4d44db7403eeae2ccef0574068a4fca268 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Wed, 20 Jun 2018 16:09:41 +0300 Subject: [PATCH 14/26] run_command: Fix possible deadlock - Reading from stdout and stderr separately can produce a deadlock. I assume that the separate proc.wait_for_exit() doesn't matter here. - Thanks to @krinsman in #90. --- batchspawner/batchspawner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/batchspawner/batchspawner.py b/batchspawner/batchspawner.py index ac2a68f9..4f485bb7 100644 --- a/batchspawner/batchspawner.py +++ b/batchspawner/batchspawner.py @@ -189,8 +189,8 @@ def run_command(self, cmd, input=None, env=None): # Apparently harmless pass proc.stdin.close() - out = yield proc.stdout.read_until_close() - eout = yield proc.stderr.read_until_close() + out, eout = yield [proc.stdout.read_until_close(), + proc.stderr.read_until_close()] proc.stdout.close() proc.stderr.close() eout = eout.decode().strip() From c6b34a99a634aa33432289fcc3989851f76fa5d3 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Tue, 31 Jul 2018 11:34:54 +0300 Subject: [PATCH 15/26] Travis: update for jupyterhub 9.0.1 and python 3.7 --- .travis.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4e8e1bf9..80fa5516 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,25 +2,23 @@ language: python sudo: false python: + - 3.7 - 3.6 - 3.5 - 3.4 env: - JHUB_VER=0.7.2 - JHUB_VER=0.8.1 - - JHUB_VER=0.9.0b2 + - JHUB_VER=0.9.1 matrix: include: - - python: 3.7-dev - env: JHUB_VER=0.9.0b2 - - python: 3.6 + - python: 3.7 env: JHUB_VER=master exclude: - python: 3.4 - env: JHUB_VER=0.9.0b2 + env: JHUB_VER=0.9.1 allow_failures: - env: JHUB_VER=master - - python: 3.7-dev before_install: - npm install -g configurable-http-proxy @@ -28,7 +26,7 @@ before_install: - git clone --quiet --branch $JHUB_VER https://github.com/jupyter/jupyterhub.git jupyterhub install: # Don't let requirements pull in tornado 5 yet except for jupyterhub master - - if [ $JHUB_VER != "master" -a $JHUB_VER != "0.9.0b2" ]; then pip install "tornado<5.0"; fi + - if [ $JHUB_VER != "master" -a $JHUB_VER != "0.9.1" ]; then pip install "tornado<5.0"; fi - pip install --pre -f travis-wheels/wheelhouse -r jupyterhub/dev-requirements.txt - pip install --pre -e jupyterhub From 327d9bda84ee3293aafbed5f612cfe7030087cdb Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Tue, 31 Jul 2018 11:34:54 +0300 Subject: [PATCH 16/26] Travis: update for jupyterhub 9.0.1 and python 3.7 --- .travis.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4e8e1bf9..2a94927b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,25 +2,23 @@ language: python sudo: false python: + - 3.7-dev - 3.6 - 3.5 - 3.4 env: - JHUB_VER=0.7.2 - JHUB_VER=0.8.1 - - JHUB_VER=0.9.0b2 + - JHUB_VER=0.9.1 matrix: include: - python: 3.7-dev - env: JHUB_VER=0.9.0b2 - - python: 3.6 env: JHUB_VER=master exclude: - python: 3.4 - env: JHUB_VER=0.9.0b2 + env: JHUB_VER=0.9.1 allow_failures: - env: JHUB_VER=master - - python: 3.7-dev before_install: - npm install -g configurable-http-proxy @@ -28,7 +26,7 @@ before_install: - git clone --quiet --branch $JHUB_VER https://github.com/jupyter/jupyterhub.git jupyterhub install: # Don't let requirements pull in tornado 5 yet except for jupyterhub master - - if [ $JHUB_VER != "master" -a $JHUB_VER != "0.9.0b2" ]; then pip install "tornado<5.0"; fi + - if [ $JHUB_VER != "master" -a $JHUB_VER != "0.9.1" ]; then pip install "tornado<5.0"; fi - pip install --pre -f travis-wheels/wheelhouse -r jupyterhub/dev-requirements.txt - pip install --pre -e jupyterhub From fa11cb10dcfaaa61a41926268e7155eced46c3d3 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Thu, 2 Aug 2018 15:37:39 +0300 Subject: [PATCH 17/26] Pin jsonschema=2.6.0 to fix test errors --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 2a94927b..305f4e2d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,6 +27,7 @@ before_install: install: # Don't let requirements pull in tornado 5 yet except for jupyterhub master - if [ $JHUB_VER != "master" -a $JHUB_VER != "0.9.1" ]; then pip install "tornado<5.0"; fi + - pip install jsonschema!=3.0.0a1 # issue #110, remove later - pip install --pre -f travis-wheels/wheelhouse -r jupyterhub/dev-requirements.txt - pip install --pre -e jupyterhub From 228107ebcd145dd71cd83818675c5a47db545397 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Fri, 3 Aug 2018 10:13:25 +0300 Subject: [PATCH 18/26] Pass the environment dict to all commands (query and cancel) - Previously, only the query command got the full enivornment. - If this is used to authenticate to the submit/query/cancel commands, then the user's environment gets this also. - Closes: #108 --- batchspawner/batchspawner.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/batchspawner/batchspawner.py b/batchspawner/batchspawner.py index ac2a68f9..4b95d570 100644 --- a/batchspawner/batchspawner.py +++ b/batchspawner/batchspawner.py @@ -250,7 +250,7 @@ def read_job_state(self): cmd = format_template(cmd, **subvars) self.log.debug('Spawner querying job: ' + cmd) try: - out = yield self.run_command(cmd) + out = yield self.run_command(cmd, env=self.get_env()) self.job_status = out except Exception as e: self.log.error('Error querying job ' + self.job_id) @@ -269,7 +269,7 @@ def cancel_batch_job(self): cmd = self.exec_prefix + ' ' + self.batch_cancel_cmd cmd = format_template(cmd, **subvars) self.log.info('Cancelling job ' + self.job_id + ': ' + cmd) - yield self.run_command(cmd) + yield self.run_command(cmd, env=self.get_env()) def load_state(self, state): """load job_id from state""" @@ -499,7 +499,12 @@ def user_env(self, env): return env def get_env(self): - """Add user environment variables""" + """Add user environment variables. + + Everything here should be passed to the user's job. If it is + used to authenticate to the batch system commands as an admin, + beware that the user will get them too. + """ env = super().get_env() env = self.user_env(env) return env From a6966016914131bfab759aaa8f59be7e534b27d9 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Wed, 8 Aug 2018 00:44:16 +0300 Subject: [PATCH 19/26] SPAWNERS.md: update SlurmSpawner and general info. --- README.md | 2 +- SPAWNERS.md | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2cb0cb57..eb552f00 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Unless otherwise stated for a specific spawner, assume that spawners *do* evaluate shell environment for users and thus the [security requriemnts of JupyterHub security for untrusted users](https://jupyterhub.readthedocs.io/en/stable/reference/websecurity.html) -are not fulfilled. +are not fulfilled. This is something which we are working on. ## Provide different configurations of BatchSpawner diff --git a/SPAWNERS.md b/SPAWNERS.md index ec7fad09..f4427506 100644 --- a/SPAWNERS.md +++ b/SPAWNERS.md @@ -16,9 +16,13 @@ Maintainers: Maintainers: @rkdarst -This spawner enforces the environment if `srun` is used, which is the -default. If you *do* want user environment to be used, set -`req_srun=''`. +This spawner enforces the environment if `srun` is used to wrap the +spawner command, which is the default. If you *do* want user +environment to be used, set `req_srun=''`. However, this is not +perfect: there is still a bash shell begun as the user which could run +arbitrary startup, define shell aliases for `srun`, etc. + +Use of `srun` is required to gracefully terminate. ## `GridengineSpawner` @@ -38,12 +42,14 @@ Maintainers: # Checklist for making spawners -- Does your spawner read shell environment before starting? +- Does your spawner read shell environment before starting? (See + [Jupyterhub Security](https://jupyterhub.readthedocs.io/en/stable/reference/websecurity.html). - Does your spawner send SIGTERM to the jupyterhub-singleuser process before SIGKILL? It should, so that the process can terminate - gracefully. If you don't see the script end (e.g. you can add `echo - "terminated gracefully"` to the end of your script and see it), you - should check. PR #75 might help here, ask the poster to finalize - it. + gracefully. Add `echo "terminated gracefully"` to the end of the + batch script - if you see this in your singleuser server output, you + know that you DO receive SIGTERM and terminate gracefully. If your + batch system can not automatically send SIGTERM before SIGKILL, PR + #75 might help here, ask for it to be finished. From 02c33a628bcfbd5f6258c731f8f9a7bda6bb7661 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Wed, 8 Aug 2018 00:56:35 +0300 Subject: [PATCH 20/26] SlurmSpawner: Add reservation option to batch script --- batchspawner/batchspawner.py | 1 + 1 file changed, 1 insertion(+) diff --git a/batchspawner/batchspawner.py b/batchspawner/batchspawner.py index 4c7c716a..99280e11 100644 --- a/batchspawner/batchspawner.py +++ b/batchspawner/batchspawner.py @@ -527,6 +527,7 @@ class SlurmSpawner(UserEnvMixin,BatchSpawnerRegexStates): {% endif %}{% if runtime %}#SBATCH --time={{runtime}} {% endif %}{% if memory %}#SBATCH --mem={{memory}} {% endif %}{% if nprocs %}#SBATCH --cpus-per-task={{nprocs}} +{% endif %}{% if reservation%}#SBATCH --reservation={{reservation}} {% endif %}{% if options %}#SBATCH {{options}}{% endif %} trap 'echo SIGTERM received' TERM From 81ff85a143e6b48331ea42795cb06932efc010a2 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Wed, 8 Aug 2018 00:56:50 +0300 Subject: [PATCH 21/26] SlurmSpawner: add test for reservation option --- batchspawner/tests/test_spawners.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/batchspawner/tests/test_spawners.py b/batchspawner/tests/test_spawners.py index acc2b80a..56c0f786 100644 --- a/batchspawner/tests/test_spawners.py +++ b/batchspawner/tests/test_spawners.py @@ -332,12 +332,14 @@ def test_slurm(db, io_loop): 'req_nprocs': '5', 'req_memory': '5678', 'req_options': 'some_option_asdf', + 'req_reservation': 'RES123', } batch_script_re_list = [ re.compile(r'srun .* singleuser_command', re.X|re.M), re.compile(r'^#SBATCH \s+ --cpus-per-task=5', re.X|re.M), re.compile(r'^#SBATCH \s+ --time=3-05:10:10', re.X|re.M), re.compile(r'^#SBATCH \s+ some_option_asdf', re.X|re.M), + re.compile(r'^#SBATCH \s+ --reservation=RES123', re.X|re.M), ] script = [ (re.compile(r'sudo.*sbatch'), str(testjob)), From 4cf37329b659b50ad28ba674900571a452a038e9 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Wed, 8 Aug 2018 01:09:45 +0300 Subject: [PATCH 22/26] Update changelog for release --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 4205400a..ac88e865 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,13 @@ clusters, as well as an option to run a local notebook directly on the jupyterhu * Add new option exec_prefix, which defaults to `sudo -E -u {username}`. This replaces explicit `sudo` in every batch command - changes in local commands may be needed. * Add many more tests. * Update minimum requirements to JupyterHub 0.8.1 and Python 3.4. +* New option: `req_keepvars_extra`, which allows keeping extra variables in addition to what is defined by JupyterHub itself (addition of variables to keep instead of replacement). #99 +* Add `req_prologue` and `req_epilogue` options to scripts which are inserted before/after the main jupyterhub-singleuser command, which allow for generic setup/cleanup without overriding the entire script. #96 +* Add a new page `SPAWNERS.md` which information on specific spawners. #97 +* Pass the environment dictionary to the queue and cancel commands as well. This is mostly user environment, but may be useful to these commands as well in some cases. #108, #111 +* SlurmSpawner: add the `req_reservation` option. # +* Improve debugging on failed submission by raising errors including error messages from the commands. #106 +* Many other non-user or developer visible changes. #107 #106 #100 ### v0.8.1 (bugfix release) From b9f71ab383640c28c81d328d30b0a03ada0f1377 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Wed, 8 Aug 2018 10:47:58 +0300 Subject: [PATCH 23/26] get_env: clarify documentation and security risk --- batchspawner/batchspawner.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/batchspawner/batchspawner.py b/batchspawner/batchspawner.py index 4b95d570..94bd2b08 100644 --- a/batchspawner/batchspawner.py +++ b/batchspawner/batchspawner.py @@ -499,11 +499,12 @@ def user_env(self, env): return env def get_env(self): - """Add user environment variables. + """Get user environment variables to be passed to the user's job - Everything here should be passed to the user's job. If it is - used to authenticate to the batch system commands as an admin, - beware that the user will get them too. + Everything here should be passed to the user's job as + environment. Caution: If these variables are used for + authentication to the batch system commands as an admin, be + aware that the user will receive access to these as well. """ env = super().get_env() env = self.user_env(env) From d4e21f49378773aff17cd7f1cf82eef90e17098d Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Wed, 8 Aug 2018 10:44:58 +0300 Subject: [PATCH 24/26] Spawner detail list: updates from code review - Thanks @willingc --- README.md | 6 +++++- SPAWNERS.md | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eb552f00..cb7cbf8f 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,11 @@ Unless otherwise stated for a specific spawner, assume that spawners *do* evaluate shell environment for users and thus the [security requriemnts of JupyterHub security for untrusted users](https://jupyterhub.readthedocs.io/en/stable/reference/websecurity.html) -are not fulfilled. This is something which we are working on. +are not fulfilled because some (most?) spawners *do* start a user +shell which will execute arbitrary user environment configuration +(``.profile``, ``.bashrc`` and the like) unless users do not have +access to their own cluster user account. This is something which we +are working on. ## Provide different configurations of BatchSpawner diff --git a/SPAWNERS.md b/SPAWNERS.md index f4427506..6548eb34 100644 --- a/SPAWNERS.md +++ b/SPAWNERS.md @@ -42,8 +42,13 @@ Maintainers: # Checklist for making spawners +Please document each of these things under the spawner list above, - +even if it is "OK", we need to track status of all spawners. If it is +a bug, users really need to know. + - Does your spawner read shell environment before starting? (See - [Jupyterhub Security](https://jupyterhub.readthedocs.io/en/stable/reference/websecurity.html). + [Jupyterhub + Security](https://jupyterhub.readthedocs.io/en/stable/reference/websecurity.html). - Does your spawner send SIGTERM to the jupyterhub-singleuser process before SIGKILL? It should, so that the process can terminate From 594ba775c93bcca331845f82a4fa46a90b70c728 Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Wed, 8 Aug 2018 10:59:22 +0300 Subject: [PATCH 25/26] README.md: Typo fix - Thanks @willingc --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cb7cbf8f..bd13b6c5 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ to run Jupyter notebooks on an academic supercomputer cluster. Unless otherwise stated for a specific spawner, assume that spawners *do* evaluate shell environment for users and thus the [security -requriemnts of JupyterHub security for untrusted +requiremnts of JupyterHub security for untrusted users](https://jupyterhub.readthedocs.io/en/stable/reference/websecurity.html) are not fulfilled because some (most?) spawners *do* start a user shell which will execute arbitrary user environment configuration From 21af0afb883ec866320690855c7e75285969ca8a Mon Sep 17 00:00:00 2001 From: Richard Darst Date: Wed, 8 Aug 2018 11:13:43 +0300 Subject: [PATCH 26/26] Changelog: Split into sections by topic. - Thanks to @willingc for the suggestion. --- README.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ac88e865..3592c2f4 100644 --- a/README.md +++ b/README.md @@ -141,18 +141,33 @@ clusters, as well as an option to run a local notebook directly on the jupyterhu ### dev (requires minimum JupyterHub 0.7.2 and Python 3.4) +Added (user) + * Add Jinja2 templating as an option for all scripts and commands. If '{{' or `{%` is used anywhere in the string, it is used as a jinja2 template. -* Update Slurm batch script. Now, the single-user notebook is run in a job step, with a wrapper of `srun`. This may need to be removed if you don't want environment variables limited. * Add new option exec_prefix, which defaults to `sudo -E -u {username}`. This replaces explicit `sudo` in every batch command - changes in local commands may be needed. -* Add many more tests. -* Update minimum requirements to JupyterHub 0.8.1 and Python 3.4. * New option: `req_keepvars_extra`, which allows keeping extra variables in addition to what is defined by JupyterHub itself (addition of variables to keep instead of replacement). #99 * Add `req_prologue` and `req_epilogue` options to scripts which are inserted before/after the main jupyterhub-singleuser command, which allow for generic setup/cleanup without overriding the entire script. #96 -* Add a new page `SPAWNERS.md` which information on specific spawners. #97 -* Pass the environment dictionary to the queue and cancel commands as well. This is mostly user environment, but may be useful to these commands as well in some cases. #108, #111 * SlurmSpawner: add the `req_reservation` option. # + +Added (developer) + +* Add many more tests. +* Add a new page `SPAWNERS.md` which information on specific spawners. Begin trying to collect a list of spawner-specific contacts. #97 + +Changed + +* Update minimum requirements to JupyterHub 0.8.1 and Python 3.4. +* Update Slurm batch script. Now, the single-user notebook is run in a job step, with a wrapper of `srun`. This may need to be removed using `req_srun=''` if you don't want environment variables limited. +* Pass the environment dictionary to the queue and cancel commands as well. This is mostly user environment, but may be useful to these commands as well in some cases. #108, #111 If these envioronment variables were used for authentication as an admin, be aware that there are pre-existing security issues because they may be passed to the user via the batch submit command, see #82. + +Fixed + * Improve debugging on failed submission by raising errors including error messages from the commands. #106 * Many other non-user or developer visible changes. #107 #106 #100 +* In Travis CI, blacklist jsonschema=3.0.0a1 because it breaks tests + +Removed + ### v0.8.1 (bugfix release)