mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-12 20:43:02 +00:00
Add detailed doc for starting/stopping/waiting for servers via api
and complete implementation in examples/server-api
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,6 +8,7 @@ dist
|
|||||||
docs/_build
|
docs/_build
|
||||||
docs/build
|
docs/build
|
||||||
docs/source/_static/rest-api
|
docs/source/_static/rest-api
|
||||||
|
docs/source/rbac/scope-table.md
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
# ignore config file at the top-level of the repo
|
# ignore config file at the top-level of the repo
|
||||||
# but not sub-dirs
|
# but not sub-dirs
|
||||||
@@ -30,3 +31,4 @@ pip-wheel-metadata
|
|||||||
docs/source/reference/metrics.rst
|
docs/source/reference/metrics.rst
|
||||||
oldest-requirements.txt
|
oldest-requirements.txt
|
||||||
jupyterhub-proxy.pid
|
jupyterhub-proxy.pid
|
||||||
|
examples/server-api/service-token
|
||||||
|
@@ -22,6 +22,9 @@ extensions = [
|
|||||||
'myst_parser',
|
'myst_parser',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
myst_enable_extensions = [
|
||||||
|
'deflist',
|
||||||
|
]
|
||||||
# The master toctree document.
|
# The master toctree document.
|
||||||
master_doc = 'index'
|
master_doc = 'index'
|
||||||
|
|
||||||
|
@@ -16,6 +16,7 @@ what happens under-the-hood when you deploy and configure your JupyterHub.
|
|||||||
proxy
|
proxy
|
||||||
separate-proxy
|
separate-proxy
|
||||||
rest
|
rest
|
||||||
|
server-api
|
||||||
monitoring
|
monitoring
|
||||||
database
|
database
|
||||||
templates
|
templates
|
||||||
|
367
docs/source/reference/server-api.md
Normal file
367
docs/source/reference/server-api.md
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
# Starting servers with the JupyterHub API
|
||||||
|
|
||||||
|
JupyterHub's [REST API][] allows launching.
|
||||||
|
This allows you to build services launching Jupyter-based services for users
|
||||||
|
without relying on the JupyterHub API at all.
|
||||||
|
[BinderHub][] is an example of such an application.
|
||||||
|
|
||||||
|
[binderhub]: https://binderhub.readthedocs.io
|
||||||
|
[rest api]: ../reference/rest.md
|
||||||
|
|
||||||
|
This document provides an example of working with the JupyterHub API to
|
||||||
|
manager servers for users.
|
||||||
|
In particular, we will cover how to:
|
||||||
|
|
||||||
|
1. [check status of servers](checking)
|
||||||
|
2. [start servers](starting)
|
||||||
|
3. [wait for servers to be ready](waiting)
|
||||||
|
4. [communicate with servers](communicating)
|
||||||
|
5. [stop servers](stopping)
|
||||||
|
|
||||||
|
(checking)=
|
||||||
|
|
||||||
|
## Checking server status
|
||||||
|
|
||||||
|
Requesting information about a user includes a `servers` field,
|
||||||
|
which is a dictionary.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /hub/api/users/:username
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required scope: `read:servers`**
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"admin": false,
|
||||||
|
"groups": [],
|
||||||
|
"pending": null,
|
||||||
|
"server": null,
|
||||||
|
"name": "test-1",
|
||||||
|
"kind": "user",
|
||||||
|
"last_activity": "2021-08-03T18:12:46.026411Z",
|
||||||
|
"created": "2021-08-03T18:09:59.767600Z",
|
||||||
|
"roles": ["user"],
|
||||||
|
"servers": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the `servers` dict is empty, the user has no running servers.
|
||||||
|
The keys of the `servers` dict are server names as strings.
|
||||||
|
Many JupyterHub deployments only use the 'default' server,
|
||||||
|
which has the empty string `''` for a name.
|
||||||
|
In this case, the servers dict will always have either zero or one elements.
|
||||||
|
|
||||||
|
This is the servers dict when the user's default server is fully running and ready:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"servers": {
|
||||||
|
"": {
|
||||||
|
"name": "",
|
||||||
|
"last_activity": "2021-08-03T18:48:35.934000Z",
|
||||||
|
"started": "2021-08-03T18:48:29.093885Z",
|
||||||
|
"pending": null,
|
||||||
|
"ready": true,
|
||||||
|
"url": "/user/test-1/",
|
||||||
|
"user_options": {},
|
||||||
|
"progress_url": "/hub/api/users/test-1/server/progress",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key properties of a server:
|
||||||
|
|
||||||
|
name
|
||||||
|
: the server's name. Always the same as the key in `servers`
|
||||||
|
|
||||||
|
ready
|
||||||
|
: boolean. If True, the server can be expected to respond to requests at `url`.
|
||||||
|
|
||||||
|
pending
|
||||||
|
: `null` or a string indicating a transitional state (such as `start` or `stop`).
|
||||||
|
Will always be `null` if `ready` is true,
|
||||||
|
and will always be a string if `ready` is false.
|
||||||
|
|
||||||
|
url
|
||||||
|
: The server's url (just the path, e.g. `/users/:name/:servername/`)
|
||||||
|
where the server can be accessed if `ready` is true.
|
||||||
|
|
||||||
|
progress_url
|
||||||
|
: The API url path (starting with `/hub/api`)
|
||||||
|
where the progress API can be used to wait for the server to be ready.
|
||||||
|
See below for more details on the progress API.
|
||||||
|
|
||||||
|
last_activity
|
||||||
|
: ISO8601 timestamp indicating when activity was last observed on the server
|
||||||
|
|
||||||
|
started
|
||||||
|
: ISO801 timestamp indicating when the server was last started
|
||||||
|
|
||||||
|
We've seen the `servers` model with no servers and with one 'ready' server.
|
||||||
|
Here is what it looks like immediately after requesting a server launch,
|
||||||
|
while the server is not ready yet:
|
||||||
|
|
||||||
|
```python
|
||||||
|
...
|
||||||
|
"servers": {
|
||||||
|
"": {
|
||||||
|
"name": "",
|
||||||
|
"last_activity": "2021-08-03T18:48:29.093885Z",
|
||||||
|
"started": "2021-08-03T18:48:29.093885Z",
|
||||||
|
"pending": "spawn",
|
||||||
|
"ready": false,
|
||||||
|
"url": "/user/test-1/",
|
||||||
|
"user_options": {},
|
||||||
|
"progress_url": "/hub/api/users/test-1/server/progress",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that `ready` is false and `pending` is `spawn`.
|
||||||
|
This means that the server is not ready
|
||||||
|
(attempting to access it may not work)
|
||||||
|
because it isn't finished spawning yet.
|
||||||
|
We'll get more into that below in [waiting for a server][].
|
||||||
|
|
||||||
|
[waiting for a server]: waiting
|
||||||
|
|
||||||
|
(starting)=
|
||||||
|
|
||||||
|
## Starting servers
|
||||||
|
|
||||||
|
To start a server, make the request
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /hub/api/users/:username/servers/[:servername]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required scope: `servers`**
|
||||||
|
|
||||||
|
(omit servername for the default server)
|
||||||
|
|
||||||
|
Assuming the request was valid,
|
||||||
|
there are two possible responses:
|
||||||
|
|
||||||
|
201 Created
|
||||||
|
: This status code means the launch completed and the server is ready.
|
||||||
|
It should be available at the server's URL immediately.
|
||||||
|
|
||||||
|
202 Accepted
|
||||||
|
: This is the more likely response,
|
||||||
|
and means that the server has begun launching,
|
||||||
|
but wasn't immediately ready.
|
||||||
|
The server has `pending: 'spawn'` at this point.
|
||||||
|
|
||||||
|
_Aside: how quickly JupyterHub responds with `202 Accepted` is governed by the `slow_spawn_timeout` tornado setting._
|
||||||
|
|
||||||
|
(waiting)=
|
||||||
|
|
||||||
|
## Waiting for a server
|
||||||
|
|
||||||
|
If you are starting a server via the API,
|
||||||
|
there's a good change you want to know when it's ready.
|
||||||
|
There are two ways to do with:
|
||||||
|
|
||||||
|
1. {ref}`Polling the server model <polling>`
|
||||||
|
2. the {ref}`progress API <progress>`
|
||||||
|
|
||||||
|
(polling)=
|
||||||
|
|
||||||
|
### Polling the server model
|
||||||
|
|
||||||
|
The simplest way to check if a server is ready
|
||||||
|
is to request the user model.
|
||||||
|
|
||||||
|
If:
|
||||||
|
|
||||||
|
1. the server name is in the user's `servers` model, and
|
||||||
|
2. `servers['servername']['ready']` is true
|
||||||
|
|
||||||
|
A Python example, checking if a server is ready:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def server_ready(hub_url, user, server_name="", token):
|
||||||
|
r = requests.get(
|
||||||
|
f"{hub_url}/hub/api/users/{user}/servers/{server_name}",
|
||||||
|
headers={"Authorization": f"token {token}"},
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
user_model = r.json()
|
||||||
|
servers = user_model.get("servers", {})
|
||||||
|
if server_name not in servers:
|
||||||
|
return False
|
||||||
|
|
||||||
|
server = servers[server_name]
|
||||||
|
if server['ready']:
|
||||||
|
print(f"Server {user}/{server_name} ready at {server['url']}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"Server {user}/{server_name} not ready, pending {server['pending']}")
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
You can keep making this check until `ready` is true.
|
||||||
|
|
||||||
|
(progress)=
|
||||||
|
|
||||||
|
### Progress API
|
||||||
|
|
||||||
|
The most _efficient_ way to wait for a server to start is the progress API.
|
||||||
|
|
||||||
|
The progress URL is available in the server model under `progress_url`,
|
||||||
|
and has the form `/hub/api/users/:user/servers/:servername/progress`.
|
||||||
|
|
||||||
|
_the default server progress can be accessed at `:user/servers//progress` or `:user/server/progress`_
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /hub/api/users/:user/servers/:servername/progress
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required scope: `read:servers`**
|
||||||
|
|
||||||
|
This is an [EventStream][] API.
|
||||||
|
In an event stream, messages are _streamed_ and delivered on lines of the form:
|
||||||
|
|
||||||
|
```
|
||||||
|
data: {"progress": 10, "message": "...", ...}
|
||||||
|
```
|
||||||
|
|
||||||
|
where the line after `data:` contains a JSON-serialized dictionary.
|
||||||
|
Lines that do not start with `data:` should be ignored.
|
||||||
|
|
||||||
|
[eventstream]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#examples
|
||||||
|
|
||||||
|
progress events have the form:
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"progress": 0-100,
|
||||||
|
"message": ""
|
||||||
|
"ready": true, # or false
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
progress
|
||||||
|
: integer, 0-100
|
||||||
|
|
||||||
|
message
|
||||||
|
: string message describing progress stages
|
||||||
|
|
||||||
|
ready
|
||||||
|
: present and true only for the last event when the server is ready
|
||||||
|
|
||||||
|
url
|
||||||
|
: only present if `ready` is true; will be the server's url
|
||||||
|
|
||||||
|
the progress API can be used even with fully ready servers.
|
||||||
|
If the server is ready,
|
||||||
|
there will only be one event that looks like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"progress": 100,
|
||||||
|
"ready": true,
|
||||||
|
"message": "Server ready at /user/test-1/",
|
||||||
|
"html_message": "Server ready at <a href=\"/user/test-1/\">/user/test-1/</a>",
|
||||||
|
"url": "/user/test-1/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
where `ready` and `url` are the same as in the server model (`ready` will always be true).
|
||||||
|
|
||||||
|
A typical complete stream from the event-stream API:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
data: {"progress": 0, "message": "Server requested"}
|
||||||
|
|
||||||
|
data: {"progress": 50, "message": "Spawning server..."}
|
||||||
|
|
||||||
|
data: {"progress": 100, "ready": true, "message": "Server ready at /user/test-user/", "html_message": "Server ready at <a href=\"/user/test-user/\">/user/test-user/</a>", "url": "/user/test-user/"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is a Python example for consuming an event stream:
|
||||||
|
|
||||||
|
```{literalinclude} ../../../examples/server-api/start-stop-server.py
|
||||||
|
:language: python
|
||||||
|
:pyobject: event_stream
|
||||||
|
```
|
||||||
|
|
||||||
|
(stopping)=
|
||||||
|
|
||||||
|
## Stopping servers
|
||||||
|
|
||||||
|
Servers can be stopped with a DELETE request:
|
||||||
|
|
||||||
|
```
|
||||||
|
DELETE /hub/api/users/:user/servers/[:servername]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Required scope: `servers`**
|
||||||
|
|
||||||
|
Like start, delete may not complete immediately.
|
||||||
|
The DELETE request has two possible response codes:
|
||||||
|
|
||||||
|
204 Deleted
|
||||||
|
: This status code means the delete completed and the server is fully stopped.
|
||||||
|
It will now be absent from the user `servers` model.
|
||||||
|
|
||||||
|
202 Accepted
|
||||||
|
: Like start, `202` means your request was accepted,
|
||||||
|
but is not yet complete.
|
||||||
|
The server has `pending: 'stop'` at this point.
|
||||||
|
|
||||||
|
Unlike start, there is no progress API for stop.
|
||||||
|
To wait for stop to finish, you must poll the user model
|
||||||
|
and wait for the server to disappear from the user `servers` model.
|
||||||
|
|
||||||
|
```{literalinclude} ../../../examples/server-api/start-stop-server.py
|
||||||
|
:language: python
|
||||||
|
:pyobject: stop_server
|
||||||
|
```
|
||||||
|
|
||||||
|
(communicating)=
|
||||||
|
|
||||||
|
## Communicating with servers
|
||||||
|
|
||||||
|
JupyterHub tokens with the the `access:servers` scope
|
||||||
|
can be used to communicate with servers themselves.
|
||||||
|
This can be the same token you used to launch your service.
|
||||||
|
|
||||||
|
```{note}
|
||||||
|
Access scopes are new in JupyterHub 2.0.
|
||||||
|
To access servers in JupyterHub 1.x,
|
||||||
|
a token must be owned by the same user as the server,
|
||||||
|
*or* be an admin token if admin_access is enabled.
|
||||||
|
```
|
||||||
|
|
||||||
|
The URL returned from a server model is the url path suffix,
|
||||||
|
e.g. `/user/:name/` to append to the jupyterhub base URL.
|
||||||
|
|
||||||
|
For instance, `{hub_url}{server_url}`,
|
||||||
|
where `hub_url` would be e.g. `http://127.0.0.1:8000` by default,
|
||||||
|
and `server_url` `/user/myname`,
|
||||||
|
for a full url of `http://127.0.0.1:8000/user/myname`.
|
||||||
|
|
||||||
|
## Python example
|
||||||
|
|
||||||
|
The JupyterHub repo includes a complete example in {file}`examples/server-api`
|
||||||
|
tying all this together.
|
||||||
|
|
||||||
|
To summarize the steps:
|
||||||
|
|
||||||
|
1. the `/user/:name`
|
||||||
|
2. the server model includes a `ready` state to tell you if it's ready
|
||||||
|
3. if it's not ready, you can use the `progress_url` field to wait
|
||||||
|
4. if it is ready, you can use the `url` field to link directly to the running srver
|
||||||
|
|
||||||
|
The example demonstrates starting and stopping servers via the JupyterHub API,
|
||||||
|
including waiting for them to start via the progress API,
|
||||||
|
as well as waiting for them to stop via polling the user model.
|
||||||
|
|
||||||
|
```{literalinclude} ../../../examples/server-api/start-stop-server.py
|
||||||
|
:language: python
|
||||||
|
:start-at: def event_stream
|
||||||
|
:end-before: def main
|
||||||
|
```
|
55
examples/server-api/jupyterhub_config.py
Normal file
55
examples/server-api/jupyterhub_config.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# create a role with permissions to:
|
||||||
|
# 1. start/stop servers, and
|
||||||
|
# 2. access the server API
|
||||||
|
|
||||||
|
c.JupyterHub.load_roles = [
|
||||||
|
{
|
||||||
|
"name": "launcher",
|
||||||
|
"scopes": [
|
||||||
|
"servers", # manage servers
|
||||||
|
"access:servers", # access servers themselves
|
||||||
|
],
|
||||||
|
# assign role to our 'launcher' service
|
||||||
|
"services": ["launcher"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# persist token to a file, to share it with the launch-server.py script
|
||||||
|
import pathlib
|
||||||
|
import secrets
|
||||||
|
|
||||||
|
here = pathlib.Path(__file__).parent
|
||||||
|
token_file = here.joinpath("service-token")
|
||||||
|
if token_file.exists():
|
||||||
|
with token_file.open("r") as f:
|
||||||
|
token = f.read()
|
||||||
|
else:
|
||||||
|
token = secrets.token_hex(16)
|
||||||
|
with token_file.open("w") as f:
|
||||||
|
f.write(token)
|
||||||
|
|
||||||
|
# define our service
|
||||||
|
c.JupyterHub.services = [
|
||||||
|
{
|
||||||
|
"name": "launcher",
|
||||||
|
"api_token": token,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# ensure spawn requests return immediately,
|
||||||
|
# rather than waiting up to 10 seconds for spawn to complete
|
||||||
|
# this ensures that we use the progress API
|
||||||
|
|
||||||
|
c.JupyterHub.tornado_settings = {"slow_spawn_timeout": 0}
|
||||||
|
|
||||||
|
# create our test-user
|
||||||
|
c.Authenticator.allowed_users = {
|
||||||
|
'test-user',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# testing boilerplate: fake auth/spawner, localhost. Don't use this for real!
|
||||||
|
c.JupyterHub.authenticator_class = 'dummy'
|
||||||
|
c.JupyterHub.spawner_class = 'simple'
|
||||||
|
c.JupyterHub.ip = '127.0.0.1'
|
173
examples/server-api/start-stop-server.py
Normal file
173
examples/server-api/start-stop-server.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Example of starting/stopping a server via the JupyterHub API
|
||||||
|
|
||||||
|
1. get user status
|
||||||
|
2. start server
|
||||||
|
3. wait for server to be ready via progress api
|
||||||
|
4. make a request to the server itself
|
||||||
|
5. stop server via API
|
||||||
|
6. wait for server to finish stopping
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import pathlib
|
||||||
|
import time
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_token():
|
||||||
|
"""boilerplate: get token from share file.
|
||||||
|
|
||||||
|
Make sure to start jupyterhub in this directory first
|
||||||
|
"""
|
||||||
|
here = pathlib.Path(__file__).parent
|
||||||
|
token_file = here.joinpath("service-token")
|
||||||
|
log.info(f"Loading token from {token_file}")
|
||||||
|
with token_file.open("r") as f:
|
||||||
|
token = f.read()
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def make_session(token):
|
||||||
|
"""Create a requests.Session with our service token in the Authorization header"""
|
||||||
|
session = requests.Session()
|
||||||
|
session.headers = {"Authorization": f"token {token}"}
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
def event_stream(session, url):
|
||||||
|
"""Generator yielding events from a JSON event stream
|
||||||
|
|
||||||
|
For use with the server progress API
|
||||||
|
"""
|
||||||
|
r = session.get(url, stream=True)
|
||||||
|
r.raise_for_status()
|
||||||
|
for line in r.iter_lines():
|
||||||
|
line = line.decode('utf8', 'replace')
|
||||||
|
# event lines all start with `data:`
|
||||||
|
# all other lines should be ignored (they will be empty)
|
||||||
|
if line.startswith('data:'):
|
||||||
|
yield json.loads(line.split(':', 1)[1])
|
||||||
|
|
||||||
|
|
||||||
|
def start_server(session, hub_url, user, server_name=""):
|
||||||
|
"""Start a server for a jupyterhub user
|
||||||
|
|
||||||
|
Returns the full URL for accessing the server
|
||||||
|
"""
|
||||||
|
user_url = f"{hub_url}/hub/api/users/{user}"
|
||||||
|
log_name = f"{user}/{server_name}".rstrip("/")
|
||||||
|
|
||||||
|
# step 1: get user status
|
||||||
|
r = session.get(user_url)
|
||||||
|
r.raise_for_status()
|
||||||
|
user_model = r.json()
|
||||||
|
|
||||||
|
# if server is not 'active', request launch
|
||||||
|
if server_name not in user_model.get('servers', {}):
|
||||||
|
log.info(f"Starting server {log_name}")
|
||||||
|
r = session.post(f"{user_url}/servers/{server_name}")
|
||||||
|
r.raise_for_status()
|
||||||
|
if r.status_code == 201:
|
||||||
|
log.info(f"Server {log_name} is launched and ready")
|
||||||
|
elif r.status_code == 202:
|
||||||
|
log.info(f"Server {log_name} is launching...")
|
||||||
|
else:
|
||||||
|
log.warning(f"Unexpected status: {r.status_code}")
|
||||||
|
r = session.get(user_url)
|
||||||
|
r.raise_for_status()
|
||||||
|
user_model = r.json()
|
||||||
|
|
||||||
|
# report server status
|
||||||
|
server = user_model['servers'][server_name]
|
||||||
|
if server['pending']:
|
||||||
|
status = f"pending {server['pending']}"
|
||||||
|
elif server['ready']:
|
||||||
|
status = "ready"
|
||||||
|
else:
|
||||||
|
# shouldn't be possible!
|
||||||
|
raise ValueError(f"Unexpected server state: {server}")
|
||||||
|
|
||||||
|
log.info(f"Server {log_name} is {status}")
|
||||||
|
|
||||||
|
# wait for server to be ready using progress API
|
||||||
|
progress_url = user_model['servers'][server_name]['progress_url']
|
||||||
|
for event in event_stream(session, f"{hub_url}{progress_url}"):
|
||||||
|
log.info(f"Progress {event['progress']}%: {event['message']}")
|
||||||
|
if event.get("ready"):
|
||||||
|
server_url = event['url']
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# server never ready
|
||||||
|
raise ValueError(f"{log_name} never started!")
|
||||||
|
|
||||||
|
# at this point, we know the server is ready and waiting to receive requests
|
||||||
|
# return the full URL where the server can be accessed
|
||||||
|
return f"{hub_url}{server_url}"
|
||||||
|
|
||||||
|
|
||||||
|
def stop_server(session, hub_url, user, server_name=""):
|
||||||
|
"""Stop a server via the JupyterHub API
|
||||||
|
|
||||||
|
Returns when the server has finished stopping
|
||||||
|
"""
|
||||||
|
# step 1: get user status
|
||||||
|
user_url = f"{hub_url}/hub/api/users/{user}"
|
||||||
|
server_url = f"{user_url}/servers/{server_name}"
|
||||||
|
log_name = f"{user}/{server_name}".rstrip("/")
|
||||||
|
|
||||||
|
log.info(f"Stopping server {log_name}")
|
||||||
|
r = session.delete(server_url)
|
||||||
|
if r.status_code == 404:
|
||||||
|
log.info(f"Server {log_name} already stopped")
|
||||||
|
|
||||||
|
r.raise_for_status()
|
||||||
|
if r.status_code == 204:
|
||||||
|
log.info(f"Server {log_name} stopped")
|
||||||
|
return
|
||||||
|
|
||||||
|
# else: 202, stop requested, but not complete
|
||||||
|
# wait for stop to finish
|
||||||
|
log.info(f"Server {log_name} stopping...")
|
||||||
|
|
||||||
|
# wait for server to be done stopping
|
||||||
|
while True:
|
||||||
|
r = session.get(user_url)
|
||||||
|
r.raise_for_status()
|
||||||
|
user_model = r.json()
|
||||||
|
if server_name not in user_model.get("servers", {}):
|
||||||
|
log.info(f"Server {log_name} stopped")
|
||||||
|
return
|
||||||
|
server = user_model["servers"][server_name]
|
||||||
|
if not server['pending']:
|
||||||
|
raise ValueError(f"Waiting for {log_name}, but no longer pending.")
|
||||||
|
log.info(f"Server {log_name} pending: {server['pending']}")
|
||||||
|
# wait to poll again
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Start and stop one server
|
||||||
|
|
||||||
|
Uses test-user and hub from jupyterhub_config.py in this directory
|
||||||
|
"""
|
||||||
|
user = "test-user"
|
||||||
|
hub_url = "http://127.0.0.1:8000"
|
||||||
|
|
||||||
|
session = make_session(get_token())
|
||||||
|
server_url = start_server(session, hub_url, user)
|
||||||
|
r = session.get(f"{server_url}/api/status")
|
||||||
|
r.raise_for_status()
|
||||||
|
log.info(f"Server status: {r.text}")
|
||||||
|
|
||||||
|
stop_server(session, hub_url, user)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
main()
|
Reference in New Issue
Block a user