mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-10 11:33:01 +00:00
create Spawners table
for named servers removes User.servers
This commit is contained in:
@@ -1129,29 +1129,30 @@ class JupyterHub(Application):
|
||||
for orm_user in db.query(orm.User):
|
||||
self.users[orm_user.id] = user = User(orm_user, self.tornado_settings)
|
||||
self.log.debug("Loading state for %s from db", user.name)
|
||||
spawner = user.spawner
|
||||
status = 0
|
||||
if user.server:
|
||||
try:
|
||||
status = yield spawner.poll()
|
||||
except Exception:
|
||||
self.log.exception("Failed to poll spawner for %s, assuming the spawner is not running.", user.name)
|
||||
status = -1
|
||||
for name, spawner in user.spawners.items():
|
||||
status = 0
|
||||
if spawner.server:
|
||||
try:
|
||||
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))
|
||||
status = -1
|
||||
|
||||
if status is None:
|
||||
self.log.info("%s still running", user.name)
|
||||
spawner.add_poll_callback(user_stopped, user)
|
||||
spawner.start_polling()
|
||||
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.
|
||||
log = self.log.warning if user.server else self.log.debug
|
||||
log("%s not running.", user.name)
|
||||
# remove all server or servers entry from db related to the user
|
||||
for server in user.servers:
|
||||
db.delete(server)
|
||||
db.commit()
|
||||
if status is None:
|
||||
self.log.info("%s still running", user.name)
|
||||
spawner.add_poll_callback(user_stopped, user)
|
||||
spawner.start_polling()
|
||||
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.
|
||||
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:
|
||||
db.delete(spawner.orm_spawner.server)
|
||||
db.commit()
|
||||
|
||||
user_summaries.append(_user_summary(user))
|
||||
|
||||
|
@@ -66,7 +66,6 @@ class Server(Base):
|
||||
__tablename__ = 'servers'
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
name = Column(Unicode(32), default='') # must be unique between user's servers
|
||||
proto = Column(Unicode(15), default='http')
|
||||
ip = Column(Unicode(255), default='') # could also be a DNS name
|
||||
port = Column(Integer, default=random_port)
|
||||
@@ -133,7 +132,10 @@ class User(Base):
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(Unicode(1023), unique=True)
|
||||
|
||||
servers = association_proxy("user_to_servers", "server", creator=lambda server: UserServer(server=server))
|
||||
_orm_spawners = relationship("Spawner", backref="user")
|
||||
@property
|
||||
def orm_spawners(self):
|
||||
return {s.name: s for s in self._orm_spawners}
|
||||
|
||||
admin = Column(Boolean, default=False)
|
||||
last_activity = Column(DateTime, default=datetime.utcnow)
|
||||
@@ -149,19 +151,12 @@ class User(Base):
|
||||
groups = relationship('Group', secondary='user_group_map', back_populates='users')
|
||||
|
||||
def __repr__(self):
|
||||
if self.servers:
|
||||
server = self.servers[0]
|
||||
return "<{cls}({name}@{ip}:{port})>".format(
|
||||
cls=self.__class__.__name__,
|
||||
name=self.name,
|
||||
ip=server.ip,
|
||||
port=server.port,
|
||||
)
|
||||
else:
|
||||
return "<{cls}({name} [unconfigured])>".format(
|
||||
cls=self.__class__.__name__,
|
||||
name=self.name,
|
||||
)
|
||||
return "<{cls}({name} {running}/{total} running)>".format(
|
||||
cls=self.__class__.__name__,
|
||||
name=self.name,
|
||||
total=len(self._orm_spawners),
|
||||
running=sum(bool(s.server) for s in self._orm_spawners),
|
||||
)
|
||||
|
||||
def new_api_token(self, token=None):
|
||||
"""Create a new API token
|
||||
@@ -177,34 +172,18 @@ class User(Base):
|
||||
"""
|
||||
return db.query(cls).filter(cls.name == name).first()
|
||||
|
||||
class Spawner(Base):
|
||||
""""State about a Spawner"""
|
||||
__tablename__ = 'spawners'
|
||||
|
||||
class UserServer(Base):
|
||||
"""The UserServer table
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'))
|
||||
|
||||
A table storing the One-To-Many relationship between a user and servers.
|
||||
Each user may have one or more servers.
|
||||
A server can have only one (1) user. This condition is maintained by UniqueConstraint.
|
||||
"""
|
||||
__tablename__ = 'users_servers'
|
||||
server_id = Column(Integer, ForeignKey('servers.id'))
|
||||
server = relationship(Server)
|
||||
|
||||
_user_id = Column(Integer, ForeignKey('users.id'), primary_key=True)
|
||||
_server_id = Column(Integer, ForeignKey('servers.id'), primary_key=True)
|
||||
|
||||
user = relationship(User, backref=backref('user_to_servers', cascade='all, delete-orphan'))
|
||||
server = relationship(Server, backref=backref('server_to_users', cascade='all, delete-orphan')
|
||||
)
|
||||
|
||||
__table_args__ = (UniqueConstraint('_server_id'),
|
||||
Index('server_user_index', '_server_id', '_user_id'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "<{cls}({name}@{ip}:{port})>".format(
|
||||
cls=self.__class__.__name__,
|
||||
name=self.user.name,
|
||||
ip=self.server.ip,
|
||||
port=self.server.port,
|
||||
)
|
||||
state = Column(JSONDict)
|
||||
name = Column(Unicode(512))
|
||||
|
||||
|
||||
class Service(Base):
|
||||
|
@@ -51,6 +51,7 @@ class Spawner(LoggingConfigurable):
|
||||
_stop_pending = False
|
||||
_waiting_for_response = False
|
||||
|
||||
orm_spawner = Any()
|
||||
db = Any()
|
||||
user = Any()
|
||||
hub = Any()
|
||||
|
@@ -405,7 +405,7 @@ def test_spawn(app, io_loop):
|
||||
data=json.dumps(options),
|
||||
)
|
||||
assert r.status_code == 201
|
||||
assert 'pid' in user.state['']
|
||||
assert 'pid' in user.orm_spawners[''].state
|
||||
app_user = get_app_user(app, name)
|
||||
assert app_user.spawner is not None
|
||||
assert app_user.spawner.user_options == options
|
||||
@@ -433,7 +433,7 @@ def test_spawn(app, io_loop):
|
||||
r = api_request(app, 'users', name, 'server', method='delete')
|
||||
assert r.status_code == 204
|
||||
|
||||
assert 'pid' not in user.state.get('', {})
|
||||
assert 'pid' not in user.orm_spawners[''].state
|
||||
status = io_loop.run_sync(app_user.spawner.poll)
|
||||
assert status == 0
|
||||
|
||||
@@ -531,7 +531,7 @@ def test_cookie(app):
|
||||
user = add_user(db, app=app, name=name)
|
||||
r = api_request(app, 'users', name, 'server', method='post')
|
||||
assert r.status_code == 201
|
||||
assert 'pid' in user.state['']
|
||||
assert 'pid' in user.orm_spawners[''].state
|
||||
app_user = get_app_user(app, name)
|
||||
|
||||
cookies = app.login_user(name)
|
||||
|
@@ -153,8 +153,8 @@ def test_spawner_poll(db, io_loop):
|
||||
assert status is None
|
||||
if user.state is None:
|
||||
user.state = {}
|
||||
user.state[''] = first_spawner.get_state()
|
||||
assert 'pid' in user.state['']
|
||||
first_spawner.orm_spawner.state = first_spawner.get_state()
|
||||
assert 'pid' in first_spawner.orm_spawner.state
|
||||
|
||||
# create a new Spawner, loading from state of previous
|
||||
spawner = new_spawner(db, user=first_spawner.user)
|
||||
|
@@ -147,17 +147,34 @@ class User(HasTraits):
|
||||
self.settings.get('base_url', '/'), 'user', self.escaped_name) + '/'
|
||||
|
||||
self.spawners = _SpawnerDict(self._new_spawner)
|
||||
# load existing named spawners
|
||||
for name in self.orm_spawners:
|
||||
self.log.debug("Loading spawner %s:%s", self.name, name)
|
||||
self.spawners[name] = self._new_spawner(name)
|
||||
|
||||
def _new_spawner(self, name):
|
||||
"""Create a new spawner"""
|
||||
orm_spawner = self.orm_spawners.get(name)
|
||||
if orm_spawner is None:
|
||||
orm_spawner = orm.Spawner(user=self.orm_user, name=name)
|
||||
self.db.add(orm_spawner)
|
||||
self.db.commit()
|
||||
assert name in self.orm_spawners
|
||||
if name == '' and self.state:
|
||||
# migrate user.state to spawner.state
|
||||
orm_spawner.state = self.state
|
||||
self.state = None
|
||||
spawner = self.spawner_class(
|
||||
user=self,
|
||||
db=self.db,
|
||||
orm_spawner=orm_spawner,
|
||||
hub=self.settings.get('hub'),
|
||||
authenticator=self.authenticator,
|
||||
config=self.settings.get('config'),
|
||||
)
|
||||
spawner.load_state((self.state or {}).get(name, {}))
|
||||
if orm_spawner.server:
|
||||
spawner.server = Server(orm_spawner.server)
|
||||
spawner.load_state(orm_spawner.state or {})
|
||||
return spawner
|
||||
|
||||
# singleton property, self.spawner maps onto spawner with empty server_name
|
||||
@@ -198,10 +215,7 @@ class User(HasTraits):
|
||||
|
||||
@property
|
||||
def server(self):
|
||||
if len(self.servers) == 0:
|
||||
return None
|
||||
else:
|
||||
return Server(orm_server=self.servers[0])
|
||||
return self.spawner.server
|
||||
|
||||
@property
|
||||
def escaped_name(self):
|
||||
@@ -264,10 +278,9 @@ class User(HasTraits):
|
||||
base_url = url_path_join(self.base_url, server_name) + '/'
|
||||
|
||||
orm_server = orm.Server(
|
||||
name=server_name,
|
||||
base_url=base_url,
|
||||
)
|
||||
self.servers.append(orm_server)
|
||||
db.add(orm_server)
|
||||
|
||||
api_token = self.new_api_token()
|
||||
db.commit()
|
||||
@@ -275,6 +288,7 @@ class User(HasTraits):
|
||||
server = Server(orm_server=orm_server)
|
||||
|
||||
spawner = self.spawners[server_name]
|
||||
spawner.orm_spawner.server = orm_server
|
||||
|
||||
# Passing server, server_name and options to the spawner
|
||||
spawner.server = server
|
||||
@@ -353,7 +367,7 @@ class User(HasTraits):
|
||||
# store state
|
||||
if self.state is None:
|
||||
self.state = {}
|
||||
self.state[server_name] = spawner.get_state()
|
||||
spawner.orm_spawner.state = spawner.get_state()
|
||||
self.last_activity = datetime.utcnow()
|
||||
db.commit()
|
||||
spawner._waiting_for_response = True
|
||||
@@ -407,7 +421,7 @@ class User(HasTraits):
|
||||
if status is None:
|
||||
yield spawner.stop()
|
||||
spawner.clear_state()
|
||||
self.state = spawner.get_state()
|
||||
spawner.orm_spawner.state = spawner.get_state()
|
||||
self.last_activity = datetime.utcnow()
|
||||
# remove server entry from db
|
||||
if spawner.server is not None:
|
||||
|
@@ -225,7 +225,7 @@ def default_server_name(user):
|
||||
|
||||
Will be the first available integer string, e.g. '1' or '2'.
|
||||
"""
|
||||
existing_names = { server.name for server in user.servers }
|
||||
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)
|
||||
|
Reference in New Issue
Block a user