further clear up named servers

- use spawner.server instead of user.server
- user.running, proxy_spec are methods that take spawner names
This commit is contained in:
Min RK
2017-07-20 16:54:17 +02:00
parent 3eca010f66
commit 382a7121e1
11 changed files with 76 additions and 54 deletions

View File

@@ -9,6 +9,7 @@ import binascii
from datetime import datetime
from getpass import getuser
import logging
from operator import itemgetter
import os
import re
import shutil
@@ -1116,8 +1117,9 @@ class JupyterHub(Application):
parts = ['{0: >8}'.format(user.name)]
if user.admin:
parts.append('admin')
if user.server:
parts.append('running at %s' % user.server)
for name, spawner in sorted(user.spawners.items(), key=itemgetter(0)):
if spawner.server:
parts.append('%r running at %s' % (name, spawner.server))
return ' '.join(parts)
@gen.coroutine
@@ -1149,7 +1151,7 @@ class JupyterHub(Application):
else:
# user not running. This is expected if server is None,
# but indicates the user's server died while the Hub wasn't running
# if user.server is defined.
# 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
@@ -1319,6 +1321,7 @@ class JupyterHub(Application):
self.log.info("Cleaning up single-user servers...")
# request (async) process termination
for uid, user in self.users.items():
user.db = self.db
if user.spawner is not None:
futures.append(user.stop())
else:

View File

@@ -57,6 +57,10 @@ class BaseHandler(RequestHandler):
def subdomain_host(self):
return self.settings.get('subdomain_host', '')
@property
def allow_named_servers(self):
return self.settings.get('allow_named_servers', False)
@property
def domain(self):
return self.settings['domain']
@@ -360,7 +364,7 @@ class BaseHandler(RequestHandler):
# though it's possible that it started at the wrong URL
self.log.warning("User %s's server is slow to become responsive (timeout=%s)",
user.name, self.slow_spawn_timeout)
self.log.debug("Expecting server for %s at: %s", user.name, user.server.url)
self.log.debug("Expecting server for %s at: %s", user.name, spawner.server.url)
# schedule finish for when the user finishes spawning
IOLoop.current().add_future(f, finish_user_spawn)
else:

View File

@@ -182,7 +182,7 @@ class AdminHandler(BaseHandler):
users = self.db.query(orm.User).order_by(*ordered)
users = [ self._user_from_orm(u) for u in users ]
running = [ u for u in users if u.running ]
running = [ u for u in users if u.running('') ]
html = self.render_template('admin.html',
user=self.get_current_user(),

View File

@@ -26,6 +26,7 @@ from traitlets import (
validate,
)
from .objects import Server
from .traitlets import Command, ByteSpecification
from .utils import random_port, url_path_join
@@ -52,11 +53,19 @@ class Spawner(LoggingConfigurable):
_waiting_for_response = False
orm_spawner = Any()
db = Any()
user = Any()
hub = Any()
authenticator = Any()
server = Any()
orm_spawner = Any()
@property
def server(self):
if self.orm_spawner and self.orm_spawner.server:
return Server(orm_server=self.orm_spawner.server)
@property
def name(self):
if self.orm_spawner:
return self.orm_spawner.name
return ''
admin_access = Bool(False)
api_token = Unicode()
oauth_client_id = Unicode()
@@ -436,7 +445,7 @@ class Spawner(LoggingConfigurable):
env['JUPYTERHUB_CLIENT_ID'] = self.oauth_client_id
env['JUPYTERHUB_HOST'] = self.hub.public_host
env['JUPYTERHUB_OAUTH_CALLBACK_URL'] = \
url_path_join(self.user.url, 'oauth_callback')
url_path_join(self.user.url, self.name, 'oauth_callback')
# Info previously passed on args
env['JUPYTERHUB_USER'] = self.user.name

View File

@@ -157,6 +157,9 @@ class MockHub(JupyterHub):
def init_signal(self):
pass
def load_config_file(self, *args, **kwargs):
pass
def start(self, argv=None):
self.db_file = NamedTemporaryFile()
self.pid_file = NamedTemporaryFile(delete=False).name
@@ -172,8 +175,6 @@ class MockHub(JupyterHub):
# add an initial user
user = orm.User(name='user')
self.db.add(user)
admin = orm.User(name='admin', admin=True)
self.db.add(admin)
self.db.commit()
yield super(MockHub, self).start()
yield self.hub.wait_up(http=True)

View File

@@ -408,16 +408,17 @@ def test_spawn(app, io_loop):
assert 'pid' in user.orm_spawners[''].state
app_user = get_app_user(app, name)
assert app_user.spawner is not None
spawner = app_user.spawner
assert app_user.spawner.user_options == options
assert not app_user.spawner._spawn_pending
status = io_loop.run_sync(app_user.spawner.poll)
assert status is None
assert user.server.base_url == ujoin(app.base_url, 'user/%s' % name) + '/'
assert spawner.server.base_url == ujoin(app.base_url, 'user/%s' % name) + '/'
url = public_url(app, user)
r = requests.get(url)
assert r.status_code == 200
assert r.text == user.server.base_url
assert r.text == spawner.server.base_url
r = requests.get(ujoin(url, 'args'))
assert r.status_code == 200
@@ -438,7 +439,7 @@ def test_spawn(app, io_loop):
assert status == 0
# check that we cleaned up after ourselves
assert user.server is None
assert spawner.server is None
after_servers = sorted(db.query(orm.Server), key=lambda s: s.url)
assert before_servers == after_servers
tokens = list(db.query(orm.APIToken).filter(orm.APIToken.user_id == user.id))

View File

@@ -0,0 +1,10 @@
from .test_api import api_request, add_user
def test_create_named_server(app):
return
app.allow_named_servers = True
username = 'user'
servername = 'foo'
r = api_request(app, 'users', username, 'servers', servername, method='post')
r.raise_for_status()

View File

@@ -39,16 +39,13 @@ def test_server(db):
def test_user(db):
user = User(orm.User(name='kaylee',
state={'pid': 4234},
))
server = orm.Server()
user.servers.append(server)
user = User(orm.User(name='kaylee'))
db.add(user)
db.commit()
spawner = user.spawners['']
spawner.orm_spawner.state = {'pid': 4234}
assert user.name == 'kaylee'
assert user.server.ip == ''
assert user.state == {'pid': 4234}
assert user.orm_spawners[''].state == {'pid': 4234}
found = orm.User.find(db, 'kaylee')
assert found.name == user.name
@@ -160,7 +157,7 @@ def test_spawn_fails(db, io_loop):
with pytest.raises(RuntimeError) as exc:
io_loop.run_sync(user.spawn)
assert user.server is None
assert user.spawners[''].server is None
assert not user.running('')

View File

@@ -158,18 +158,18 @@ def test_check_routes(app, io_loop, username, endpoints):
# check a valid route exists for user
test_user = app.users[username]
before = sorted(io_loop.run_sync(app.proxy.get_all_routes))
assert test_user.proxy_spec in before
assert test_user.proxy_spec() in before
# check if a route is removed when user deleted
io_loop.run_sync(lambda: app.proxy.check_routes(app.users, app._service_map))
io_loop.run_sync(lambda: proxy.delete_user(test_user))
during = sorted(io_loop.run_sync(app.proxy.get_all_routes))
assert test_user.proxy_spec not in during
assert test_user.proxy_spec() not in during
# check if a route exists for user
io_loop.run_sync(lambda: app.proxy.check_routes(app.users, app._service_map))
after = sorted(io_loop.run_sync(app.proxy.get_all_routes))
assert test_user.proxy_spec in after
assert test_user.proxy_spec() in after
# check that before and after state are the same
assert before == after

View File

@@ -43,9 +43,9 @@ def setup():
def new_spawner(db, **kwargs):
user = kwargs.setdefault('user', User(db.query(orm.User).first(), {}))
kwargs.setdefault('cmd', [sys.executable, '-c', _echo_sleep])
kwargs.setdefault('hub', Hub())
kwargs.setdefault('user', User(db.query(orm.User).first(), {}))
kwargs.setdefault('notebook_dir', os.getcwd())
kwargs.setdefault('default_url', '/user/{username}/lab')
kwargs.setdefault('oauth_client_id', 'mock-client-id')
@@ -53,7 +53,7 @@ def new_spawner(db, **kwargs):
kwargs.setdefault('TERM_TIMEOUT', 1)
kwargs.setdefault('KILL_TIMEOUT', 1)
kwargs.setdefault('poll_interval', 1)
return LocalProcessSpawner(db=db, **kwargs)
return user._new_spawner('', spawner_class=LocalProcessSpawner, **kwargs)
@pytest.mark.gen_test
@@ -63,8 +63,6 @@ def test_spawner(db, request):
assert ip == '127.0.0.1'
assert isinstance(port, int)
assert port > 0
spawner.user.server.ip = ip
spawner.user.server.port = port
db.commit()
# wait for the process to get to the while True: loop
@@ -85,7 +83,7 @@ def wait_for_spawner(spawner, timeout=10):
"""
deadline = time.monotonic() + timeout
def wait():
return spawner.user.server.wait_up(timeout=1, http=True)
return spawner.server.wait_up(timeout=1, http=True)
while time.monotonic() < deadline:
status = yield spawner.poll()
assert status is None
@@ -104,8 +102,8 @@ def test_single_user_spawner(app, request):
spawner = user.spawner
spawner.cmd = ['jupyterhub-singleuser']
yield user.spawn()
assert user.server.ip == '127.0.0.1'
assert user.server.port > 0
assert spawner.server.ip == '127.0.0.1'
assert spawner.server.port > 0
yield wait_for_spawner(spawner)
status = yield spawner.poll()
assert status is None
@@ -233,10 +231,12 @@ def test_shell_cmd(db, tmpdir, request):
cmd=[sys.executable, '-m', 'jupyterhub.tests.mocksu'],
shell_cmd=['bash', '--rcfile', str(f), '-i', '-c'],
)
s.orm_spawner.server = orm.Server()
db.commit()
(ip, port) = yield s.start()
request.addfinalizer(s.stop)
s.user.server.ip = ip
s.user.server.port = port
s.server.ip = ip
s.server.port = port
db.commit()
yield wait_for_spawner(s)
r = requests.get('http://%s:%i/env' % (ip, port))

View File

@@ -13,7 +13,6 @@ from .utils import url_path_join, default_server_name
from . import orm
from ._version import _check_version, __version__
from .objects import Server
from traitlets import HasTraits, Any, Dict, observe, default
from .spawner import LocalProcessSpawner
@@ -105,12 +104,8 @@ class User(HasTraits):
db = change.new
if self._user_id is not None:
self.orm_user = db.query(orm.User).filter(orm.User.id == self._user_id).first()
for spawner in self.spawners.values():
spawner.db = db
if (spawner.server):
orm_server = spawner.server.orm_server
inspect(orm_server).session.expunge(orm_server)
db.add(orm_server)
for name, spawner in self.spawners.items():
spawner.orm_spawner = self.orm_user.orm_spawners[name]
_user_id = None
orm_user = Any(allow_none=True)
@@ -151,9 +146,11 @@ class User(HasTraits):
for name in self.orm_spawners:
self.spawners[name] = self._new_spawner(name)
def _new_spawner(self, name):
def _new_spawner(self, name, spawner_class=None, **kwargs):
"""Create a new spawner"""
self.log.debug("Creating Spawner for %s:%s", self.name, name)
if spawner_class is None:
spawner_class = self.spawner_class
self.log.debug("Creating %s for %s:%s", spawner_class, self.name, name)
orm_spawner = self.orm_spawners.get(name)
if orm_spawner is None:
@@ -165,16 +162,17 @@ class User(HasTraits):
# migrate user.state to spawner.state
orm_spawner.state = self.state
self.state = None
spawner = self.spawner_class(
spawn_kwargs = dict(
user=self,
db=self.db,
orm_spawner=orm_spawner,
hub=self.settings.get('hub'),
authenticator=self.authenticator,
config=self.settings.get('config'),
)
if orm_spawner.server:
spawner.server = Server(orm_spawner.server)
# update with kwargs. Mainly for testing.
spawn_kwargs.update(kwargs)
spawner = spawner_class(**spawn_kwargs)
spawner.load_state(orm_spawner.state or {})
return spawner
@@ -226,9 +224,9 @@ class User(HasTraits):
def proxy_spec(self, name=''):
if self.settings.get('subdomain_host'):
return url_path_join(self.domain, self.base_url, name)
return url_path_join(self.domain, self.base_url, name, '/')
else:
return url_path_join(self.base_url, name)
return url_path_join(self.base_url, name, '/')
@property
def domain(self):
@@ -286,13 +284,12 @@ class User(HasTraits):
api_token = self.new_api_token()
db.commit()
server = Server(orm_server=orm_server)
spawner = self.spawners[server_name]
spawner.orm_spawner.server = orm_server
server = spawner.server
# Passing server, server_name and options to the spawner
spawner.server = server
# Passing user_options to the spawner
spawner.user_options = options or {}
# we are starting a new server, make sure it doesn't restore state
spawner.clear_state()
@@ -426,8 +423,8 @@ class User(HasTraits):
self.last_activity = datetime.utcnow()
# remove server entry from db
if spawner.server is not None:
self.db.delete(spawner.server.orm_server)
spawner.server = None
self.db.delete(spawner.orm_spawner.server)
spawner.orm_spawner.server = None
if not spawner.will_resume:
# find and remove the API token if the spawner isn't
# going to re-use it next time