mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-18 15:33:02 +00:00
Merge remote-tracking branch 'upstream/master' into docker-host
This commit is contained in:
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
@@ -51,6 +51,13 @@ jobs:
|
||||
print("OK")
|
||||
EOF
|
||||
|
||||
# ref: https://github.com/actions/upload-artifact#readme
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: jupyterhub-${{ github.sha }}
|
||||
path: "dist/*"
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Publish to PyPI
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
env:
|
||||
|
@@ -1,10 +1,7 @@
|
||||
Eventlogging and Telemetry
|
||||
==========================
|
||||
|
||||
JupyterHub can be configured to record structured events from a running server using Jupyter's `Telemetry System`_. The types of events that JupyterHub emits are defined by `JSON schemas`_ listed below_
|
||||
|
||||
emitted as JSON data, defined and validated by the JSON schemas listed below.
|
||||
|
||||
JupyterHub can be configured to record structured events from a running server using Jupyter's `Telemetry System`_. The types of events that JupyterHub emits are defined by `JSON schemas`_ listed at the bottom of this page_.
|
||||
|
||||
.. _logging: https://docs.python.org/3/library/logging.html
|
||||
.. _`Telemetry System`: https://github.com/jupyter/telemetry
|
||||
@@ -38,13 +35,12 @@ Here's a basic example:
|
||||
The output is a file, ``"event.log"``, with events recorded as JSON data.
|
||||
|
||||
|
||||
|
||||
.. _below:
|
||||
.. _page:
|
||||
|
||||
Event schemas
|
||||
-------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:maxdepth: 2
|
||||
|
||||
server-actions.rst
|
||||
server-actions.rst
|
||||
|
@@ -179,3 +179,13 @@ The number of named servers per user can be limited by setting
|
||||
```python
|
||||
c.JupyterHub.named_server_limit_per_user = 5
|
||||
```
|
||||
|
||||
## Switching to Jupyter Server
|
||||
|
||||
[Jupyter Server](https://jupyter-server.readthedocs.io/en/latest/) is a new Tornado Server backend for Jupyter web applications (e.g. JupyterLab 3.0 uses this package as its default backend).
|
||||
|
||||
By default, the single-user notebook server uses the (old) `NotebookApp` from the [notebook](https://github.com/jupyter/notebook) package. You can switch to using Jupyter Server's `ServerApp` backend (this will likely become the default in future releases) by setting the `JUPYTERHUB_SINGLEUSER_APP` environment variable to:
|
||||
|
||||
```bash
|
||||
export JUPYTERHUB_SINGLEUSER_APP='jupyter_server.serverapp.ServerApp'
|
||||
```
|
||||
|
@@ -237,6 +237,13 @@ class UserAPIHandler(APIHandler):
|
||||
)
|
||||
|
||||
await maybe_future(self.authenticator.delete_user(user))
|
||||
|
||||
# allow the spawner to cleanup any persistent resources associated with the user
|
||||
try:
|
||||
await user.spawner.delete_forever()
|
||||
except Exception as e:
|
||||
self.log.error("Error cleaning up persistent resources: %s" % e)
|
||||
|
||||
# remove from registry
|
||||
self.users.delete(user)
|
||||
|
||||
|
@@ -374,7 +374,8 @@ class JupyterHub(Application):
|
||||
300, help="Interval (in seconds) at which to update last-activity timestamps."
|
||||
).tag(config=True)
|
||||
proxy_check_interval = Integer(
|
||||
30, help="Interval (in seconds) at which to check if the proxy is running."
|
||||
5,
|
||||
help="DEPRECATED since version 0.8: Use ConfigurableHTTPProxy.check_running_interval",
|
||||
).tag(config=True)
|
||||
service_check_interval = Integer(
|
||||
60,
|
||||
@@ -688,6 +689,7 @@ class JupyterHub(Application):
|
||||
).tag(config=True)
|
||||
|
||||
_proxy_config_map = {
|
||||
'proxy_check_interval': 'check_running_interval',
|
||||
'proxy_cmd': 'command',
|
||||
'debug_proxy': 'debug',
|
||||
'proxy_auth_token': 'auth_token',
|
||||
@@ -846,15 +848,30 @@ class JupyterHub(Application):
|
||||
to reduce the cost of checking authentication tokens.
|
||||
""",
|
||||
).tag(config=True)
|
||||
cookie_secret = Bytes(
|
||||
cookie_secret = Union(
|
||||
[Bytes(), Unicode()],
|
||||
help="""The cookie secret to use to encrypt cookies.
|
||||
|
||||
Loaded from the JPY_COOKIE_SECRET env variable by default.
|
||||
|
||||
Should be exactly 256 bits (32 bytes).
|
||||
"""
|
||||
""",
|
||||
).tag(config=True, env='JPY_COOKIE_SECRET')
|
||||
|
||||
@validate('cookie_secret')
|
||||
def _validate_secret_key(self, proposal):
|
||||
"""Coerces strings with even number of hexadecimal characters to bytes."""
|
||||
r = proposal['value']
|
||||
if isinstance(r, str):
|
||||
try:
|
||||
return bytes.fromhex(r)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
"cookie_secret set as a string must contain an even amount of hexadecimal characters."
|
||||
)
|
||||
else:
|
||||
return r
|
||||
|
||||
@observe('cookie_secret')
|
||||
def _cookie_secret_check(self, change):
|
||||
secret = change.new
|
||||
|
@@ -1101,6 +1101,13 @@ class PAMAuthenticator(LocalAuthenticator):
|
||||
else:
|
||||
return super().normalize_username(username)
|
||||
|
||||
def get_custom_html(self, base_url):
|
||||
"""Get custom HTML for the authenticator.
|
||||
|
||||
.. versionadded: 1.4
|
||||
"""
|
||||
return self.custom_html
|
||||
|
||||
|
||||
for _old_name, _new_name, _version in [
|
||||
("check_group_whitelist", "check_group_allowed", "1.2"),
|
||||
|
@@ -493,6 +493,11 @@ class BaseHandler(RequestHandler):
|
||||
path=url_path_join(self.base_url, 'services'),
|
||||
**kwargs,
|
||||
)
|
||||
# clear tornado cookie
|
||||
self.clear_cookie(
|
||||
'_xsrf',
|
||||
**self.settings.get('xsrf_cookie_kwargs', {}),
|
||||
)
|
||||
# Reset _jupyterhub_user
|
||||
self._jupyterhub_user = None
|
||||
|
||||
|
@@ -3,6 +3,7 @@
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import asyncio
|
||||
|
||||
from jinja2 import Template
|
||||
from tornado import web
|
||||
from tornado.escape import url_escape
|
||||
from tornado.httputil import url_concat
|
||||
@@ -90,17 +91,23 @@ class LoginHandler(BaseHandler):
|
||||
"""Render the login page."""
|
||||
|
||||
def _render(self, login_error=None, username=None):
|
||||
return self.render_template(
|
||||
'login.html',
|
||||
next=url_escape(self.get_argument('next', default='')),
|
||||
username=username,
|
||||
login_error=login_error,
|
||||
custom_html=self.authenticator.custom_html,
|
||||
login_url=self.settings['login_url'],
|
||||
authenticator_login_url=url_concat(
|
||||
context = {
|
||||
"next": url_escape(self.get_argument('next', default='')),
|
||||
"username": username,
|
||||
"login_error": login_error,
|
||||
"login_url": self.settings['login_url'],
|
||||
"authenticator_login_url": url_concat(
|
||||
self.authenticator.login_url(self.hub.base_url),
|
||||
{'next': self.get_argument('next', '')},
|
||||
),
|
||||
}
|
||||
custom_html = Template(
|
||||
self.authenticator.get_custom_html(self.hub.base_url)
|
||||
).render(**context)
|
||||
return self.render_template(
|
||||
'login.html',
|
||||
**context,
|
||||
custom_html=custom_html,
|
||||
)
|
||||
|
||||
async def get(self):
|
||||
|
@@ -501,20 +501,24 @@ class AdminHandler(BaseHandler):
|
||||
# get User.col.desc() order objects
|
||||
ordered = [getattr(c, o)() for c, o in zip(cols, orders)]
|
||||
|
||||
query = self.db.query(orm.User).outerjoin(orm.Spawner).distinct(orm.User.id)
|
||||
subquery = query.subquery("users")
|
||||
users = (
|
||||
self.db.query(orm.User)
|
||||
.select_entity_from(subquery)
|
||||
.outerjoin(orm.Spawner)
|
||||
.order_by(*ordered)
|
||||
.limit(per_page)
|
||||
.offset(offset)
|
||||
)
|
||||
|
||||
users = [self._user_from_orm(u) for u in users]
|
||||
|
||||
running = []
|
||||
for u in users:
|
||||
running.extend(s for s in u.spawners.values() if s.active)
|
||||
|
||||
pagination.total = self.db.query(orm.User.id).count()
|
||||
pagination.total = query.count()
|
||||
|
||||
auth_state = await self.current_user.get_auth_state()
|
||||
html = await self.render_template(
|
||||
|
@@ -2,7 +2,9 @@
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import json
|
||||
import logging
|
||||
import traceback
|
||||
from functools import partial
|
||||
from http.cookies import SimpleCookie
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlunparse
|
||||
@@ -132,19 +134,25 @@ def log_request(handler):
|
||||
status < 300 and isinstance(handler, (StaticFileHandler, HealthCheckHandler))
|
||||
):
|
||||
# static-file success and 304 Found are debug-level
|
||||
log_method = access_log.debug
|
||||
log_level = logging.DEBUG
|
||||
elif status < 400:
|
||||
log_method = access_log.info
|
||||
log_level = logging.INFO
|
||||
elif status < 500:
|
||||
log_method = access_log.warning
|
||||
log_level = logging.WARNING
|
||||
else:
|
||||
log_method = access_log.error
|
||||
log_level = logging.ERROR
|
||||
|
||||
uri = _scrub_uri(request.uri)
|
||||
headers = _scrub_headers(request.headers)
|
||||
|
||||
request_time = 1000.0 * handler.request.request_time()
|
||||
|
||||
# always log slow responses (longer than 1s) at least info-level
|
||||
if request_time >= 1000 and log_level < logging.INFO:
|
||||
log_level = logging.INFO
|
||||
|
||||
log_method = partial(access_log.log, log_level)
|
||||
|
||||
try:
|
||||
user = handler.current_user
|
||||
except (HTTPError, RuntimeError):
|
||||
|
@@ -450,7 +450,11 @@ class ConfigurableHTTPProxy(Proxy):
|
||||
Loaded from the CONFIGPROXY_AUTH_TOKEN env variable by default.
|
||||
"""
|
||||
).tag(config=True)
|
||||
check_running_interval = Integer(5, config=True)
|
||||
check_running_interval = Integer(
|
||||
5,
|
||||
help="Interval (in seconds) at which to check if the proxy is running.",
|
||||
config=True,
|
||||
)
|
||||
|
||||
@default('auth_token')
|
||||
def _auth_token_default(self):
|
||||
|
@@ -93,6 +93,16 @@ class MockSpawner(SimpleLocalProcessSpawner):
|
||||
def _cmd_default(self):
|
||||
return [sys.executable, '-m', 'jupyterhub.tests.mocksu']
|
||||
|
||||
async def delete_forever(self):
|
||||
"""Called when a user is deleted.
|
||||
|
||||
This can do things like request removal of resources such as persistent storage.
|
||||
Only called on stopped spawners, and is likely the last action ever taken for the user.
|
||||
|
||||
Will only be called once on the user's default Spawner.
|
||||
"""
|
||||
pass
|
||||
|
||||
use_this_api_token = None
|
||||
|
||||
def start(self):
|
||||
|
@@ -199,6 +199,18 @@ def test_cookie_secret_env(tmpdir, request):
|
||||
assert not os.path.exists(hub.cookie_secret_file)
|
||||
|
||||
|
||||
def test_cookie_secret_string_():
|
||||
cfg = Config()
|
||||
|
||||
cfg.JupyterHub.cookie_secret = "not hex"
|
||||
with pytest.raises(ValueError):
|
||||
JupyterHub(config=cfg)
|
||||
|
||||
cfg.JupyterHub.cookie_secret = "abc123"
|
||||
app = JupyterHub(config=cfg)
|
||||
assert app.cookie_secret == binascii.a2b_hex('abc123')
|
||||
|
||||
|
||||
async def test_load_groups(tmpdir, request):
|
||||
to_load = {
|
||||
'blue': ['cyclops', 'rogue', 'wolverine'],
|
||||
|
@@ -121,7 +121,7 @@ def auth_header(db, name):
|
||||
"""Return header with user's API authorization token."""
|
||||
user = find_user(db, name)
|
||||
if user is None:
|
||||
user = add_user(db, name=name)
|
||||
raise KeyError(f"No such user: {name}")
|
||||
token = user.new_api_token()
|
||||
return {'Authorization': 'token %s' % token}
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
alembic
|
||||
alembic>=1.4
|
||||
async_generator>=1.9
|
||||
certipy>=0.1.2
|
||||
entrypoints
|
||||
|
@@ -130,7 +130,9 @@
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Services<span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
{% for service in services %}
|
||||
{% block service scoped %}
|
||||
<li><a class="dropdown-item" href="{{service.prefix}}">{{service.name}}</a></li>
|
||||
{% endblock %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
@@ -144,7 +146,7 @@
|
||||
{% block login_widget %}
|
||||
<span id="login_widget">
|
||||
{% if user %}
|
||||
<p class="navbar-text">{{user.name}}</p>
|
||||
<p class="navbar-text">{{user.name}}</p>
|
||||
<a id="logout" role="button" class="navbar-btn btn-sm btn btn-default" href="{{logout_url}}"> <i aria-hidden="true" class="fa fa-sign-out"></i> Logout</a>
|
||||
{% else %}
|
||||
<a id="login" role="button" class="btn-sm btn navbar-btn btn-default" href="{{login_url}}">Login</a>
|
||||
|
Reference in New Issue
Block a user