Skip to content

Instantly share code, notes, and snippets.

@minrk
Created February 17, 2021 09:25
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save minrk/1940060144e7ae2717f57018d4964769 to your computer and use it in GitHub Desktop.
Save minrk/1940060144e7ae2717f57018d4964769 to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Checking for running servers in JupyterHub\n",
"\n",
"As asked [on the Jupyter forum](https://discourse.jupyter.org/t/jupyterhub-rest-api-detect-if-user-already-has-server-running/7805):\n",
"\n",
"> Using the Rest api is there a way to check on whether or not a user has a non named notebook server running"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import requests\n",
"\n",
"# an admin service token added via config\n",
"# or a token owned by the user\n",
"api_token = \"fb605a38c7ed4056b2280ee9bdb25258\"\n",
"\n",
"s = requests.Session()\n",
"s.headers[\"Authorization\"] = f\"token {api_token}\"\n",
"\n",
"username = \"test-1\"\n",
"hub_host = \"http://localhost:8765\"\n",
"hub_api = f\"{hub_host}/hub/api\"\n",
"user_url = f\"{hub_api}/users/{username}\"\n",
"server_url = f\"{hub_api}/users/{username}/server\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The [user model](https://jupyterhub.readthedocs.io/en/latest/_static/rest-api/index.html#/definitions/User) has a `servers` dict of 'active' servers.\n",
"'active' means it is not stopped, i.e. it can be 'ready' or pending a transition from stopped to running or running to stopped.\n",
"\n",
"The first state is: If a server name is not present in the dict, it is definitely not running.\n",
"\n",
"Here we get the user model for a user who has no running servers.\n",
"The `servers` dict is empty."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'kind': 'user',\n",
" 'name': 'test-1',\n",
" 'admin': False,\n",
" 'groups': [],\n",
" 'server': None,\n",
" 'pending': None,\n",
" 'created': '2021-02-17T08:41:53.146076Z',\n",
" 'last_activity': '2021-02-17T09:25:25.506361Z',\n",
" 'servers': {}}"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"r = s.get(user_url)\n",
"r.json()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Start a server\n",
"\n",
"If we start the server and check again immediately,\n",
"the server will be in a \"pending spawn\" state,\n",
"which is in transition from stopped to running.\n",
"\n",
"Note the following state:\n",
"\n",
"- the `servers` dict has a [server record](https://jupyterhub.readthedocs.io/en/latest/_static/rest-api/index.html#/definitions/Server) with key `''`. The empty string is the default server name,\n",
" i.e. the server at `/user/{name}`.\n",
"- The server has a field `'pending': 'spawn'` indicating that it is pending a spawn action\n",
"- The server has `'ready': False` indicating that it is not ready"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'kind': 'user',\n",
" 'name': 'test-1',\n",
" 'admin': False,\n",
" 'groups': [],\n",
" 'server': None,\n",
" 'pending': 'spawn',\n",
" 'created': '2021-02-17T08:41:53.146076Z',\n",
" 'last_activity': '2021-02-17T09:25:25.568339Z',\n",
" 'servers': {'': {'name': '',\n",
" 'last_activity': '2021-02-17T09:25:25.568339Z',\n",
" 'started': '2021-02-17T09:25:25.568339Z',\n",
" 'pending': 'spawn',\n",
" 'ready': False,\n",
" 'state': None,\n",
" 'url': '/user/test-1/',\n",
" 'user_options': {},\n",
" 'progress_url': '/hub/api/users/test-1/server/progress'}}}"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"r = s.post(server_url)\n",
"r.raise_for_status()\n",
"r = s.get(user_url)\n",
"r.json()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Wait for the server to finish starting\n",
"\n",
"We can wait for the server to be ready by following the progress event stream.\n",
"When this finishes, the server will be ready."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'message': 'Server requested', 'progress': 0}\n",
"{'message': 'Spawning server...', 'progress': 50}\n",
"{'html_message': 'Server ready at <a href=\"/user/test-1/\">/user/test-1/</a>',\n",
" 'message': 'Server ready at /user/test-1/',\n",
" 'progress': 100,\n",
" 'ready': True,\n",
" 'url': '/user/test-1/'}\n"
]
}
],
"source": [
"import json\n",
"from pprint import pprint\n",
"\n",
"progress_url = hub_host + r.json()[\"servers\"][\"\"][\"progress_url\"]\n",
"\n",
"for line in s.get(progress_url, stream=True).iter_lines():\n",
" line = line.decode(\"utf8\")\n",
" if line.startswith(\"data:\"):\n",
" rest = line.split(\":\", 1)[1]\n",
" event = json.loads(rest)\n",
" pprint(event)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now that the server is running and ready, we can check the user model again.\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'kind': 'user',\n",
" 'name': 'test-1',\n",
" 'admin': False,\n",
" 'groups': [],\n",
" 'server': '/user/test-1/',\n",
" 'pending': None,\n",
" 'created': '2021-02-17T08:41:53.146076Z',\n",
" 'last_activity': '2021-02-17T09:25:26.775787Z',\n",
" 'servers': {'': {'name': '',\n",
" 'last_activity': '2021-02-17T09:25:26.775787Z',\n",
" 'started': '2021-02-17T09:25:25.568339Z',\n",
" 'pending': None,\n",
" 'ready': True,\n",
" 'state': None,\n",
" 'url': '/user/test-1/',\n",
" 'user_options': {},\n",
" 'progress_url': '/hub/api/users/test-1/server/progress'}}}"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"r = s.get(user_url)\n",
"r.json()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Stopping the server\n",
"\n",
"We can do the same process to stop the server"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'kind': 'user',\n",
" 'name': 'test-1',\n",
" 'admin': False,\n",
" 'groups': [],\n",
" 'server': None,\n",
" 'pending': None,\n",
" 'created': '2021-02-17T08:41:53.146076Z',\n",
" 'last_activity': '2021-02-17T09:25:29.302282Z',\n",
" 'servers': {}}"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"s.delete(server_url)\n",
"r = s.get(user_url)\n",
"r.json()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To summarize:\n",
" \n",
"- A [user](https://jupyterhub.readthedocs.io/en/latest/_static/rest-api/index.html#/definitions/User)'s servers dict contains all \"active\" servers, keyed by server name\n",
"- `\"\"` is the key for the default server (no name)\n",
"- Each [server dict](https://jupyterhub.readthedocs.io/en/latest/_static/rest-api/index.html#/definitions/Server) has\n",
" - a `ready` boolean field that is True if the server is running and ready to handle requests, False otherwise\n",
" - a `pending` field that is None if no transitions are pending, or a string describing which transition is pending (i.e. 'spawn' or 'stop').\n",
"\n",
"What you do with `pending` will depend on why you are asking.\n",
"Most API requests on a server will raise 400 if you try to act on a server that's pending some transition, so you may need to wait if you are asking because you want to do something like start a server if it's not already running.\n",
"\n",
"If a server is ready *or* `pending == 'spawn'`, it's safe to redirect users to the server's URL\n",
"because JupyterHub will send them to the spawn-pending page and ultimately redirect when it's ready.\n",
"\n",
"If you have a reason to wait for a server to finish a pending spawn via the API, the progress event stream API works.\n",
"\n",
"A server pending 'stop' can't be visited and can't be restarted until the stop event is complete.\n",
"\n",
"To retrieve the current status of the user's default server or any named server:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"test-1/ not running\n"
]
}
],
"source": [
"username = \"test-1\"\n",
"servername = \"\" # empty for default, or pick any name\n",
"\n",
"user_model = s.get(f\"{hub_api}/users/{username}\").json()\n",
"server = user_model[\"servers\"].get(servername, {})\n",
"if server:\n",
" pending = server[\"pending\"]\n",
" ready = server[\"ready\"]\n",
" # server.url is a *path*, does not include https://host unless user-subdomains are used\n",
" url = server[\"url\"]\n",
"else:\n",
" # no server field, not running, nothing pending\n",
" pending = None\n",
" ready = False\n",
" url = None\n",
"\n",
"if ready:\n",
" print(f\"{username}/{servername} running and ready at {url}\")\n",
"elif pending:\n",
" print(f\"{username}/{servername} pending {pending}\")\n",
"else:\n",
" print(f\"{username}/{servername} not running\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.6"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment