mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-10 11:33:01 +00:00
148 lines
4.6 KiB
Markdown
148 lines
4.6 KiB
Markdown
# Writing a custom Spawner
|
|
|
|
Each single-user server is started by a [Spawner][].
|
|
The Spawner represents an abstract interface to a process,
|
|
and a custom Spawner needs to be able to take three actions:
|
|
|
|
1. start the process
|
|
2. poll whether the process is still running
|
|
3. stop the process
|
|
|
|
See a list of custom Spawners [on the wiki](https://github.com/jupyter/jupyterhub/wiki/Spawners).
|
|
|
|
|
|
## Spawner.start
|
|
|
|
`Spawner.start` should start the single-user server for a single user.
|
|
Information about the user can be retrieved from `self.user`,
|
|
an object encapsulating the user's name, authentication, and server info.
|
|
|
|
When `Spawner.start` returns, it should have stored the IP and port
|
|
of the single-user server in `self.user.server`.
|
|
|
|
**NOTE:** when writing coroutines, *never* `yield` in between a db change and a commit.
|
|
Most `Spawner.start`s should have something looking like:
|
|
|
|
```python
|
|
def start(self):
|
|
self.user.server.ip = 'localhost' # or other host or IP address, as seen by the Hub
|
|
self.user.server.port = 1234 # port selected somehow
|
|
self.db.commit() # always commit before yield, if modifying db values
|
|
yield self._actually_start_server_somehow()
|
|
```
|
|
|
|
When `Spawner.start` returns, the single-user server process should actually be running,
|
|
not just requested. JupyterHub can handle `Spawner.start` being very slow
|
|
(such as PBS-style batch queues, or instantiating whole AWS instances)
|
|
via relaxing the `Spawner.start_timeout` config value.
|
|
|
|
|
|
## Spawner.poll
|
|
|
|
`Spawner.poll` should check if the spawner is still running.
|
|
It should return `None` if it is still running,
|
|
and an integer exit status, otherwise.
|
|
|
|
For the local process case, this uses `os.kill(PID, 0)`
|
|
to check if the process is still around.
|
|
|
|
|
|
## Spawner.stop
|
|
|
|
`Spawner.stop` should stop the process. It must be a tornado coroutine,
|
|
and should return when the process has finished exiting.
|
|
|
|
|
|
## Spawner state
|
|
|
|
JupyterHub should be able to stop and restart without having to teardown
|
|
single-user servers. This means that a Spawner may need to persist
|
|
some information that it can be restored.
|
|
A dictionary of JSON-able state can be used to store this information.
|
|
|
|
Unlike start/stop/poll, the state methods must not be coroutines.
|
|
|
|
In the single-process case, this is only the process ID of the server:
|
|
|
|
```python
|
|
def get_state(self):
|
|
"""get the current state"""
|
|
state = super().get_state()
|
|
if self.pid:
|
|
state['pid'] = self.pid
|
|
return state
|
|
|
|
def load_state(self, state):
|
|
"""load state from the database"""
|
|
super().load_state(state)
|
|
if 'pid' in state:
|
|
self.pid = state['pid']
|
|
|
|
def clear_state(self):
|
|
"""clear any state (called after shutdown)"""
|
|
super().clear_state()
|
|
self.pid = 0
|
|
```
|
|
|
|
## Spawner options form
|
|
|
|
(new in 0.4)
|
|
|
|
Some deployments may want to offer options to users to influence how their servers are started.
|
|
This may include cluster-based deployments, where users specify what resources should be available,
|
|
or docker-based deployments where users can select from a list of base images.
|
|
|
|
This feature is enabled by setting `Spawner.options_form`, which is an HTML form snippet
|
|
inserted unmodified into the spawn form.
|
|
If the `Spawner.options_form` is defined, when a user would start their server, they will be directed to a form page, like this:
|
|
|
|

|
|
|
|
If `Spawner.options_form` is undefined, the users server is spawned directly, and no spawn page is rendered.
|
|
|
|
See [this example](https://github.com/jupyter/jupyterhub/blob/master/examples/spawn-form/jupyterhub_config.py) for a form that allows custom CLI args for the local spawner.
|
|
|
|
|
|
### `Spawner.options_from_form`
|
|
|
|
Options from this form will always be a dictionary of lists of strings, e.g.:
|
|
|
|
```python
|
|
{
|
|
'integer': ['5'],
|
|
'text': ['some text'],
|
|
'select': ['a', 'b'],
|
|
}
|
|
```
|
|
|
|
When formdata arrives, it is passed through `Spawner.options_from_form(formdata)`,
|
|
which is a method to turn the form data into the correct structure.
|
|
This method must return a dictionary, and is meant to interpret the lists-of-strings into the correct types, e.g. for the above form it would look like:
|
|
|
|
```python
|
|
def options_from_form(self, formdata):
|
|
options = {}
|
|
options['integer'] = int(formdata['integer'][0]) # single integer value
|
|
options['text'] = formdata['text'][0] # single string value
|
|
options['select'] = formdata['select'] # list already correct
|
|
options['notinform'] = 'extra info' # not in the form at all
|
|
return options
|
|
```
|
|
|
|
which would return:
|
|
|
|
```python
|
|
{
|
|
'integer': 5,
|
|
'text': 'some text',
|
|
'select': ['a', 'b'],
|
|
'notinform': 'extra info',
|
|
}
|
|
```
|
|
|
|
When `Spawner.spawn` is called, this dict is accessible as `self.user_options`.
|
|
|
|
|
|
|
|
[Spawner]: ../jupyterhub/spawner.py
|