Merge pull request #50 from minrk/last_activity

store last_activity in the database
This commit is contained in:
Min RK
2014-09-22 20:02:37 -07:00
5 changed files with 84 additions and 29 deletions

View File

@@ -8,6 +8,7 @@ import getpass
import io import io
import logging import logging
import os import os
from datetime import datetime
from subprocess import Popen from subprocess import Popen
try: try:
@@ -36,8 +37,10 @@ from . import handlers, apihandlers
from . import orm from . import orm
from ._data import DATA_FILES_PATH from ._data import DATA_FILES_PATH
from .utils import url_path_join, random_hex, TimeoutError from .utils import (
url_path_join, random_hex, TimeoutError,
ISO8601_ms, ISO8601_s,
)
# classes for config # classes for config
from .auth import Authenticator, PAMAuthenticator from .auth import Authenticator, PAMAuthenticator
from .spawner import Spawner, LocalProcessSpawner from .spawner import Spawner, LocalProcessSpawner
@@ -111,7 +114,10 @@ class JupyterHubApp(Application):
Useful for daemonizing jupyterhub. Useful for daemonizing jupyterhub.
""" """
) )
proxy_check_interval = Integer(10, config=True, last_activity_interval = Integer(600, config=True,
help="Interval (in seconds) at which to update last-activity timestamps."
)
proxy_check_interval = Integer(30, config=True,
help="Interval (in seconds) at which to check if the proxy is running." help="Interval (in seconds) at which to check if the proxy is running."
) )
@@ -641,6 +647,23 @@ class JupyterHubApp(Application):
with io.open(self.config_file, encoding='utf8', mode='w') as f: with io.open(self.config_file, encoding='utf8', mode='w') as f:
f.write(config_text) f.write(config_text)
@gen.coroutine
def update_last_activity(self):
"""Update User.last_activity timestamps from the proxy"""
routes = yield self.proxy.fetch_routes()
for prefix, route in routes.items():
user = orm.User.find(self.db, route.get('user'))
if user is None:
self.log.warn("Found no user for route: %s", route)
continue
try:
dt = datetime.strptime(route['last_activity'], ISO8601_ms)
except Exception:
dt = datetime.strptime(route['last_activity'], ISO8601_s)
user.last_activity = max(user.last_activity, dt)
self.db.commit()
def start(self): def start(self):
"""Start the whole thing""" """Start the whole thing"""
if self.generate_config: if self.generate_config:
@@ -664,6 +687,10 @@ class JupyterHubApp(Application):
pc = PeriodicCallback(self.check_proxy, 1e3 * self.proxy_check_interval) pc = PeriodicCallback(self.check_proxy, 1e3 * self.proxy_check_interval)
pc.start() pc.start()
if self.last_activity_interval:
pc = PeriodicCallback(self.update_last_activity, 1e3 * self.last_activity_interval)
pc.start()
# start the webserver # start the webserver
http_server = tornado.httpserver.HTTPServer(self.tornado_application) http_server = tornado.httpserver.HTTPServer(self.tornado_application)
http_server.listen(self.hub_port) http_server.listen(self.hub_port)

View File

