Files
jupyterhub/examples/server-api/start-stop-server.py
Min RK b1b6a9e76c Add detailed doc for starting/stopping/waiting for servers via api
and complete implementation in examples/server-api
2021-08-04 12:49:12 +02:00

174 lines
5.1 KiB
Python

#!/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()