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

View File

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

View File

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

View File

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

View File

@@ -156,7 +156,10 @@ class MockHub(JupyterHub):
def init_signal(self): def init_signal(self):
pass pass
def load_config_file(self, *args, **kwargs):
pass
def start(self, argv=None): def start(self, argv=None):
self.db_file = NamedTemporaryFile() self.db_file = NamedTemporaryFile()
self.pid_file = NamedTemporaryFile(delete=False).name self.pid_file = NamedTemporaryFile(delete=False).name
@@ -172,8 +175,6 @@ class MockHub(JupyterHub):
# add an initial user # add an initial user
user = orm.User(name='user') user = orm.User(name='user')
self.db.add(user) self.db.add(user)
admin = orm.User(name='admin', admin=True)
self.db.add(admin)
self.db.commit() self.db.commit()
yield super(MockHub, self).start() yield super(MockHub, self).start()
yield self.hub.wait_up(http=True) 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 assert 'pid' in user.orm_spawners[''].state
app_user = get_app_user(app, name) app_user = get_app_user(app, name)
assert app_user.spawner is not None assert app_user.spawner is not None
spawner = app_user.spawner
assert app_user.spawner.user_options == options assert app_user.spawner.user_options == options
assert not app_user.spawner._spawn_pending assert not app_user.spawner._spawn_pending
status = io_loop.run_sync(app_user.spawner.poll) status = io_loop.run_sync(app_user.spawner.poll)
assert status is None 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) url = public_url(app, user)
r = requests.get(url) r = requests.get(url)
assert r.status_code == 200 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')) r = requests.get(ujoin(url, 'args'))
assert r.status_code == 200 assert r.status_code == 200
@@ -438,7 +439,7 @@ def test_spawn(app, io_loop):
assert status == 0 assert status == 0
# check that we cleaned up after ourselves # 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) after_servers = sorted(db.query(orm.Server), key=lambda s: s.url)
assert before_servers == after_servers assert before_servers == after_servers
tokens = list(db.query(orm.APIToken).filter(orm.APIToken.user_id == user.id)) 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): def test_user(db):
user = User(orm.User(name='kaylee', user = User(orm.User(name='kaylee'))
state={'pid': 4234},
))
server = orm.Server()
user.servers.append(server)
db.add(user) db.add(user)
db.commit() db.commit()
spawner = user.spawners['']
spawner.orm_spawner.state = {'pid': 4234}
assert user.name == 'kaylee' assert user.name == 'kaylee'
assert user.server.ip == '' assert user.orm_spawners[''].state == {'pid': 4234}
assert user.state == {'pid': 4234}
found = orm.User.find(db, 'kaylee') found = orm.User.find(db, 'kaylee')
assert found.name == user.name assert found.name == user.name
@@ -160,7 +157,7 @@ def test_spawn_fails(db, io_loop):
with pytest.raises(RuntimeError) as exc: with pytest.raises(RuntimeError) as exc:
io_loop.run_sync(user.spawn) io_loop.run_sync(user.spawn)
assert user.server is None assert user.spawners[''].server is None
assert not user.running('') 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 # check a valid route exists for user
test_user = app.users[username] test_user = app.users[username]
before = sorted(io_loop.run_sync(app.proxy.get_all_routes)) 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 # 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: app.proxy.check_routes(app.users, app._service_map))
io_loop.run_sync(lambda: proxy.delete_user(test_user)) io_loop.run_sync(lambda: proxy.delete_user(test_user))
during = sorted(io_loop.run_sync(app.proxy.get_all_routes)) 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 # check if a route exists for user
io_loop.run_sync(lambda: app.proxy.check_routes(app.users, app._service_map)) 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)) 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 # check that before and after state are the same
assert before == after assert before == after

View File

