mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-14 13:33:00 +00:00
Merge branch 'jupyterhub:main' into main
This commit is contained in:
2
.github/workflows/test-docs.yml
vendored
2
.github/workflows/test-docs.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Validate REST API definition
|
- name: Validate REST API definition
|
||||||
uses: char0n/swagger-editor-validate@v1.3.1
|
uses: char0n/swagger-editor-validate@v1.3.2
|
||||||
with:
|
with:
|
||||||
definition-file: docs/source/_static/rest-api.yml
|
definition-file: docs/source/_static/rest-api.yml
|
||||||
|
|
||||||
|
@@ -183,12 +183,28 @@ as well as the admin page:
|
|||||||
Named servers can be accessed, created, started, stopped, and deleted
|
Named servers can be accessed, created, started, stopped, and deleted
|
||||||
from these pages. Activity tracking is now per-server as well.
|
from these pages. Activity tracking is now per-server as well.
|
||||||
|
|
||||||
The number of named servers per user can be limited by setting
|
The number of named servers per user can be limited by setting a constant value:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
c.JupyterHub.named_server_limit_per_user = 5
|
c.JupyterHub.named_server_limit_per_user = 5
|
||||||
```
|
```
|
||||||
|
|
||||||
|
or a callable/awaitable based on the handler object:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def named_server_limit_per_user_fn(handler):
|
||||||
|
user = handler.current_user
|
||||||
|
if user and user.admin:
|
||||||
|
return 0
|
||||||
|
return 5
|
||||||
|
|
||||||
|
c.JupyterHub.named_server_limit_per_user = named_server_limit_per_user_fn
|
||||||
|
```
|
||||||
|
|
||||||
|
This can be useful for quota service implementations. The example above limits the number of named servers for non-admin users only.
|
||||||
|
|
||||||
|
If `named_server_limit_per_user` is set to `0`, no limit is enforced.
|
||||||
|
|
||||||
(classic-notebook-ui)=
|
(classic-notebook-ui)=
|
||||||
|
|
||||||
## Switching back to classic notebook
|
## Switching back to classic notebook
|
||||||
|
@@ -502,17 +502,19 @@ class UserServerAPIHandler(APIHandler):
|
|||||||
if server_name:
|
if server_name:
|
||||||
if not self.allow_named_servers:
|
if not self.allow_named_servers:
|
||||||
raise web.HTTPError(400, "Named servers are not enabled.")
|
raise web.HTTPError(400, "Named servers are not enabled.")
|
||||||
if (
|
|
||||||
self.named_server_limit_per_user > 0
|
named_server_limit_per_user = (
|
||||||
and server_name not in user.orm_spawners
|
await self.get_current_user_named_server_limit()
|
||||||
):
|
)
|
||||||
|
|
||||||
|
if named_server_limit_per_user > 0 and server_name not in user.orm_spawners:
|
||||||
named_spawners = list(user.all_spawners(include_default=False))
|
named_spawners = list(user.all_spawners(include_default=False))
|
||||||
if self.named_server_limit_per_user <= len(named_spawners):
|
if named_server_limit_per_user <= len(named_spawners):
|
||||||
raise web.HTTPError(
|
raise web.HTTPError(
|
||||||
400,
|
400,
|
||||||
"User {} already has the maximum of {} named servers."
|
"User {} already has the maximum of {} named servers."
|
||||||
" One must be deleted before a new server can be created".format(
|
" One must be deleted before a new server can be created".format(
|
||||||
user_name, self.named_server_limit_per_user
|
user_name, named_server_limit_per_user
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
spawner = user.get_spawner(server_name, replace_failed=True)
|
spawner = user.get_spawner(server_name, replace_failed=True)
|
||||||
|
@@ -1150,14 +1150,27 @@ class JupyterHub(Application):
|
|||||||
False, help="Allow named single-user servers per user"
|
False, help="Allow named single-user servers per user"
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
named_server_limit_per_user = Integer(
|
named_server_limit_per_user = Union(
|
||||||
0,
|
[Integer(), Callable()],
|
||||||
|
default_value=0,
|
||||||
help="""
|
help="""
|
||||||
Maximum number of concurrent named servers that can be created by a user at a time.
|
Maximum number of concurrent named servers that can be created by a user at a time.
|
||||||
|
|
||||||
Setting this can limit the total resources a user can consume.
|
Setting this can limit the total resources a user can consume.
|
||||||
|
|
||||||
If set to 0, no limit is enforced.
|
If set to 0, no limit is enforced.
|
||||||
|
|
||||||
|
Can be an integer or a callable/awaitable based on the handler object:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
def named_server_limit_per_user_fn(handler):
|
||||||
|
user = handler.current_user
|
||||||
|
if user and user.admin:
|
||||||
|
return 0
|
||||||
|
return 5
|
||||||
|
|
||||||
|
c.JupyterHub.named_server_limit_per_user = named_server_limit_per_user_fn
|
||||||
""",
|
""",
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
|
@@ -248,6 +248,17 @@ class BaseHandler(RequestHandler):
|
|||||||
def authenticate_prometheus(self):
|
def authenticate_prometheus(self):
|
||||||
return self.settings.get('authenticate_prometheus', True)
|
return self.settings.get('authenticate_prometheus', True)
|
||||||
|
|
||||||
|
async def get_current_user_named_server_limit(self):
|
||||||
|
"""
|
||||||
|
Return named server limit for current user.
|
||||||
|
"""
|
||||||
|
named_server_limit_per_user = self.named_server_limit_per_user
|
||||||
|
|
||||||
|
if callable(named_server_limit_per_user):
|
||||||
|
return await maybe_future(named_server_limit_per_user(self))
|
||||||
|
|
||||||
|
return named_server_limit_per_user
|
||||||
|
|
||||||
def get_auth_token(self):
|
def get_auth_token(self):
|
||||||
"""Get the authorization token from Authorization header"""
|
"""Get the authorization token from Authorization header"""
|
||||||
auth_header = self.request.headers.get('Authorization', '')
|
auth_header = self.request.headers.get('Authorization', '')
|
||||||
|
@@ -72,7 +72,7 @@ class HomeHandler(BaseHandler):
|
|||||||
user=user,
|
user=user,
|
||||||
url=url,
|
url=url,
|
||||||
allow_named_servers=self.allow_named_servers,
|
allow_named_servers=self.allow_named_servers,
|
||||||
named_server_limit_per_user=self.named_server_limit_per_user,
|
named_server_limit_per_user=await self.get_current_user_named_server_limit(),
|
||||||
url_path_join=url_path_join,
|
url_path_join=url_path_join,
|
||||||
# can't use user.spawners because the stop method of User pops named servers from user.spawners when they're stopped
|
# can't use user.spawners because the stop method of User pops named servers from user.spawners when they're stopped
|
||||||
spawners=user.orm_user._orm_spawners,
|
spawners=user.orm_user._orm_spawners,
|
||||||
@@ -129,17 +129,19 @@ class SpawnHandler(BaseHandler):
|
|||||||
if server_name:
|
if server_name:
|
||||||
if not self.allow_named_servers:
|
if not self.allow_named_servers:
|
||||||
raise web.HTTPError(400, "Named servers are not enabled.")
|
raise web.HTTPError(400, "Named servers are not enabled.")
|
||||||
if (
|
|
||||||
self.named_server_limit_per_user > 0
|
named_server_limit_per_user = (
|
||||||
and server_name not in user.orm_spawners
|
await self.get_current_user_named_server_limit()
|
||||||
):
|
)
|
||||||
|
|
||||||
|
if named_server_limit_per_user > 0 and server_name not in user.orm_spawners:
|
||||||
named_spawners = list(user.all_spawners(include_default=False))
|
named_spawners = list(user.all_spawners(include_default=False))
|
||||||
if self.named_server_limit_per_user <= len(named_spawners):
|
if named_server_limit_per_user <= len(named_spawners):
|
||||||
raise web.HTTPError(
|
raise web.HTTPError(
|
||||||
400,
|
400,
|
||||||
"User {} already has the maximum of {} named servers."
|
"User {} already has the maximum of {} named servers."
|
||||||
" One must be deleted before a new server can be created".format(
|
" One must be deleted before a new server can be created".format(
|
||||||
user.name, self.named_server_limit_per_user
|
user.name, named_server_limit_per_user
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -458,7 +460,7 @@ class AdminHandler(BaseHandler):
|
|||||||
auth_state=auth_state,
|
auth_state=auth_state,
|
||||||
admin_access=True,
|
admin_access=True,
|
||||||
allow_named_servers=self.allow_named_servers,
|
allow_named_servers=self.allow_named_servers,
|
||||||
named_server_limit_per_user=self.named_server_limit_per_user,
|
named_server_limit_per_user=await self.get_current_user_named_server_limit(),
|
||||||
server_version=f'{__version__} {self.version_hash}',
|
server_version=f'{__version__} {self.version_hash}',
|
||||||
api_page_limit=self.settings["api_page_default_limit"],
|
api_page_limit=self.settings["api_page_default_limit"],
|
||||||
base_url=self.settings["base_url"],
|
base_url=self.settings["base_url"],
|
||||||
|
@@ -25,6 +25,25 @@ def named_servers(app):
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def named_servers_with_callable_limit(app):
|
||||||
|
def named_server_limit_per_user_fn(handler):
|
||||||
|
"""Limit number of named servers to `2` for non-admin users. No limit for admin users."""
|
||||||
|
user = handler.current_user
|
||||||
|
if user and user.admin:
|
||||||
|
return 0
|
||||||
|
return 2
|
||||||
|
|
||||||
|
with mock.patch.dict(
|
||||||
|
app.tornado_settings,
|
||||||
|
{
|
||||||
|
'allow_named_servers': True,
|
||||||
|
'named_server_limit_per_user': named_server_limit_per_user_fn,
|
||||||
|
},
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def default_server_name(app, named_servers):
|
def default_server_name(app, named_servers):
|
||||||
"""configure app to use a default server name"""
|
"""configure app to use a default server name"""
|
||||||
@@ -292,6 +311,57 @@ async def test_named_server_limit(app, named_servers):
|
|||||||
assert r.text == ''
|
assert r.text == ''
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'username,admin',
|
||||||
|
[
|
||||||
|
('nonsuperfoo', False),
|
||||||
|
('superfoo', True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_named_server_limit_as_callable(
|
||||||
|
app, named_servers_with_callable_limit, username, admin
|
||||||
|
):
|
||||||
|
"""Test named server limit based on `named_server_limit_per_user_fn` callable"""
|
||||||
|
user = add_user(app.db, app, name=username, admin=admin)
|
||||||
|
cookies = await app.login_user(username)
|
||||||
|
|
||||||
|
# Create 1st named server
|
||||||
|
servername1 = 'bar-1'
|
||||||
|
r = await api_request(
|
||||||
|
app, 'users', username, 'servers', servername1, method='post', cookies=cookies
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
assert r.status_code == 201
|
||||||
|
assert r.text == ''
|
||||||
|
|
||||||
|
# Create 2nd named server
|
||||||
|
servername2 = 'bar-2'
|
||||||
|
r = await api_request(
|
||||||
|
app, 'users', username, 'servers', servername2, method='post', cookies=cookies
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
assert r.status_code == 201
|
||||||
|
assert r.text == ''
|
||||||
|
|
||||||
|
# Create 3rd named server
|
||||||
|
servername3 = 'bar-3'
|
||||||
|
r = await api_request(
|
||||||
|
app, 'users', username, 'servers', servername3, method='post', cookies=cookies
|
||||||
|
)
|
||||||
|
|
||||||
|
# No named server limit for admin users as in `named_server_limit_per_user_fn` callable
|
||||||
|
if admin:
|
||||||
|
r.raise_for_status()
|
||||||
|
assert r.status_code == 201
|
||||||
|
assert r.text == ''
|
||||||
|
else:
|
||||||
|
assert r.status_code == 400
|
||||||
|
assert r.json() == {
|
||||||
|
"status": 400,
|
||||||
|
"message": f"User {username} already has the maximum of 2 named servers. One must be deleted before a new server can be created",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_named_server_spawn_form(app, username, named_servers):
|
async def test_named_server_spawn_form(app, username, named_servers):
|
||||||
server_name = "myserver"
|
server_name = "myserver"
|
||||||
base_url = public_url(app)
|
base_url = public_url(app)
|
||||||
|
Reference in New Issue
Block a user