mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-15 14:03:02 +00:00
Compare commits
28 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
30d4b2cef4 | ||
![]() |
34e4719893 | ||
![]() |
c6ac9e1d15 | ||
![]() |
70b8876239 | ||
![]() |
5e34f4481a | ||
![]() |
eae5594698 | ||
![]() |
f02022a00c | ||
![]() |
f964013516 | ||
![]() |
5f7ffaf1f6 | ||
![]() |
0e7ccb7520 | ||
![]() |
c9db504a49 | ||
![]() |
716677393e | ||
![]() |
ba8484f161 | ||
![]() |
ceec84dbb4 | ||
![]() |
f2a83ec846 | ||
![]() |
7deea6083a | ||
![]() |
a169ff3548 | ||
![]() |
f84a88da21 | ||
![]() |
eecec7183e | ||
![]() |
f11705ee26 | ||
![]() |
78ac5abf23 | ||
![]() |
2beeaa0932 | ||
![]() |
90cb8423bc | ||
![]() |
3b07bd286b | ||
![]() |
73564b97ea | ||
![]() |
65cad5efad | ||
![]() |
52eb627cd6 | ||
![]() |
506e568a9a |
@@ -203,6 +203,43 @@ paths:
|
||||
description: The user's notebook server has stopped
|
||||
'202':
|
||||
description: The user's notebook server has not yet stopped as it is taking a while to stop
|
||||
/users/{name}/servers/{server_name}:
|
||||
post:
|
||||
summary: Start a user's single-user named-server notebook server
|
||||
parameters:
|
||||
- name: name
|
||||
description: username
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
- name: server_name
|
||||
description: name given to a named-server
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
'201':
|
||||
description: The user's notebook named-server has started
|
||||
'202':
|
||||
description: The user's notebook named-server has not yet started, but has been requested
|
||||
delete:
|
||||
summary: Stop a user's named-server
|
||||
parameters:
|
||||
- name: name
|
||||
description: username
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
- name: server_name
|
||||
description: name given to a named-server
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
'204':
|
||||
description: The user's notebook named-server has stopped
|
||||
'202':
|
||||
description: The user's notebook named-server has not yet stopped as it is taking a while to stop
|
||||
/users/{name}/admin-access:
|
||||
post:
|
||||
summary: Grant admin access to this user's notebook server
|
||||
|
@@ -123,6 +123,11 @@ via other mechanisms. One such example is using [GitHub OAuth][].
|
||||
|
||||
Because the username is passed from the Authenticator to the Spawner,
|
||||
a custom Authenticator and Spawner are often used together.
|
||||
For example, the Authenticator methods, [pre_spawn_start(user, spawner)][]
|
||||
and [post_spawn_stop(user, spawner)][], are hooks that can be used to do
|
||||
auth-related startup (e.g. opening PAM sessions) and cleanup
|
||||
(e.g. closing PAM sessions).
|
||||
|
||||
|
||||
See a list of custom Authenticators [on the wiki](https://github.com/jupyterhub/jupyterhub/wiki/Authenticators).
|
||||
|
||||
@@ -140,3 +145,5 @@ Beginning with version 0.8, JupyterHub is an OAuth provider.
|
||||
[OAuth]: https://en.wikipedia.org/wiki/OAuth
|
||||
[GitHub OAuth]: https://developer.github.com/v3/oauth/
|
||||
[OAuthenticator]: https://github.com/jupyterhub/oauthenticator
|
||||
[pre_spawn_start(user, spawner)]: http://jupyterhub.readthedocs.io/en/latest/api/auth.html#jupyterhub.auth.Authenticator.pre_spawn_start
|
||||
[post_spawn_stop(user, spawner)]: http://jupyterhub.readthedocs.io/en/latest/api/auth.html#jupyterhub.auth.Authenticator.post_spawn_stop
|
||||
|
@@ -200,7 +200,9 @@ or via the `JUPYTERHUB_API_TOKEN` environment variable.
|
||||
|
||||
Most of the logic for authentication implementation is found in the
|
||||
[`HubAuth.user_for_cookie`](services.auth.html#jupyterhub.services.auth.HubAuth.user_for_cookie)
|
||||
method, which makes a request of the Hub, and returns:
|
||||
and in the
|
||||
[`HubAuth.user_for_token`](services.auth.html#jupyterhub.services.auth.HubAuth.user_for_token)
|
||||
methods, which makes a request of the Hub, and returns:
|
||||
|
||||
- None, if no user could be identified, or
|
||||
- a dict of the following form:
|
||||
@@ -252,8 +254,11 @@ def authenticated(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
cookie = request.cookies.get(auth.cookie_name)
|
||||
token = request.headers.get(auth.auth_header_name)
|
||||
if cookie:
|
||||
user = auth.user_for_cookie(cookie)
|
||||
elif token:
|
||||
user = auth.user_for_token(token)
|
||||
else:
|
||||
user = None
|
||||
if user:
|
||||
|
@@ -28,8 +28,11 @@ def authenticated(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
cookie = request.cookies.get(auth.cookie_name)
|
||||
token = request.headers.get(auth.auth_header_name)
|
||||
if cookie:
|
||||
user = auth.user_for_cookie(cookie)
|
||||
elif token:
|
||||
user = auth.user_for_token(token)
|
||||
else:
|
||||
user = None
|
||||
if user:
|
||||
|
@@ -7,7 +7,7 @@ version_info = (
|
||||
0,
|
||||
8,
|
||||
0,
|
||||
'b5',
|
||||
'rc1',
|
||||
)
|
||||
|
||||
__version__ = '.'.join(map(str, version_info))
|
||||
|
@@ -12,9 +12,16 @@ config = context.config
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if 'jupyterhub' in sys.modules:
|
||||
from traitlets.config import MultipleInstanceError
|
||||
from jupyterhub.app import JupyterHub
|
||||
app = None
|
||||
if JupyterHub.initialized():
|
||||
app = JupyterHub.instance()
|
||||
try:
|
||||
app = JupyterHub.instance()
|
||||
except MultipleInstanceError:
|
||||
# could have been another Application
|
||||
pass
|
||||
if app is not None:
|
||||
alembic_logger = logging.getLogger('alembic')
|
||||
alembic_logger.propagate = True
|
||||
alembic_logger.parent = app.log
|
||||
|
@@ -114,7 +114,7 @@ class APIHandler(BaseHandler):
|
||||
if spawner.pending:
|
||||
s['pending'] = spawner.pending
|
||||
if spawner.server:
|
||||
s['url'] = user.url + name + '/'
|
||||
s['url'] = url_path_join(user.url, name, '/')
|
||||
return model
|
||||
|
||||
def group_model(self, group):
|
||||
|
@@ -185,8 +185,6 @@ class UserServerAPIHandler(APIHandler):
|
||||
user = self.find_user(name)
|
||||
if server_name and not self.allow_named_servers:
|
||||
raise web.HTTPError(400, "Named servers are not enabled.")
|
||||
if self.allow_named_servers and not server_name:
|
||||
server_name = user.default_server_name()
|
||||
spawner = user.spawners[server_name]
|
||||
pending = spawner.pending
|
||||
if pending == 'spawn':
|
||||
|
@@ -1219,7 +1219,7 @@ class JupyterHub(Application):
|
||||
status = yield spawner.poll()
|
||||
except Exception:
|
||||
self.log.exception("Failed to poll spawner for %s, assuming the spawner is not running.",
|
||||
user.name if name else '%s|%s' % (user.name, name))
|
||||
spawner._log_name)
|
||||
status = -1
|
||||
|
||||
if status is None:
|
||||
@@ -1230,11 +1230,13 @@ class JupyterHub(Application):
|
||||
# user not running. This is expected if server is None,
|
||||
# but indicates the user's server died while the Hub wasn't running
|
||||
# if spawner.server is defined.
|
||||
log = self.log.warning if spawner.server else self.log.debug
|
||||
log("%s not running.", user.name)
|
||||
# remove all server or servers entry from db related to the user
|
||||
if spawner.server:
|
||||
self.log.warning("%s appears to have stopped while the Hub was down", spawner._log_name)
|
||||
# remove server entry from db
|
||||
db.delete(spawner.orm_spawner.server)
|
||||
spawner.server = None
|
||||
else:
|
||||
self.log.debug("%s not running", spawner._log_name)
|
||||
db.commit()
|
||||
|
||||
user_summaries.append(_user_summary(user))
|
||||
|
@@ -20,7 +20,7 @@ from .. import __version__
|
||||
from .. import orm
|
||||
from ..objects import Server
|
||||
from ..spawner import LocalProcessSpawner
|
||||
from ..utils import default_server_name, url_path_join
|
||||
from ..utils import url_path_join
|
||||
|
||||
# pattern for the authentication token header
|
||||
auth_header_pat = re.compile(r'^(?:token|bearer)\s+([^\s]+)$', flags=re.IGNORECASE)
|
||||
@@ -380,8 +380,6 @@ class BaseHandler(RequestHandler):
|
||||
self.extra_error_html = self.spawn_home_error
|
||||
|
||||
user_server_name = user.name
|
||||
if self.allow_named_servers and not server_name:
|
||||
server_name = default_server_name(user)
|
||||
|
||||
if server_name:
|
||||
user_server_name = '%s:%s' % (user.name, server_name)
|
||||
|
@@ -177,7 +177,7 @@ class Spawner(Base):
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'))
|
||||
|
||||
server_id = Column(Integer, ForeignKey('servers.id'))
|
||||
server_id = Column(Integer, ForeignKey('servers.id', ondelete='SET NULL'))
|
||||
server = relationship(Server)
|
||||
|
||||
state = Column(JSONDict)
|
||||
@@ -213,7 +213,7 @@ class Service(Base):
|
||||
api_tokens = relationship("APIToken", backref="service")
|
||||
|
||||
# service-specific interface
|
||||
_server_id = Column(Integer, ForeignKey('servers.id'))
|
||||
_server_id = Column(Integer, ForeignKey('servers.id', ondelete='SET NULL'))
|
||||
server = relationship(Server, primaryjoin=_server_id == Server.id)
|
||||
pid = Column(Integer)
|
||||
|
||||
|
@@ -657,13 +657,12 @@ class HubAuthenticated(object):
|
||||
def get_login_url(self):
|
||||
"""Return the Hub's login URL"""
|
||||
login_url = self.hub_auth.login_url
|
||||
app_log.debug("Redirecting to login url: %s", login_url)
|
||||
if isinstance(self.hub_auth, HubOAuthenticated):
|
||||
if isinstance(self.hub_auth, HubOAuth):
|
||||
# add state argument to OAuth url
|
||||
state = self.hub_auth.set_state_cookie(self, next_url=self.request.uri)
|
||||
return url_concat(login_url, {'state': state})
|
||||
else:
|
||||
return login_url
|
||||
login_url = url_concat(login_url, {'state': state})
|
||||
app_log.debug("Redirecting to login url: %s", login_url)
|
||||
return login_url
|
||||
|
||||
def check_hub_user(self, model):
|
||||
"""Check whether Hub-authenticated user or service should be allowed.
|
||||
@@ -774,7 +773,7 @@ class HubOAuthCallbackHandler(HubOAuthenticated, RequestHandler):
|
||||
next_url = None
|
||||
if arg_state or cookie_state:
|
||||
# clear cookie state now that we've consumed it
|
||||
self.clear_cookie(self.hub_auth.state_cookie_name)
|
||||
self.clear_cookie(self.hub_auth.state_cookie_name, path=self.hub_auth.base_url)
|
||||
if isinstance(cookie_state, bytes):
|
||||
cookie_state = cookie_state.decode('ascii', 'replace')
|
||||
# check that state matches
|
||||
|
@@ -301,5 +301,8 @@ class Service(LoggingConfigurable):
|
||||
if not self.managed:
|
||||
raise RuntimeError("Cannot stop unmanaged service %s" % self)
|
||||
if self.spawner:
|
||||
if self.orm.server:
|
||||
self.db.delete(self.orm.server)
|
||||
self.db.commit()
|
||||
self.spawner.stop_polling()
|
||||
return self.spawner.stop()
|
||||
|
@@ -144,11 +144,13 @@ page_template = """
|
||||
{% block header_buttons %}
|
||||
{{super()}}
|
||||
|
||||
<a href='{{hub_control_panel_url}}'
|
||||
class='btn btn-default btn-sm navbar-btn pull-right'
|
||||
style='margin-right: 4px; margin-left: 2px;'
|
||||
>
|
||||
Control Panel</a>
|
||||
<span>
|
||||
<a href='{{hub_control_panel_url}}'
|
||||
class='btn btn-default btn-sm navbar-btn pull-right'
|
||||
style='margin-right: 4px; margin-left: 2px;'>
|
||||
Control Panel
|
||||
</a>
|
||||
</span>
|
||||
{% endblock %}
|
||||
{% block logo %}
|
||||
<img src='{{logo_url}}' alt='Jupyter Notebook'/>
|
||||
|
@@ -839,7 +839,7 @@ class LocalProcessSpawner(Spawner):
|
||||
This is the default spawner for JupyterHub.
|
||||
"""
|
||||
|
||||
INTERRUPT_TIMEOUT = Integer(10,
|
||||
interrupt_timeout = Integer(10,
|
||||
help="""
|
||||
Seconds to wait for single-user server process to halt after SIGINT.
|
||||
|
||||
@@ -847,7 +847,7 @@ class LocalProcessSpawner(Spawner):
|
||||
"""
|
||||
).tag(config=True)
|
||||
|
||||
TERM_TIMEOUT = Integer(5,
|
||||
term_timeout = Integer(5,
|
||||
help="""
|
||||
Seconds to wait for single-user server process to halt after SIGTERM.
|
||||
|
||||
@@ -855,7 +855,7 @@ class LocalProcessSpawner(Spawner):
|
||||
"""
|
||||
).tag(config=True)
|
||||
|
||||
KILL_TIMEOUT = Integer(5,
|
||||
kill_timeout = Integer(5,
|
||||
help="""
|
||||
Seconds to wait for process to halt after SIGKILL before giving up.
|
||||
|
||||
@@ -1071,7 +1071,7 @@ class LocalProcessSpawner(Spawner):
|
||||
return
|
||||
self.log.debug("Interrupting %i", self.pid)
|
||||
yield self._signal(signal.SIGINT)
|
||||
yield self.wait_for_death(self.INTERRUPT_TIMEOUT)
|
||||
yield self.wait_for_death(self.interrupt_timeout)
|
||||
|
||||
# clean shutdown failed, use TERM
|
||||
status = yield self.poll()
|
||||
@@ -1079,7 +1079,7 @@ class LocalProcessSpawner(Spawner):
|
||||
return
|
||||
self.log.debug("Terminating %i", self.pid)
|
||||
yield self._signal(signal.SIGTERM)
|
||||
yield self.wait_for_death(self.TERM_TIMEOUT)
|
||||
yield self.wait_for_death(self.term_timeout)
|
||||
|
||||
# TERM failed, use KILL
|
||||
status = yield self.poll()
|
||||
@@ -1087,7 +1087,7 @@ class LocalProcessSpawner(Spawner):
|
||||
return
|
||||
self.log.debug("Killing %i", self.pid)
|
||||
yield self._signal(signal.SIGKILL)
|
||||
yield self.wait_for_death(self.KILL_TIMEOUT)
|
||||
yield self.wait_for_death(self.kill_timeout)
|
||||
|
||||
status = yield self.poll()
|
||||
if status is None:
|
||||
|
@@ -8,9 +8,11 @@ from subprocess import check_output, Popen, PIPE
|
||||
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
||||
from unittest.mock import patch
|
||||
|
||||
from tornado import gen
|
||||
import pytest
|
||||
|
||||
from .mocking import MockHub
|
||||
from .test_api import add_user
|
||||
from .. import orm
|
||||
from ..app import COOKIE_SECRET_BYTES
|
||||
|
||||
@@ -161,3 +163,57 @@ def test_load_groups():
|
||||
assert gold is not None
|
||||
assert sorted([ u.name for u in gold.users ]) == sorted(to_load['gold'])
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_resume_spawners(tmpdir, request):
|
||||
if not os.getenv('JUPYTERHUB_TEST_DB_URL'):
|
||||
p = patch.dict(os.environ, {
|
||||
'JUPYTERHUB_TEST_DB_URL': 'sqlite:///%s' % tmpdir.join('jupyterhub.sqlite'),
|
||||
})
|
||||
p.start()
|
||||
request.addfinalizer(p.stop)
|
||||
@gen.coroutine
|
||||
def new_hub():
|
||||
app = MockHub()
|
||||
app.config.ConfigurableHTTPProxy.should_start = False
|
||||
yield app.initialize([])
|
||||
return app
|
||||
app = yield new_hub()
|
||||
db = app.db
|
||||
# spawn a user's server
|
||||
name = 'kurt'
|
||||
user = add_user(db, app, name=name)
|
||||
yield user.spawn()
|
||||
proc = user.spawner.proc
|
||||
assert proc is not None
|
||||
|
||||
# stop the Hub without cleaning up servers
|
||||
app.cleanup_servers = False
|
||||
yield app.stop()
|
||||
|
||||
# proc is still running
|
||||
assert proc.poll() is None
|
||||
|
||||
# resume Hub, should still be running
|
||||
app = yield new_hub()
|
||||
db = app.db
|
||||
user = app.users[name]
|
||||
assert user.running
|
||||
assert user.spawner.server is not None
|
||||
|
||||
# stop the Hub without cleaning up servers
|
||||
app.cleanup_servers = False
|
||||
yield app.stop()
|
||||
|
||||
# stop the server while the Hub is down. BAMF!
|
||||
proc.terminate()
|
||||
proc.wait(timeout=10)
|
||||
assert proc.poll() is not None
|
||||
|
||||
# resume Hub, should be stopped
|
||||
app = yield new_hub()
|
||||
db = app.db
|
||||
user = app.users[name]
|
||||
assert not user.running
|
||||
assert user.spawner.server is None
|
||||
assert list(db.query(orm.Server)) == []
|
||||
|
@@ -46,4 +46,3 @@ def test_upgrade_entrypoint(tmpdir):
|
||||
|
||||
# run tokenapp again, it should work
|
||||
tokenapp.start()
|
||||
|
@@ -17,6 +17,57 @@ def named_servers(app):
|
||||
app.tornado_application.settings[key] = app.tornado_settings[key] = False
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_default_server(app, named_servers):
|
||||
"""Test the default /users/:user/server handler when named servers are enabled"""
|
||||
username = 'rosie'
|
||||
user = add_user(app.db, app, name=username)
|
||||
r = yield api_request(app, 'users', username, 'server', method='post')
|
||||
assert r.status_code == 201
|
||||
assert r.text == ''
|
||||
|
||||
r = yield api_request(app, 'users', username)
|
||||
r.raise_for_status()
|
||||
|
||||
user_model = r.json()
|
||||
user_model.pop('last_activity')
|
||||
assert user_model == {
|
||||
'name': username,
|
||||
'groups': [],
|
||||
'kind': 'user',
|
||||
'admin': False,
|
||||
'pending': None,
|
||||
'server': user.url,
|
||||
'servers': {
|
||||
'': {
|
||||
'name': '',
|
||||
'url': user.url,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# now stop the server
|
||||
r = yield api_request(app, 'users', username, 'server', method='delete')
|
||||
assert r.status_code == 204
|
||||
assert r.text == ''
|
||||
|
||||
r = yield api_request(app, 'users', username)
|
||||
r.raise_for_status()
|
||||
|
||||
user_model = r.json()
|
||||
user_model.pop('last_activity')
|
||||
assert user_model == {
|
||||
'name': username,
|
||||
'groups': [],
|
||||
'kind': 'user',
|
||||
'admin': False,
|
||||
'pending': None,
|
||||
'server': None,
|
||||
'servers': {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_create_named_server(app, named_servers):
|
||||
username = 'walnut'
|
||||
@@ -49,13 +100,13 @@ def test_create_named_server(app, named_servers):
|
||||
'kind': 'user',
|
||||
'admin': False,
|
||||
'pending': None,
|
||||
'server': None,
|
||||
'server': user.url,
|
||||
'servers': {
|
||||
name: {
|
||||
'name': name,
|
||||
'url': url_path_join(user.url, name, '/'),
|
||||
}
|
||||
for name in ['1', servername]
|
||||
for name in ['', servername]
|
||||
},
|
||||
}
|
||||
|
||||
@@ -86,13 +137,13 @@ def test_delete_named_server(app, named_servers):
|
||||
'kind': 'user',
|
||||
'admin': False,
|
||||
'pending': None,
|
||||
'server': None,
|
||||
'server': user.url,
|
||||
'servers': {
|
||||
name: {
|
||||
'name': name,
|
||||
'url': url_path_join(user.url, name, '/'),
|
||||
}
|
||||
for name in ['1']
|
||||
for name in ['']
|
||||
},
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,7 @@ import requests_mock
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.web import RequestHandler, Application, authenticated, HTTPError
|
||||
from tornado.httputil import url_concat
|
||||
|
||||
from ..services.auth import _ExpiringDict, HubAuth, HubAuthenticated
|
||||
from ..utils import url_path_join
|
||||
@@ -316,7 +317,8 @@ def test_hubauth_service_token(app, mockservice_url):
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_oauth_service(app, mockservice_url):
|
||||
url = url_path_join(public_url(app, mockservice_url) + 'owhoami/')
|
||||
service = mockservice_url
|
||||
url = url_path_join(public_url(app, mockservice_url) + 'owhoami/?arg=x')
|
||||
# first request is only going to set login cookie
|
||||
# FIXME: redirect to originating URL (OAuth loses this info)
|
||||
s = requests.Session()
|
||||
@@ -326,6 +328,14 @@ def test_oauth_service(app, mockservice_url):
|
||||
s_get = lambda *args, **kwargs: async_requests.executor.submit(s.get, *args, **kwargs)
|
||||
r = yield s_get(url)
|
||||
r.raise_for_status()
|
||||
assert r.url == url
|
||||
# verify oauth cookie is set
|
||||
assert 'service-%s' % service.name in set(s.cookies.keys())
|
||||
# verify oauth state cookie has been consumed
|
||||
assert 'service-%s-oauth-state' % service.name not in set(s.cookies.keys())
|
||||
# verify oauth state cookie was set at some point
|
||||
assert set(r.history[0].cookies.keys()) == {'service-%s-oauth-state' % service.name}
|
||||
|
||||
# second request should be authenticated
|
||||
r = yield s_get(url, allow_redirects=False)
|
||||
r.raise_for_status()
|
||||
@@ -340,7 +350,7 @@ def test_oauth_service(app, mockservice_url):
|
||||
# token-authenticated request to HubOAuth
|
||||
token = app.users[name].new_api_token()
|
||||
# token in ?token parameter
|
||||
r = yield async_requests.get(public_url(app, mockservice_url) + 'owhoami/?token=%s' % token)
|
||||
r = yield async_requests.get(url_concat(url, {'token': token}))
|
||||
r.raise_for_status()
|
||||
reply = r.json()
|
||||
assert reply['name'] == name
|
||||
@@ -349,11 +359,12 @@ def test_oauth_service(app, mockservice_url):
|
||||
assert len(r.cookies) != 0
|
||||
# ensure cookie works in future requests
|
||||
r = yield async_requests.get(
|
||||
public_url(app, mockservice_url) + 'owhoami/',
|
||||
url,
|
||||
cookies=r.cookies,
|
||||
allow_redirects=False,
|
||||
)
|
||||
r.raise_for_status()
|
||||
assert r.url == url
|
||||
reply = r.json()
|
||||
assert reply['name'] == name
|
||||
|
||||
|
@@ -51,9 +51,9 @@ def new_spawner(db, **kwargs):
|
||||
kwargs.setdefault('notebook_dir', os.getcwd())
|
||||
kwargs.setdefault('default_url', '/user/{username}/lab')
|
||||
kwargs.setdefault('oauth_client_id', 'mock-client-id')
|
||||
kwargs.setdefault('INTERRUPT_TIMEOUT', 1)
|
||||
kwargs.setdefault('TERM_TIMEOUT', 1)
|
||||
kwargs.setdefault('KILL_TIMEOUT', 1)
|
||||
kwargs.setdefault('interrupt_timeout', 1)
|
||||
kwargs.setdefault('term_timeout', 1)
|
||||
kwargs.setdefault('kill_timeout', 1)
|
||||
kwargs.setdefault('poll_interval', 1)
|
||||
return user._new_spawner('', spawner_class=LocalProcessSpawner, **kwargs)
|
||||
|
||||
@@ -299,7 +299,7 @@ def test_spawner_reuse_api_token(db, app):
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_spawner_insert_api_token(db, app):
|
||||
def test_spawner_insert_api_token(app):
|
||||
"""Token provided by spawner is not in the db
|
||||
|
||||
Insert token into db as a user-provided token.
|
||||
@@ -326,7 +326,7 @@ def test_spawner_insert_api_token(db, app):
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_spawner_bad_api_token(db, app):
|
||||
def test_spawner_bad_api_token(app):
|
||||
"""Tokens are revoked when a Spawner gets another user's token"""
|
||||
# we need two users for this one
|
||||
user = add_user(app.db, app, name='antimone')
|
||||
@@ -346,3 +346,37 @@ def test_spawner_bad_api_token(db, app):
|
||||
yield user.spawn()
|
||||
assert orm.APIToken.find(app.db, other_token) is None
|
||||
assert other_user.api_tokens == []
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_spawner_delete_server(app):
|
||||
"""Test deleting spawner.server
|
||||
|
||||
This can occur during app startup if their server has been deleted.
|
||||
"""
|
||||
db = app.db
|
||||
user = add_user(app.db, app, name='gaston')
|
||||
spawner = user.spawner
|
||||
orm_server = orm.Server()
|
||||
db.add(orm_server)
|
||||
db.commit()
|
||||
server_id = orm_server.id
|
||||
spawner.server = Server.from_orm(orm_server)
|
||||
db.commit()
|
||||
|
||||
assert spawner.server is not None
|
||||
assert spawner.orm_spawner.server is not None
|
||||
|
||||
# trigger delete via db
|
||||
db.delete(spawner.orm_spawner.server)
|
||||
db.commit()
|
||||
assert spawner.orm_spawner.server is None
|
||||
|
||||
# setting server = None also triggers delete
|
||||
spawner.server = None
|
||||
db.commit()
|
||||
# verify that the server was actually deleted from the db
|
||||
assert db.query(orm.Server).filter(orm.Server.id == server_id).first() is None
|
||||
# verify that both ORM and top-level references are None
|
||||
assert spawner.orm_spawner.server is None
|
||||
assert spawner.server is None
|
||||
|
@@ -34,18 +34,27 @@ def test_memoryspec():
|
||||
c = C()
|
||||
|
||||
c.mem = 1024
|
||||
assert isinstance(c.mem, int)
|
||||
assert c.mem == 1024
|
||||
|
||||
c.mem = '1024K'
|
||||
assert isinstance(c.mem, int)
|
||||
assert c.mem == 1024 * 1024
|
||||
|
||||
c.mem = '1024M'
|
||||
assert isinstance(c.mem, int)
|
||||
assert c.mem == 1024 * 1024 * 1024
|
||||
|
||||
c.mem = '1.5M'
|
||||
assert isinstance(c.mem, int)
|
||||
assert c.mem == 1.5 * 1024 * 1024
|
||||
|
||||
c.mem = '1024G'
|
||||
assert isinstance(c.mem, int)
|
||||
assert c.mem == 1024 * 1024 * 1024 * 1024
|
||||
|
||||
c.mem = '1024T'
|
||||
assert isinstance(c.mem, int)
|
||||
assert c.mem == 1024 * 1024 * 1024 * 1024 * 1024
|
||||
|
||||
with pytest.raises(TraitError):
|
||||
|
@@ -48,7 +48,7 @@ class ByteSpecification(Integer):
|
||||
'K': 1024,
|
||||
'M': 1024 * 1024,
|
||||
'G': 1024 * 1024 * 1024,
|
||||
'T': 1024 * 1024 * 1024 * 1024
|
||||
'T': 1024 * 1024 * 1024 * 1024,
|
||||
}
|
||||
|
||||
# Default to allowing None as a value
|
||||
@@ -62,11 +62,15 @@ class ByteSpecification(Integer):
|
||||
If it has one of the suffixes, it is converted into the appropriate
|
||||
pure byte value.
|
||||
"""
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
num = value[:-1]
|
||||
if isinstance(value, (int, float)):
|
||||
return int(value)
|
||||
|
||||
try:
|
||||
num = float(value[:-1])
|
||||
except ValueError:
|
||||
raise TraitError('{val} is not a valid memory specification. Must be an int or a string with suffix K, M, G, T'.format(val=value))
|
||||
suffix = value[-1]
|
||||
if not num.isdigit() and suffix not in ByteSpecification.UNIT_SUFFIXES:
|
||||
if suffix not in self.UNIT_SUFFIXES:
|
||||
raise TraitError('{val} is not a valid memory specification. Must be an int or a string with suffix K, M, G, T'.format(val=value))
|
||||
else:
|
||||
return int(num) * ByteSpecification.UNIT_SUFFIXES[suffix]
|
||||
return int(float(num) * self.UNIT_SUFFIXES[suffix])
|
||||
|
@@ -12,7 +12,7 @@ from tornado import gen
|
||||
from tornado.log import app_log
|
||||
from traitlets import HasTraits, Any, Dict, default
|
||||
|
||||
from .utils import url_path_join, default_server_name
|
||||
from .utils import url_path_join
|
||||
|
||||
from . import orm
|
||||
from ._version import _check_version, __version__
|
||||
|
@@ -298,17 +298,3 @@ def url_path_join(*pieces):
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def default_server_name(user):
|
||||
"""Return the default name for a new server for a given user.
|
||||
|
||||
Will be the first available integer string, e.g. '1' or '2'.
|
||||
"""
|
||||
existing_names = set(user.spawners)
|
||||
# if there are 5 servers, count from 1 to 6
|
||||
for n in range(1, len(existing_names) + 2):
|
||||
name = str(n)
|
||||
if name not in existing_names:
|
||||
return name
|
||||
raise RuntimeError("It should be impossible to get here")
|
||||
|
||||
|
Reference in New Issue
Block a user