Compare commits

...

28 Commits

Author SHA1 Message Date
Min RK
30d4b2cef4 0.8.0rc1 2017-09-19 19:07:34 +02:00
Carol Willing
34e4719893 Merge pull request #1434 from Analect/rest-api-named-server
Add handling for POST/DELETE of named-servers in hub API introduced in 0.8x
2017-09-19 06:17:05 -07:00
analect
c6ac9e1d15 Add handling for POST/DELETE of named-servers introduced in 0.8x 2017-09-19 13:20:15 +01:00
Min RK
70b8876239 Merge pull request #1413 from yuvipanda/memory-float
Allow non integral memory byte specifications
2017-09-18 10:50:56 +02:00
Min RK
5e34f4481a refer to self.UNIT_SUFFIXES 2017-09-18 10:10:20 +02:00
Min RK
eae5594698 byte specifications always return integers 2017-09-18 10:09:14 +02:00
Carol Willing
f02022a00c Merge pull request #1428 from minrk/default-server-name
allow default (empty) server name with named servers
2017-09-17 20:01:31 -07:00
Min RK
f964013516 exercise default server handler with named servers enabled 2017-09-17 11:55:50 +02:00
Min RK
5f7ffaf1f6 allow default (empty) server name with named servers
remove generated names behavior because it doesn't work
2017-09-17 11:47:17 +02:00
Carol Willing
0e7ccb7520 Merge pull request #1422 from minrk/lowercase-timeouts
lowercase LocalProcessSpawner timeouts
2017-09-15 08:11:15 -07:00
Min RK
c9db504a49 Merge pull request #1424 from phill84/bugfix/control-panel-button-height
wrap control panel button in a span
2017-09-15 06:56:41 -07:00
Jiening Wen
716677393e wrap control panel button in a span
make sure the same style is applied to all buttons in header-container
2017-09-15 15:29:38 +02:00
Min RK
ba8484f161 lowercase LocalProcessSpawner timeouts
traitlets doesn't like uppercase configurables
2017-09-15 12:07:03 +02:00
Yuvi Panda
ceec84dbb4 Merge pull request #1417 from minrk/test-delete
test restoring and deleting spawners while the Hub is down
2017-09-14 12:54:38 -07:00
Yuvi Panda
f2a83ec846 Merge pull request #1418 from minrk/oauth-state-boogaloo
Fixes (and tests!) for oauth state handling
2017-09-14 12:43:39 -07:00
Carol Willing
7deea6083a Merge pull request #1416 from minrk/traitlets-log
avoid error if another traitlets Application is initialized
2017-09-14 10:50:52 -07:00
Min RK
a169ff3548 test oauth redirects
include coverage of state handling
2017-09-14 16:06:57 +02:00
Min RK
f84a88da21 fix oauth state redirect
check for HubOAuth, not HubOAuthenticated
2017-09-14 16:06:36 +02:00
Min RK
eecec7183e fix clearing of oauth state cookie
missing path arg
2017-09-14 16:01:34 +02:00
Min RK
f11705ee26 delete service.server from db when they stop
same ondelete='SET NULL' as on spawner.server
2017-09-14 13:30:38 +02:00
Min RK
78ac5abf23 test restoring and deleting spawners while the Hub is down
- set ONDELETE='set null' on spawner->server relation (fixes error when deleting servers that stopped)
- set `spawner.server = None`, which is not triggered when deleting orm_spawner.server
2017-09-14 13:16:29 +02:00
Min RK
2beeaa0932 avoid error if another traitlets Application is initialized
encountered when doing db debugging in IPython
2017-09-14 11:37:34 +02:00
yuvipanda
90cb8423bc Allow non integral memory byte specifications 2017-09-12 16:19:10 -07:00
Min RK
3b07bd286b Merge pull request #1408 from DeepHorizons/update_service_doc
Updated the reference flask service example to include token auth
2017-09-12 23:49:55 +02:00
Joshua Milas
73564b97ea Updated the whoami-flask example 2017-09-11 12:16:17 -04:00
Joshua Milas
65cad5efad Updated the reference flask example to include token auth 2017-09-11 00:09:57 -04:00
Carol Willing
52eb627cd6 Merge pull request #1407 from willingc/spawn-hooks
Add pre/post spawn hooks to docs
2017-09-08 13:01:56 -07:00
Carol Willing
506e568a9a Add pre/post spawn hooks to docs 2017-09-08 13:00:14 -07:00
24 changed files with 277 additions and 66 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -7,7 +7,7 @@ version_info = (
0,
8,
0,
'b5',
'rc1',
)
__version__ = '.'.join(map(str, version_info))

View File

@@ -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

View File

@@ -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):

View File

@@ -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':

View File

@@ -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))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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'/>

View File

@@ -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:

View File

@@ -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)) == []

View File

@@ -46,4 +46,3 @@ def test_upgrade_entrypoint(tmpdir):
# run tokenapp again, it should work
tokenapp.start()

View File

@@ -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 ['']
},
}

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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])

View File

@@ -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__

View File

@@ -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")