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