@@ -3,6 +3,7 @@
# Copyright (c) Jupyter Development Team. # Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
from datetime import datetime
import errno import errno
import json import json
import socket import socket
@@ -16,6 +17,7 @@ from sqlalchemy.types import TypeDecorator, VARCHAR
from sqlalchemy import ( from sqlalchemy import (
inspect, inspect,
Column, Integer, String, ForeignKey, Unicode, Binary, Boolean, Column, Integer, String, ForeignKey, Unicode, Binary, Boolean,
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, backref from sqlalchemy.orm import sessionmaker, relationship, backref
@@ -134,43 +136,47 @@ class Proxy(Base):
else: else:
return "<%s [unconfigured]>" % self.__class__.__name__ return "<%s [unconfigured]>" % self.__class__.__name__
def api_request(self, path, method='GET', body=None, client=None):
"""Make an authenticated API request of the proxy"""
client = client or AsyncHTTPClient()
url = url_path_join(self.api_server.url, path)
if isinstance(body, dict):
body = json.dumps(body)
self.log.debug("Fetching %s %s", method, url)
req = HTTPRequest(url,
method=method,
headers={'Authorization': 'token {}'.format(self.auth_token)},
body=body,
)
return client.fetch(req)
@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."""
self.log.info("Adding user %s to proxy %s => %s", self.log.info("Adding user %s to proxy %s => %s",
user.name, user.server.base_url, user.server.host, user.name, user.server.base_url, user.server.host,
) )
client = client or AsyncHTTPClient()
req = HTTPRequest(url_path_join( yield self.api_request(user.server.base_url,
self.api_server.url, method='POST',
user.server.base_url, body=dict(
),
method="POST",
headers={'Authorization': 'token {}'.format(self.auth_token)},
body=json.dumps(dict(
target=user.server.host, target=user.server.host,
user=user.name, user=user.name,
)), ),
client=client,
) )
res = yield client.fetch(req)
@gen.coroutine @gen.coroutine
def delete_user(self, user, client=None): def delete_user(self, user, client=None):
"""Remove a user's server to the proxy table.""" """Remove a user's server to the proxy table."""
self.log.info("Removing user %s from proxy", user.name) self.log.info("Removing user %s from proxy", user.name)
client = client or AsyncHTTPClient() yield self.api_request(user.server.base_url,
req = HTTPRequest(url_path_join( method='DELETE',
self.api_server.url, client=client,
user.server.base_url,
),
method="DELETE",
headers={'Authorization': 'token {}'.format(self.auth_token)},
) )
res = yield client.fetch(req)
@gen.coroutine @gen.coroutine
def add_all_users(self): def add_all_users(self):
"""Update the proxy table from the database. """Update the proxy table from the database.
@@ -186,6 +192,12 @@ class Proxy(Base):
for f in futures: for f in futures:
yield f yield f
@gen.coroutine
def fetch_routes(self, client=None):
"""Fetch the proxy's routes"""
resp = yield self.api_request('/', client=client)
raise gen.Return(json.loads(resp.body.decode('utf8', 'replace')))
class Hub(Base): class Hub(Base):
"""Bring it all together at the hub. """Bring it all together at the hub.
@@ -235,6 +247,7 @@ class User(Base):
_server_id = Column(Integer, ForeignKey('servers.id')) _server_id = Column(Integer, ForeignKey('servers.id'))
server = relationship(Server, primaryjoin=_server_id == Server.id) 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)
api_tokens = relationship("APIToken", backref="user") api_tokens = relationship("APIToken", backref="user")
cookie_tokens = relationship("CookieToken", backref="user") cookie_tokens = relationship("CookieToken", backref="user")
@@ -277,6 +290,7 @@ class User(Base):
@gen.coroutine @gen.coroutine
def spawn(self, spawner_class, base_url='/', hub=None, config=None): def spawn(self, spawner_class, base_url='/', hub=None, config=None):
"""Start the user's spawner"""
db = inspect(self).session db = inspect(self).session
if hub is None: if hub is None:
hub = db.query(Hub).first() hub = db.query(Hub).first()
@@ -302,6 +316,7 @@ class User(Base):
# store state # store state
self.state = spawner.get_state() self.state = spawner.get_state()
self.last_activity = datetime.utcnow()
db.commit() db.commit()
yield self.server.wait_up() yield self.server.wait_up()
@@ -309,6 +324,7 @@ class User(Base):
@gen.coroutine @gen.coroutine
def stop(self): def stop(self):
"""Stop the user's spawner"""
if self.spawner is None: if self.spawner is None:
return return
status = yield self.spawner.poll() status = yield self.spawner.poll()

View File

@@ -28,6 +28,10 @@ def random_port():
sock.close() sock.close()
return port return port
# ISO8601 for strptime with/without milliseconds
ISO8601_ms = '%Y-%m-%dT%H:%M:%S.%fZ'
ISO8601_s = '%Y-%m-%dT%H:%M:%SZ'
def random_hex(nbytes): def random_hex(nbytes):
"""Return nbytes random bytes as a unicode hex string """Return nbytes random bytes as a unicode hex string

View File

@@ -1,7 +1,7 @@
// Copyright (c) Jupyter Development Team. // Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License. // Distributed under the terms of the Modified BSD License.
require(["jquery", "bootstrap", "jhapi"], function ($, bs, JHAPI) { require(["jquery", "bootstrap", "moment", "jhapi"], function ($, bs, moment, JHAPI) {
"use strict"; "use strict";
var base_url = window.jhdata.base_url; var base_url = window.jhdata.base_url;
@@ -14,6 +14,12 @@ require(["jquery", "bootstrap", "jhapi"], function ($, bs, JHAPI) {
return element; return element;
}; };
$(".time-col").map(function (i, el) {
// convert ISO datestamps to nice momentjs ones
el = $(el);
el.text(moment(new Date(el.text())).fromNow());
});
$(".stop-server").click(function () { $(".stop-server").click(function () {
var el = $(this); var el = $(this);
var row = get_row(el); var row = get_row(el);

View File

@@ -6,14 +6,16 @@
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="4">Users</th> <th colspan="2">User</th>
<th colspan="3">Last Seen</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for u in users %} {% for u in users %}
<tr class="user-row" data-user="{{u.name}}" data-admin="{{u.admin}}"> <tr class="user-row" data-user="{{u.name}}" data-admin="{{u.admin}}">
<td class="name-col col-sm-4">{{u.name}}</td> <td class="name-col col-sm-2">{{u.name}}</td>
<td class="admin-col col-sm-4">{% if u.admin %}admin{% endif %}</td> <td class="admin-col col-sm-2">{% if u.admin %}admin{% endif %}</td>
<td class="time-col col-sm-4">{{u.last_activity.isoformat() + 'Z'}}</td>
<td class="server-col col-sm-2 text-center"> <td class="server-col col-sm-2 text-center">
{% if u.server %} {% if u.server %}
<span class="stop-server btn btn-xs btn-danger">stop server</span> <span class="stop-server btn btn-xs btn-danger">stop server</span>
@@ -30,8 +32,8 @@
</tr> </tr>
{% endfor %} {% endfor %}
<tr class="user-row add-user-row"> <tr class="user-row add-user-row">
<td colspan="4"> <td colspan="5">
<a id="add-user" class="col-xs-12 btn btn-primary">Add User</a> <a id="add-user" class="col-xs-12 btn btn-default">Add User</a>
</td> </td>
</tr> </tr>
</tbody> </tbody>