mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-15 22:13:00 +00:00
Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f7903a78d1 | ||
![]() |
edddacec1d | ||
![]() |
c5a33f227f | ||
![]() |
aaa20e5787 | ||
![]() |
c8ebf1ec90 | ||
![]() |
89745c002b | ||
![]() |
3e0ee49ce8 | ||
![]() |
bbbeffb443 | ||
![]() |
c29a5ca4ce | ||
![]() |
f5182fe349 |
@@ -4,14 +4,14 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: reorder-python-imports
|
- id: reorder-python-imports
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 20.8b1
|
rev: 22.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
rev: v2.2.1
|
rev: v2.2.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
- repo: https://gitlab.com/pycqa/flake8
|
- repo: https://github.com/pycqa/flake8
|
||||||
rev: "3.8.4"
|
rev: "3.8.4"
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
|
@@ -3,7 +3,7 @@ swagger: "2.0"
|
|||||||
info:
|
info:
|
||||||
title: JupyterHub
|
title: JupyterHub
|
||||||
description: The REST API for JupyterHub
|
description: The REST API for JupyterHub
|
||||||
version: 1.5.0
|
version: 1.5.1
|
||||||
license:
|
license:
|
||||||
name: BSD-3-Clause
|
name: BSD-3-Clause
|
||||||
schemes: [http, https]
|
schemes: [http, https]
|
||||||
|
@@ -17,6 +17,22 @@ A few fully backward-compatible features have been backported from 2.0.
|
|||||||
|
|
||||||
[ghsa-cw7p-q79f-m2v7]: https://github.com/jupyterhub/jupyterhub/security/advisories/GHSA-cw7p-q79f-m2v7
|
[ghsa-cw7p-q79f-m2v7]: https://github.com/jupyterhub/jupyterhub/security/advisories/GHSA-cw7p-q79f-m2v7
|
||||||
|
|
||||||
|
### 1.5.1 2022-12-01
|
||||||
|
|
||||||
|
This is a patch release, improving db resiliency when certain errors occur, without requiring a jupyterhub restart.
|
||||||
|
|
||||||
|
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/1.5.0...1.5.1))
|
||||||
|
|
||||||
|
#### Merged PRs
|
||||||
|
|
||||||
|
- Backport db rollback fixes to 1.x [#4076](https://github.com/jupyterhub/jupyterhub/pull/4076) ([@mriedem](https://github.com/mriedem), [@minrk](https://github.com/minrk)), [@nsshah1288](https://github.com/nsshah1288)
|
||||||
|
|
||||||
|
#### Contributors to this release
|
||||||
|
|
||||||
|
([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2022-09-09&to=2022-11-30&type=c))
|
||||||
|
|
||||||
|
[@mriedem](https://github.com/mriedem) | [@minrk](https://github.com/minrk) | [@nsshah1288](https://github.com/nsshah1288)
|
||||||
|
|
||||||
### [1.5.0] 2021-11-04
|
### [1.5.0] 2021-11-04
|
||||||
|
|
||||||
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/1.4.2...1.5.0))
|
([full changelog](https://github.com/jupyterhub/jupyterhub/compare/1.4.2...1.5.0))
|
||||||
|
@@ -10,7 +10,7 @@ from jupyter_client.localinterfaces import public_ips
|
|||||||
|
|
||||||
|
|
||||||
def create_dir_hook(spawner):
|
def create_dir_hook(spawner):
|
||||||
""" Create directory """
|
"""Create directory"""
|
||||||
username = spawner.user.name # get the username
|
username = spawner.user.name # get the username
|
||||||
volume_path = os.path.join('/volumes/jupyterhub', username)
|
volume_path = os.path.join('/volumes/jupyterhub', username)
|
||||||
if not os.path.exists(volume_path):
|
if not os.path.exists(volume_path):
|
||||||
@@ -20,7 +20,7 @@ def create_dir_hook(spawner):
|
|||||||
|
|
||||||
|
|
||||||
def clean_dir_hook(spawner):
|
def clean_dir_hook(spawner):
|
||||||
""" Delete directory """
|
"""Delete directory"""
|
||||||
username = spawner.user.name # get the username
|
username = spawner.user.name # get the username
|
||||||
temp_path = os.path.join('/volumes/jupyterhub', username, 'temp')
|
temp_path = os.path.join('/volumes/jupyterhub', username, 'temp')
|
||||||
if os.path.exists(temp_path) and os.path.isdir(temp_path):
|
if os.path.exists(temp_path) and os.path.isdir(temp_path):
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
version_info = (
|
version_info = (
|
||||||
1,
|
1,
|
||||||
5,
|
5,
|
||||||
0,
|
1,
|
||||||
"", # release (b1, rc1, or "" for final or dev)
|
"", # release (b1, rc1, or "" for final or dev)
|
||||||
# "dev", # dev or nothing for beta/rc/stable releases
|
# "dev", # dev or nothing for beta/rc/stable releases
|
||||||
)
|
)
|
||||||
|
@@ -42,7 +42,7 @@ class GroupListAPIHandler(_GroupAPIHandler):
|
|||||||
|
|
||||||
@admin_only
|
@admin_only
|
||||||
async def post(self):
|
async def post(self):
|
||||||
"""POST creates Multiple groups """
|
"""POST creates Multiple groups"""
|
||||||
model = self.get_json_body()
|
model = self.get_json_body()
|
||||||
if not model or not isinstance(model, dict) or not model.get('groups'):
|
if not model or not isinstance(model, dict) or not model.get('groups'):
|
||||||
raise web.HTTPError(400, "Must specify at least one group to create")
|
raise web.HTTPError(400, "Must specify at least one group to create")
|
||||||
|
@@ -5,7 +5,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import atexit
|
import atexit
|
||||||
import binascii
|
import binascii
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -90,6 +89,7 @@ from .pagination import Pagination
|
|||||||
from .proxy import Proxy, ConfigurableHTTPProxy
|
from .proxy import Proxy, ConfigurableHTTPProxy
|
||||||
from .traitlets import URLPrefix, Command, EntryPointType, Callable
|
from .traitlets import URLPrefix, Command, EntryPointType, Callable
|
||||||
from .utils import (
|
from .utils import (
|
||||||
|
catch_db_error,
|
||||||
maybe_future,
|
maybe_future,
|
||||||
url_path_join,
|
url_path_join,
|
||||||
print_stacks,
|
print_stacks,
|
||||||
@@ -2000,6 +2000,7 @@ class JupyterHub(Application):
|
|||||||
# purge expired tokens hourly
|
# purge expired tokens hourly
|
||||||
purge_expired_tokens_interval = 3600
|
purge_expired_tokens_interval = 3600
|
||||||
|
|
||||||
|
@catch_db_error
|
||||||
def purge_expired_tokens(self):
|
def purge_expired_tokens(self):
|
||||||
"""purge all expiring token objects from the database
|
"""purge all expiring token objects from the database
|
||||||
|
|
||||||
@@ -2015,7 +2016,7 @@ class JupyterHub(Application):
|
|||||||
await self._add_tokens(self.service_tokens, kind='service')
|
await self._add_tokens(self.service_tokens, kind='service')
|
||||||
await self._add_tokens(self.api_tokens, kind='user')
|
await self._add_tokens(self.api_tokens, kind='user')
|
||||||
|
|
||||||
self.purge_expired_tokens()
|
await self.purge_expired_tokens()
|
||||||
# purge expired tokens hourly
|
# purge expired tokens hourly
|
||||||
# we don't need to be prompt about this
|
# we don't need to be prompt about this
|
||||||
# because expired tokens cannot be used anyway
|
# because expired tokens cannot be used anyway
|
||||||
@@ -2663,6 +2664,7 @@ class JupyterHub(Application):
|
|||||||
with open(self.config_file, mode='w') as f:
|
with open(self.config_file, mode='w') as f:
|
||||||
f.write(config_text)
|
f.write(config_text)
|
||||||
|
|
||||||
|
@catch_db_error
|
||||||
async def update_last_activity(self):
|
async def update_last_activity(self):
|
||||||
"""Update User.last_activity timestamps from the proxy"""
|
"""Update User.last_activity timestamps from the proxy"""
|
||||||
routes = await self.proxy.get_all_routes()
|
routes = await self.proxy.get_all_routes()
|
||||||
|
@@ -81,9 +81,14 @@ class BaseHandler(RequestHandler):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
await self.get_current_user()
|
await self.get_current_user()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
self.log.exception("Failed to get current user")
|
# ensure get_current_user is never called again for this handler,
|
||||||
|
# since it failed
|
||||||
self._jupyterhub_user = None
|
self._jupyterhub_user = None
|
||||||
|
self.log.exception("Failed to get current user")
|
||||||
|
if isinstance(e, SQLAlchemyError):
|
||||||
|
self.log.error("Rolling back session due to database error")
|
||||||
|
self.db.rollback()
|
||||||
|
|
||||||
return await maybe_future(super().prepare())
|
return await maybe_future(super().prepare())
|
||||||
|
|
||||||
@@ -426,7 +431,8 @@ class BaseHandler(RequestHandler):
|
|||||||
except Exception:
|
except Exception:
|
||||||
# don't let errors here raise more than once
|
# don't let errors here raise more than once
|
||||||
self._jupyterhub_user = None
|
self._jupyterhub_user = None
|
||||||
self.log.exception("Error getting current user")
|
# but still raise, which will get handled in .prepare()
|
||||||
|
raise
|
||||||
return self._jupyterhub_user
|
return self._jupyterhub_user
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1499,14 +1505,10 @@ class UserUrlHandler(BaseHandler):
|
|||||||
|
|
||||||
# if request is expecting JSON, assume it's an API request and fail with 503
|
# if request is expecting JSON, assume it's an API request and fail with 503
|
||||||
# because it won't like the redirect to the pending page
|
# because it won't like the redirect to the pending page
|
||||||
if (
|
if get_accepted_mimetype(
|
||||||
get_accepted_mimetype(
|
self.request.headers.get('Accept', ''),
|
||||||
self.request.headers.get('Accept', ''),
|
choices=['application/json', 'text/html'],
|
||||||
choices=['application/json', 'text/html'],
|
) == 'application/json' or 'api' in user_path.split('/'):
|
||||||
)
|
|
||||||
== 'application/json'
|
|
||||||
or 'api' in user_path.split('/')
|
|
||||||
):
|
|
||||||
self._fail_api_request(user_name, server_name)
|
self._fail_api_request(user_name, server_name)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1588,7 +1590,7 @@ class UserUrlHandler(BaseHandler):
|
|||||||
if redirects:
|
if redirects:
|
||||||
self.log.warning("Redirect loop detected on %s", self.request.uri)
|
self.log.warning("Redirect loop detected on %s", self.request.uri)
|
||||||
# add capped exponential backoff where cap is 10s
|
# add capped exponential backoff where cap is 10s
|
||||||
await asyncio.sleep(min(1 * (2 ** redirects), 10))
|
await asyncio.sleep(min(1 * (2**redirects), 10))
|
||||||
# rewrite target url with new `redirects` query value
|
# rewrite target url with new `redirects` query value
|
||||||
url_parts = urlparse(target)
|
url_parts = urlparse(target)
|
||||||
query_parts = parse_qs(url_parts.query)
|
query_parts = parse_qs(url_parts.query)
|
||||||
|
@@ -223,7 +223,7 @@ class User(Base):
|
|||||||
|
|
||||||
|
|
||||||
class Spawner(Base):
|
class Spawner(Base):
|
||||||
""""State about a Spawner"""
|
""" "State about a Spawner"""
|
||||||
|
|
||||||
__tablename__ = 'spawners'
|
__tablename__ = 'spawners'
|
||||||
|
|
||||||
|
@@ -452,7 +452,7 @@ class SingleUserNotebookAppMixin(Configurable):
|
|||||||
i,
|
i,
|
||||||
RETRIES,
|
RETRIES,
|
||||||
)
|
)
|
||||||
await asyncio.sleep(min(2 ** i, 16))
|
await asyncio.sleep(min(2**i, 16))
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
@@ -4,9 +4,9 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import errno
|
import errno
|
||||||
|
import functools
|
||||||
import hashlib
|
import hashlib
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
|
||||||
import random
|
import random
|
||||||
import secrets
|
import secrets
|
||||||
import socket
|
import socket
|
||||||
@@ -22,6 +22,7 @@ from hmac import compare_digest
|
|||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
|
||||||
from async_generator import aclosing
|
from async_generator import aclosing
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
from tornado import ioloop
|
from tornado import ioloop
|
||||||
from tornado import web
|
from tornado import web
|
||||||
from tornado.httpclient import AsyncHTTPClient
|
from tornado.httpclient import AsyncHTTPClient
|
||||||
@@ -642,3 +643,21 @@ def get_accepted_mimetype(accept_header, choices=None):
|
|||||||
else:
|
else:
|
||||||
return mime
|
return mime
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def catch_db_error(f):
|
||||||
|
"""Catch and rollback database errors"""
|
||||||
|
|
||||||
|
@functools.wraps(f)
|
||||||
|
async def catching(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
r = f(self, *args, **kwargs)
|
||||||
|
if inspect.isawaitable(r):
|
||||||
|
r = await r
|
||||||
|
except SQLAlchemyError:
|
||||||
|
self.log.exception("Rolling back session due to database error")
|
||||||
|
self.db.rollback()
|
||||||
|
else:
|
||||||
|
return r
|
||||||
|
|
||||||
|
return catching
|
||||||
|
@@ -26,8 +26,8 @@ import os
|
|||||||
import pipes
|
import pipes
|
||||||
import shutil
|
import shutil
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from distutils.version import LooseVersion as V
|
|
||||||
|
|
||||||
|
from distutils.version import LooseVersion as V
|
||||||
from invoke import run as invoke_run
|
from invoke import run as invoke_run
|
||||||
from invoke import task
|
from invoke import task
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user