Merge pull request #907 from barrachri/adding_multi_server_features_orm

Allow users to have multiple servers (in db)
This commit is contained in:
Min RK
2017-01-11 15:19:47 +01:00
committed by GitHub
5 changed files with 112 additions and 60 deletions

View File

@@ -1095,7 +1095,10 @@ class JupyterHub(Application):
# 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 user.server else self.log.debug
log("%s not running.", user.name) log("%s not running.", user.name)
user.server = None # remove all server or servers entry from db related to the user
for server in user.servers:
db.delete(server)
db.commit()
user_summaries.append(_user_summary(user)) user_summaries.append(_user_summary(user))

View File

@@ -17,8 +17,10 @@ from sqlalchemy import (
DateTime, DateTime,
) )
from sqlalchemy.ext.declarative import declarative_base, declared_attr from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy.orm import sessionmaker, relationship from sqlalchemy.orm import sessionmaker, relationship, backref
from sqlalchemy.pool import StaticPool from sqlalchemy.pool import StaticPool
from sqlalchemy.schema import Index, UniqueConstraint
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.sql.expression import bindparam from sqlalchemy.sql.expression import bindparam
from sqlalchemy import create_engine, Table from sqlalchemy import create_engine, Table
@@ -68,6 +70,9 @@ class Server(Base):
base_url = Column(Unicode(255), default='/') base_url = Column(Unicode(255), default='/')
cookie_name = Column(Unicode(255), default='cookie') cookie_name = Column(Unicode(255), default='cookie')
# added to handle multi-server feature
last_activity = Column(DateTime, default=datetime.utcnow)
def __repr__(self): def __repr__(self):
return "<Server(%s:%s)>" % (self.ip, self.port) return "<Server(%s:%s)>" % (self.ip, self.port)
@@ -181,6 +186,8 @@ class Proxy(Base):
client=client, client=client,
) )
# FIX-ME
# we need to add a reference to a specific server
@gen.coroutine @gen.coroutine
def add_user(self, user, client=None): def add_user(self, user, client=None):
"""Add a user's server to the proxy table.""" """Add a user's server to the proxy table."""
@@ -248,6 +255,8 @@ class Proxy(Base):
resp = yield self.api_request('', client=client) resp = yield self.api_request('', client=client)
return json.loads(resp.body.decode('utf8', 'replace')) return json.loads(resp.body.decode('utf8', 'replace'))
# FIX-ME
# we need to add a reference to a specific server
@gen.coroutine @gen.coroutine
def check_routes(self, user_dict, service_dict, routes=None): def check_routes(self, user_dict, service_dict, routes=None):
"""Check that all users are properly routed on the proxy""" """Check that all users are properly routed on the proxy"""
@@ -334,7 +343,6 @@ class Group(Base):
@classmethod @classmethod
def find(cls, db, name): def find(cls, db, name):
"""Find a group by name. """Find a group by name.
Returns None if not found. Returns None if not found.
""" """
return db.query(cls).filter(cls.name==name).first() return db.query(cls).filter(cls.name==name).first()
@@ -343,8 +351,10 @@ class Group(Base):
class User(Base): class User(Base):
"""The User table """The User table
Each user has a single server, Each user can have one or more single user notebook servers.
and multiple tokens used for authorization.
Each single user notebook server will have a unique token for authorization.
Therefore, a user with multiple notebook servers will have multiple tokens.
API tokens grant access to the Hub's REST API. API tokens grant access to the Hub's REST API.
These are used by single-user servers to authenticate requests, These are used by single-user servers to authenticate requests,
@@ -355,13 +365,17 @@ class User(Base):
A `state` column contains a JSON dict, A `state` column contains a JSON dict,
used for restoring state of a Spawner. used for restoring state of a Spawner.
`servers` is a list that contains a reference for each of the user's single user notebook servers.
The method `server` returns the first entry in the user's `servers` list.
""" """
__tablename__ = 'users' __tablename__ = 'users'
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)
# should we allow multiple servers per user?
_server_id = Column(Integer, ForeignKey('servers.id', ondelete="SET NULL")) servers = association_proxy("user_to_servers", "server", creator=lambda server: UserServer(server=server))
server = relationship(Server, primaryjoin=_server_id == Server.id)
admin = Column(Boolean, default=False) admin = Column(Boolean, default=False)
last_activity = Column(DateTime, default=datetime.utcnow) last_activity = Column(DateTime, default=datetime.utcnow)
@@ -377,6 +391,16 @@ class User(Base):
other_user_cookies = set([]) other_user_cookies = set([])
@property
def server(self):
"""Returns the first element of servers.
Returns None if the list is empty.
"""
if len(self.servers) == 0:
return None
else:
return self.servers[0]
def __repr__(self): def __repr__(self):
if self.server: if self.server:
return "<{cls}({name}@{ip}:{port})>".format( return "<{cls}({name}@{ip}:{port})>".format(
@@ -401,12 +425,40 @@ class User(Base):
@classmethod @classmethod
def find(cls, db, name): def find(cls, db, name):
"""Find a user by name. """Find a user by name.
Returns None if not found. Returns None if not found.
""" """
return db.query(cls).filter(cls.name==name).first() return db.query(cls).filter(cls.name==name).first()
class UserServer(Base):
"""The UserServer table
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'
_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):
"""A service run with JupyterHub """A service run with JupyterHub
@@ -414,7 +466,6 @@ class Service(Base):
A service can have API tokens for accessing the Hub's API A service can have API tokens for accessing the Hub's API
It has: It has:
- name - name
- admin - admin
- api tokens - api tokens
@@ -441,7 +492,6 @@ class Service(Base):
def new_api_token(self, token=None): def new_api_token(self, token=None):
"""Create a new API token """Create a new API token
If `token` is given, load that token. If `token` is given, load that token.
""" """
return APIToken.new(token=token, service=self) return APIToken.new(token=token, service=self)

View File

@@ -30,8 +30,8 @@ def db():
_db = orm.new_session_factory('sqlite:///:memory:', echo=True)() _db = orm.new_session_factory('sqlite:///:memory:', echo=True)()
user = orm.User( user = orm.User(
name=getuser(), name=getuser(),
server=orm.Server(),
) )
user.servers.append(orm.Server())
hub = orm.Hub( hub = orm.Hub(
server=orm.Server(), server=orm.Server(),
) )

View File

@@ -65,9 +65,10 @@ def test_hub(db):
def test_user(db): def test_user(db):
user = orm.User(name='kaylee', user = orm.User(name='kaylee',
server=orm.Server(),
state={'pid': 4234}, state={'pid': 4234},
) )
server = orm.Server()
user.servers.append(server)
db.add(user) db.add(user)
db.commit() db.commit()
assert user.name == 'kaylee' assert user.name == 'kaylee'

View File

@@ -198,12 +198,12 @@ class User(HasTraits):
def spawn(self, options=None): def spawn(self, options=None):
"""Start the user's spawner""" """Start the user's spawner"""
db = self.db db = self.db
server = orm.Server(
self.server = orm.Server(
cookie_name=self.cookie_name, cookie_name=self.cookie_name,
base_url=self.base_url, base_url=self.base_url,
) )
db.add(self.server) self.servers.append(server)
db.add(self)
db.commit() db.commit()
api_token = self.new_api_token() api_token = self.new_api_token()
@@ -315,11 +315,10 @@ class User(HasTraits):
spawner.clear_state() spawner.clear_state()
self.state = spawner.get_state() self.state = spawner.get_state()
self.last_activity = datetime.utcnow() self.last_activity = datetime.utcnow()
# cleanup server entry, API token from defunct server # Cleanup defunct servers: delete entry and API token for each server
if self.server: for server in self.servers:
# cleanup server entry from db # remove server entry from db
self.db.delete(self.server) self.db.delete(server)
self.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
@@ -335,4 +334,3 @@ class User(HasTraits):
yield gen.maybe_future( yield gen.maybe_future(
auth.post_spawn_stop(self, spawner) auth.post_spawn_stop(self, spawner)
) )