@@ -43,9 +43,9 @@ def setup():
def new_spawner(db, **kwargs): 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('cmd', [sys.executable, '-c', _echo_sleep])
kwargs.setdefault('hub', Hub()) kwargs.setdefault('hub', Hub())
kwargs.setdefault('user', User(db.query(orm.User).first(), {}))
kwargs.setdefault('notebook_dir', os.getcwd()) kwargs.setdefault('notebook_dir', os.getcwd())
kwargs.setdefault('default_url', '/user/{username}/lab') kwargs.setdefault('default_url', '/user/{username}/lab')
kwargs.setdefault('oauth_client_id', 'mock-client-id') kwargs.setdefault('oauth_client_id', 'mock-client-id')
@@ -53,7 +53,7 @@ def new_spawner(db, **kwargs):
kwargs.setdefault('TERM_TIMEOUT', 1) kwargs.setdefault('TERM_TIMEOUT', 1)
kwargs.setdefault('KILL_TIMEOUT', 1) kwargs.setdefault('KILL_TIMEOUT', 1)
kwargs.setdefault('poll_interval', 1) kwargs.setdefault('poll_interval', 1)
return LocalProcessSpawner(db=db, **kwargs) return user._new_spawner('', spawner_class=LocalProcessSpawner, **kwargs)
@pytest.mark.gen_test @pytest.mark.gen_test
@@ -63,8 +63,6 @@ def test_spawner(db, request):
assert ip == '127.0.0.1' assert ip == '127.0.0.1'
assert isinstance(port, int) assert isinstance(port, int)
assert port > 0 assert port > 0
spawner.user.server.ip = ip
spawner.user.server.port = port
db.commit() db.commit()
# wait for the process to get to the while True: loop # 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 deadline = time.monotonic() + timeout
def wait(): 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: while time.monotonic() < deadline:
status = yield spawner.poll() status = yield spawner.poll()
assert status is None assert status is None
@@ -104,8 +102,8 @@ def test_single_user_spawner(app, request):
spawner = user.spawner spawner = user.spawner
spawner.cmd = ['jupyterhub-singleuser'] spawner.cmd = ['jupyterhub-singleuser']
yield user.spawn() yield user.spawn()
assert user.server.ip == '127.0.0.1' assert spawner.server.ip == '127.0.0.1'
assert user.server.port > 0 assert spawner.server.port > 0
yield wait_for_spawner(spawner) yield wait_for_spawner(spawner)
status = yield spawner.poll() status = yield spawner.poll()
assert status is None assert status is None
@@ -233,10 +231,12 @@ def test_shell_cmd(db, tmpdir, request):
cmd=[sys.executable, '-m', 'jupyterhub.tests.mocksu'], cmd=[sys.executable, '-m', 'jupyterhub.tests.mocksu'],
shell_cmd=['bash', '--rcfile', str(f), '-i', '-c'], shell_cmd=['bash', '--rcfile', str(f), '-i', '-c'],
) )
s.orm_spawner.server = orm.Server()
db.commit()
(ip, port) = yield s.start() (ip, port) = yield s.start()
request.addfinalizer(s.stop) request.addfinalizer(s.stop)
s.user.server.ip = ip s.server.ip = ip
s.user.server.port = port s.server.port = port
db.commit() db.commit()
yield wait_for_spawner(s) yield wait_for_spawner(s)
r = requests.get('http://%s:%i/env' % (ip, port)) 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 . import orm
from ._version import _check_version, __version__ from ._version import _check_version, __version__
from .objects import Server
from traitlets import HasTraits, Any, Dict, observe, default from traitlets import HasTraits, Any, Dict, observe, default
from .spawner import LocalProcessSpawner from .spawner import LocalProcessSpawner
@@ -105,12 +104,8 @@ class User(HasTraits):
db = change.new db = change.new
if self._user_id is not None: if self._user_id is not None:
self.orm_user = db.query(orm.User).filter(orm.User.id == self._user_id).first() self.orm_user = db.query(orm.User).filter(orm.User.id == self._user_id).first()
for spawner in self.spawners.values(): for name, spawner in self.spawners.items():
spawner.db = db spawner.orm_spawner = self.orm_user.orm_spawners[name]
if (spawner.server):
orm_server = spawner.server.orm_server
inspect(orm_server).session.expunge(orm_server)
db.add(orm_server)
_user_id = None _user_id = None
orm_user = Any(allow_none=True) orm_user = Any(allow_none=True)
@@ -151,9 +146,11 @@ class User(HasTraits):
for name in self.orm_spawners: for name in self.orm_spawners:
self.spawners[name] = self._new_spawner(name) 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""" """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) orm_spawner = self.orm_spawners.get(name)
if orm_spawner is None: if orm_spawner is None:
@@ -165,16 +162,17 @@ class User(HasTraits):
# migrate user.state to spawner.state # migrate user.state to spawner.state
orm_spawner.state = self.state orm_spawner.state = self.state
self.state = None self.state = None
spawner = self.spawner_class(
spawn_kwargs = dict(
user=self, user=self,
db=self.db,
orm_spawner=orm_spawner, orm_spawner=orm_spawner,
hub=self.settings.get('hub'), hub=self.settings.get('hub'),
authenticator=self.authenticator, authenticator=self.authenticator,
config=self.settings.get('config'), config=self.settings.get('config'),
) )
if orm_spawner.server: # update with kwargs. Mainly for testing.
spawner.server = Server(orm_spawner.server) spawn_kwargs.update(kwargs)
spawner = spawner_class(**spawn_kwargs)
spawner.load_state(orm_spawner.state or {}) spawner.load_state(orm_spawner.state or {})
return spawner return spawner
@@ -226,9 +224,9 @@ class User(HasTraits):
def proxy_spec(self, name=''): def proxy_spec(self, name=''):
if self.settings.get('subdomain_host'): 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: else:
return url_path_join(self.base_url, name) return url_path_join(self.base_url, name, '/')
@property @property
def domain(self): def domain(self):
@@ -286,13 +284,12 @@ class User(HasTraits):
api_token = self.new_api_token() api_token = self.new_api_token()
db.commit() db.commit()
server = Server(orm_server=orm_server)
spawner = self.spawners[server_name] spawner = self.spawners[server_name]
spawner.orm_spawner.server = orm_server spawner.orm_spawner.server = orm_server
server = spawner.server
# Passing server, server_name and options to the spawner # Passing user_options to the spawner
spawner.server = server
spawner.user_options = options or {} spawner.user_options = options or {}
# we are starting a new server, make sure it doesn't restore state # we are starting a new server, make sure it doesn't restore state
spawner.clear_state() spawner.clear_state()
@@ -426,8 +423,8 @@ class User(HasTraits):
self.last_activity = datetime.utcnow() self.last_activity = datetime.utcnow()
# remove server entry from db # remove server entry from db
if spawner.server is not None: if spawner.server is not None:
self.db.delete(spawner.server.orm_server) self.db.delete(spawner.orm_spawner.server)
spawner.server = None spawner.orm_spawner.server = None
if not spawner.will_resume: if not spawner.will_resume:
# find and remove the API token if the spawner isn't # find and remove the API token if the spawner isn't
# going to re-use it next time # going to re-use it next time