Handle named servers in UserSpawnHandler, home, admin pages

Made CSS and HTML (and Jinja2) of admin page compatible with named servers.
This commit is contained in:
William Krinsman
2018-08-15 15:11:43 -07:00
committed by Min RK
parent 571ca2dec6
commit 02cb5ec076
9 changed files with 388 additions and 66 deletions

View File

@@ -20,7 +20,7 @@ from sqlalchemy.exc import SQLAlchemyError
from tornado.log import app_log
from tornado.httputil import url_concat, HTTPHeaders
from tornado.ioloop import IOLoop
from tornado.web import RequestHandler
from tornado.web import RequestHandler, MissingArgumentError
from tornado import gen, web
from .. import __version__
@@ -854,10 +854,10 @@ class BaseHandler(RequestHandler):
await self.proxy.delete_user(user, server_name)
await user.stop(server_name)
async def stop_single_user(self, user, name=''):
if name not in user.spawners:
raise KeyError("User %s has no such spawner %r", user.name, name)
spawner = user.spawners[name]
async def stop_single_user(self, user, server_name=''):
if server_name not in user.spawners:
raise KeyError("User %s has no such spawner %r", user.name, server_name)
spawner = user.spawners[server_name]
if spawner.pending:
raise RuntimeError("%s pending %s" % (spawner._log_name, spawner.pending))
# set user._stop_pending before doing anything async
@@ -873,8 +873,8 @@ class BaseHandler(RequestHandler):
"""
tic = IOLoop.current().time()
try:
await self.proxy.delete_user(user, name)
await user.stop(name)
await self.proxy.delete_user(user, server_name)
await user.stop(server_name)
finally:
spawner._stop_pending = False
toc = IOLoop.current().time()
@@ -885,7 +885,7 @@ class BaseHandler(RequestHandler):
await gen.with_timeout(timedelta(seconds=self.slow_stop_timeout), stop())
except gen.TimeoutError:
# hit timeout, but stop is still pending
self.log.warning("User %s:%s server is slow to stop", user.name, name)
self.log.warning("User %s:%s server is slow to stop", user.name, server_name)
#---------------------------------------------------------------
# template rendering
@@ -1019,7 +1019,7 @@ class PrefixRedirectHandler(BaseHandler):
class UserSpawnHandler(BaseHandler):
"""Redirect requests to /user/name/* handled by the Hub.
"""Redirect requests to /user/user_name/* handled by the Hub.
If logged in, spawn a single-user server and redirect request.
If a user, alice, requests /user/bob/notebooks/mynotebook.ipynb,
@@ -1035,21 +1035,21 @@ class UserSpawnHandler(BaseHandler):
self.write(json.dumps({"message": "%s is not running" % user.name}))
self.finish()
async def get(self, name, user_path):
async def get(self, user_name, user_path):
if not user_path:
user_path = '/'
current_user = self.current_user
if (
current_user
and current_user.name != name
and current_user.name != user_name
and current_user.admin
and self.settings.get('admin_access', False)
):
# allow admins to spawn on behalf of users
user = self.find_user(name)
user = self.find_user(user_name)
if user is None:
# no such user
raise web.HTTPError(404, "No such user %s" % name)
raise web.HTTPError(404, "No such user %s" % user_name)
self.log.info("Admin %s requesting spawn on behalf of %s",
current_user.name, user.name)
admin_spawn = True
@@ -1059,7 +1059,7 @@ class UserSpawnHandler(BaseHandler):
admin_spawn = False
# For non-admins, we should spawn if the user matches
# otherwise redirect users to their own server
should_spawn = (current_user and current_user.name == name)
should_spawn = (current_user and current_user.name == user_name)
if "api" in user_path.split("/") and not user.active:
# API request for not-running server (e.g. notebook UI left open)
@@ -1071,7 +1071,7 @@ class UserSpawnHandler(BaseHandler):
# if spawning fails for any reason, point users to /hub/home to retry
self.extra_error_html = self.spawn_home_error
# If people visit /user/:name directly on the Hub,
# If people visit /user/:user_name directly on the Hub,
# the redirects will just loop, because the proxy is bypassed.
# Try to check for that and warn,
# though the user-facing behavior is unchanged
@@ -1087,7 +1087,11 @@ class UserSpawnHandler(BaseHandler):
""", self.request.full_url(), self.proxy.public_url)
# logged in as valid user, check for pending spawn
spawner = user.spawner
if self.allow_named_servers:
server_name = self.get_argument('server', '')
else:
server_name = ''
spawner = user.spawners[server_name]
# First, check for previous failure.
if (
@@ -1152,7 +1156,7 @@ class UserSpawnHandler(BaseHandler):
{'next': self.request.uri}))
return
else:
await self.spawn_single_user(user)
await self.spawn_single_user(user, server_name)
# spawn didn't finish, show pending page
if spawner.pending:
@@ -1208,7 +1212,7 @@ class UserSpawnHandler(BaseHandler):
url_parts = urlparse(target)
query_parts = parse_qs(url_parts.query)
query_parts['redirects'] = redirects + 1
url_parts = url_parts._replace(query=urlencode(query_parts))
url_parts = url_parts._replace(query=urlencode(query_parts, doseq=True))
target = urlunparse(url_parts)
else:
target = url_concat(target, {'redirects': 1})
@@ -1276,7 +1280,7 @@ class AddSlashHandler(BaseHandler):
default_handlers = [
(r'', AddSlashHandler), # add trailing / to `/hub`
(r'/user/([^/]+)(/.*)?', UserSpawnHandler),
(r'/user/(?P<user_name>[^/]+)(?P<user_path>/.*)?', UserSpawnHandler),
(r'/user-redirect/(.*)?', UserRedirectHandler),
(r'/security/csp-report', CSPReportHandler),
]

View File

@@ -50,13 +50,18 @@ class HomeHandler(BaseHandler):
# trigger poll_and_notify event in case of a server that died
await user.spawner.poll_and_notify()
# send the user to /spawn if they aren't running or pending a spawn,
# send the user to /spawn if they have no active servers,
# to establish that this is an explicit spawn request rather
# than an implicit one, which can be caused by any link to `/user/:name`
url = user.url if user.spawner.active else url_path_join(self.hub.base_url, 'spawn')
# than an implicit one, which can be caused by any link to `/user/:name(/:server_name)`
url = url_path_join(self.hub.base_url, 'user', user.name) if user.active else url_path_join(self.hub.base_url, 'spawn')
html = self.render_template('home.html',
user=user,
url=url,
allow_named_servers=self.allow_named_servers,
url_path_join=url_path_join,
# can't use user.spawners because the stop method of User pops named servers from user.spawners when they're stopped
spawners = user.orm_user._orm_spawners,
default_server = user.spawner,
)
self.finish(html)
@@ -214,11 +219,12 @@ class AdminHandler(BaseHandler):
running = [ u for u in users if u.running ]
html = self.render_template('admin.html',
user=self.current_user,
current_user=self.current_user,
admin_access=self.settings.get('admin_access', False),
users=users,
running=running,
sort={s:o for s,o in zip(sorts, orders)},
allow_named_servers=self.allow_named_servers,
)
self.finish(html)

View File

@@ -182,19 +182,19 @@ class User:
await self.save_auth_state(auth_state)
return auth_state
def _new_spawner(self, name, spawner_class=None, **kwargs):
def _new_spawner(self, server_name, spawner_class=None, **kwargs):
"""Create a new spawner"""
if spawner_class is None:
spawner_class = self.spawner_class
self.log.debug("Creating %s for %s:%s", spawner_class, self.name, name)
self.log.debug("Creating %s for %s:%s", spawner_class, self.name, server_name)
orm_spawner = self.orm_spawners.get(name)
orm_spawner = self.orm_spawners.get(server_name)
if orm_spawner is None:
orm_spawner = orm.Spawner(user=self.orm_user, name=name)
orm_spawner = orm.Spawner(user=self.orm_user, name=server_name)
self.db.add(orm_spawner)
self.db.commit()
assert name in self.orm_spawners
if name == '' and self.state:
assert server_name in self.orm_spawners
if server_name == '' and self.state:
# migrate user.state to spawner.state
orm_spawner.state = self.state
self.state = None
@@ -202,15 +202,15 @@ class User:
# use fully quoted name for client_id because it will be used in cookie-name
# self.escaped_name may contain @ which is legal in URLs but not cookie keys
client_id = 'jupyterhub-user-%s' % quote(self.name)
if name:
client_id = '%s-%s' % (client_id, quote(name))
if server_name:
client_id = '%s-%s' % (client_id, quote(server_name))
spawn_kwargs = dict(
user=self,
orm_spawner=orm_spawner,
hub=self.settings.get('hub'),
authenticator=self.authenticator,
config=self.settings.get('config'),
proxy_spec=url_path_join(self.proxy_spec, name, '/'),
proxy_spec=url_path_join(self.proxy_spec, server_name, '/'),
db=self.db,
oauth_client_id=client_id,
cookie_options = self.settings.get('cookie_options', {}),

View File

@@ -62,24 +62,50 @@ require(["jquery", "bootstrap", "moment", "jhapi", "utils"], function ($, bs, mo
el.text(m.isValid() ? m.fromNow() : "Never");
});
$(".stop-server").click(function () {
$(".stop-server.default-server").click(function () {
var el = $(this);
var row = get_row(el);
var user = row.data('user');
el.text("stopping...");
api.stop_server(user, {
success: function () {
el.text('stop server').addClass('hidden');
row.find('.access-server').addClass('hidden');
row.find('.start-server').removeClass('hidden');
}
if (el.data("named_servers") === true) {
el.text("stop default server").addClass("hidden");
} else {
el.text("stop server").addClass("hidden");
}
row.find(".access-server").addClass("hidden");
row.find(".start-server").removeClass("hidden");
},
});
});
$(".access-server").map(function (i, el) {
$(".stop-server[id^='stop-']").click(function () {
var el = $(this);
var row = get_row(el);
var user = row.data("user");
var server_name = (this.id).replace(/^(stop-)/, "");
el.text("stopping...");
api.stop_named_server(user, server_name, {
success: function() {
el.text("stop "+server_name).addClass("hidden");
row.find(".access-server").addClass("hidden");
row.find(".start-server").removeClass("hidden");
},
});
});
$(".access-server.default-server").map(function (i, el) {
el = $(el);
var user = get_row(el).data('user');
el.attr('href', utils.url_path_join(prefix, 'user', user) + '/');
var user = get_row(el).data("user");
el.attr("href", utils.url_path_join(prefix, "user", user) + "/");
});
$(".access-server[id^='access-']").map(function (i, el) {
el = $(el);
var user = get_row(el).data("user");
var server_name = (this.id).replace(/^(access-)/, "");
el.attr("href", utils.url_path_join(prefix, "user", user, server_name) + "/" );
});
if (admin_access && options_form) {
@@ -94,16 +120,35 @@ require(["jquery", "bootstrap", "moment", "jhapi", "utils"], function ($, bs, mo
// since it would mean opening a bunch of tabs
$('#start-all-servers').addClass('hidden');
} else {
$(".start-server").click(function () {
$(".start-server.default-server").click(function () {
var el = $(this);
var row = get_row(el);
var user = row.data('user');
el.text("starting...");
api.start_server(user, {
success: function () {
el.text('start server').addClass('hidden');
row.find('.stop-server').removeClass('hidden');
row.find('.access-server').removeClass('hidden');
if (el.data("named_servers") === true ){
el.text("start default server").addClass("hidden");
} else {
el.text("start server").addClass("hidden");
}
row.find(".stop-server").removeClass("hidden");
row.find(".access-server").removeClass("hidden");
}
});
});
$(".start-server[id^='start-']").click(function () {
var el = $(this);
var row = get_row(el);
var user = row.data("user");
var server_name = (this.id).replace(/^(start-)/, "");
el.text("starting...");
api.start_named_server(user, server_name, {
success: function () {
el.text("start "+server_name).addClass("hidden");
row.find(".stop-server").removeClass("hidden");
row.find(".access-server").removeClass("hidden");
}
});
});

View File

@@ -17,13 +17,156 @@ require(["jquery", "jhapi"], function($, JHAPI) {
});
api.stop_server(user, {
success: function() {
if ($("#start").data("named_servers") === true) {
$("#start")
.text("Start Default Server")
.attr("title", "Start the default server")
.attr("disabled", false)
.off("click");
} else {
$("#start")
.text("Start My Server")
.attr("title", "Start your server")
.attr("disabled", false)
.off("click");
$("#stop").hide();
}
}
},
});
});
//// Named servers buttons
function stop() {
let server_name = (this.id).replace(/^(stop-)/, "");
// before request
$("#stop-"+server_name)
.attr("disabled", true)
.off("click")
.click(function() {
return false;});
$("#goto-"+server_name)
.attr("disabled", true)
.off("click")
.click(function() {
return false;});
// request
api.stop_named_server(user, server_name, {
success: function() {
// after request --> establish final local state
$("#stop-"+server_name)
.data("state", false)
.hide()
$("#goto-"+server_name)
.data("state", false)
.hide()
$("#start-"+server_name)
.data("state", false)
.show()
.attr("disabled", false)
.off("click")
.click(start);
},
});
};
function goto() {
let server_name = (this.id).replace(/^(goto-)/, "");
// before request
$("#stop-"+server_name)
.attr("disabled", true)
.off("click")
.click(function() {
return false;});
$("#goto-"+server_name)
.attr("disabled", true)
.off("click")
.click(function() {
return false;});
window.location = base_url+"/user/"+user+"/"+server_name+"/tree"; }
function start() {
let server_name = (this.id).replace(/^(start-)/, "");
// before request
$("#start-"+server_name)
.attr("disabled", true)
.off("click")
.click(function() {
return false;});
// request
api.start_named_server(user, server_name, {
success: function() {
// after request --> establish final local state
$("#stop-"+server_name)
.data("state", true)
.show()
.attr("disabled", false)
.off("click")
.click(stop);
$("#goto-"+server_name)
.data("state", true)
.show()
.attr("disabled", false)
.off("click")
.click(goto);
$("#start-"+server_name)
.data("state", true)
.hide()
}});};
// Initial state: TRUE (server running) -- local state transitions occur by clicking stop
// - stop visible
// - goto visible
// - start invisible
$("[data-state='true'][id^='stop-']")
.show()
.attr("disabled", false)
.click(stop);
$("[data-state='true'][id^='goto-']")
.show()
.attr("disabled", false)
.click(goto);
$("[data-state='true'][id^='start-']")
.hide()
.attr("disabled", true)
.off("click")
.click(function() {
return false;});
// Initial state: FALSE (server not running) -- local state transitions occur by clicking start
// - stop invisible
// - goto invisible
// - start visible
$("[data-state='false'][id^='stop-']")
.hide()
.attr("disabled", true)
.off("click")
.click(function() {
return false;});
$("[data-state='false'][id^='goto-']")
.hide()
.attr("disabled", true)
.off("click")
.click(function() {
return false;});
$("[data-state='false'][id^='start-']")
.show()
.attr("disabled", false)
.click(start);
});

View File

@@ -52,6 +52,15 @@ define(['jquery', 'utils'], function ($, utils) {
);
};
JHAPI.prototype.start_named_server = function (user, server_name, options) {
options = options || {};
options = update(options, {type: 'POST', dataType: null});
this.api_request(
utils.url_path_join('users', user, 'servers', server_name),
options
);
};
JHAPI.prototype.stop_server = function (user, options) {
options = options || {};
options = update(options, {type: 'DELETE', dataType: null});
@@ -61,6 +70,15 @@ define(['jquery', 'utils'], function ($, utils) {
);
};
JHAPI.prototype.stop_named_server = function (user, server_name, options) {
options = options || {};
options = update(options, {type: 'DELETE', dataType: null});
this.api_request(
utils.url_path_join('users', user, 'servers', server_name),
options
);
};
JHAPI.prototype.list_users = function (options) {
this.api_request('users', options);
};

View File

@@ -1,3 +1,3 @@
i.sort-icon {
margin-left: 4px;
}
}

View File

@@ -40,37 +40,87 @@
<a id="shutdown-hub" role="button" class="col-xs-2 col-xs-offset-1 btn btn-danger">Shutdown Hub</a>
</td>
</tr>
{% for u in users %}
<tr class="user-row" data-user="{{u.name}}" data-admin="{{u.admin}}">
{% for user in users %}
<tr class="user-row{% if allow_named_servers %} default-server-row{% endif %}" id="user-{{user.name}}" data-user="{{ user.name }}" data-admin="{{user.admin}}">
{% block user_row scoped %}
<td class="name-col col-sm-2">{{u.name}}</td>
<td class="admin-col col-sm-2">{% if u.admin %}admin{% endif %}</td>
<td class="name-col col-sm-2">{{user.name}}</td>
<td class="admin-col col-sm-2">{% if user.admin %}admin{% endif %}</td>
<td class="time-col col-sm-3">
{%- if u.last_activity -%}
{{ u.last_activity.isoformat() + 'Z' }}
{%- if user.last_activity -%}
{{ user.last_activity.isoformat() + 'Z' }}
{%- else -%}
Never
{%- endif -%}
</td>
<td class="server-col col-sm-2 text-center">
<a role="button" class="stop-server btn btn-xs btn-danger {% if not u.running %}hidden{% endif %}">stop server</a>
<a role="button" class="start-server btn btn-xs btn-primary {% if u.running %}hidden{% endif %}">start server</a>
<a role="button" class="stop-server btn btn-xs btn-danger{% if not user.running %} hidden{% endif %} default-server" data-named_servers={% if allow_named_servers %}"true"{% else %}"false"{% endif %}>
{% if allow_named_servers %}
stop default server
{% else %}
stop server
{% endif %}
</a>
<a role="button" class="start-server btn btn-xs btn-primary{% if user.running %} hidden{% endif %} default-server" data-named_servers={% if allow_named_servers %}"true"{% else %}"false"{% endif %}>
{% if allow_named_servers %}
start default server
{% else %}
start server</a>
{% endif %}
</td>
<td class="server-col col-sm-1 text-center">
{% if admin_access %}
<a role="button" class="access-server btn btn-xs btn-primary {% if not u.running %}hidden{% endif %}">access server</a>
<a role="button" class="access-server btn btn-xs btn-primary{% if not user.running %} hidden{% endif %} default-server" data-named_servers={% if allow_named_servers %}"true"{% else %}"false"{% endif %}>
{% if allow_named_servers %}
access default server
{% else %}
access server
{% endif %}
</a>
{% endif %}
</td>
<td class="edit-col col-sm-1 text-center">
<a role="button" class="edit-user btn btn-xs btn-primary">edit</a>
<a role="button" class="edit-user btn btn-xs btn-primary">edit{% if allow_named_servers %} {{ user.name }}{% endif %}</a>
</td>
<td class="edit-col col-sm-1 text-center">
{% if u.name != user.name %}
<a role="button" class="delete-user btn btn-xs btn-danger">delete</a>
{% if user.name != current_user.name %}
<a role="button" class="delete-user btn btn-xs btn-danger">delete{% if allow_named_servers %} {{ user.name }}{% endif %}</a>
{% endif %}
</td>
</tr>
{% if allow_named_servers %}
{% for spawner in user.orm_user._orm_spawners %}
{% if spawner.name %}
<tr class="user-row" data-user="{{user.name}}" data-admin="{{user.admin}}">
{# {% block user_row scoped %} #}
<td class="name-col col-sm-2"> </td>
<td class="admin-col col-sm-2"> </td>
<td class="time-col col-sm-3">
{%- if spawner.last_activity -%}
{{ spawner.last_activity.isoformat() + 'Z' }}
{%- else -%}
Never
{%- endif -%}
</td>
<td class="server-col col-sm-2 text-center">
<a role="button" class="stop-server btn btn-xs btn-danger{% if not spawner.state %} hidden{% endif %}" id="stop-{{ spawner.name }}">stop {{ spawner.name }}</a>
<a role="button" class="start-server btn btn-xs btn-primary{% if spawner.state %} hidden{% endif %}" id="start-{{ spawner.name }}">start {{ spawner.name }}</a>
</td>
<td class="server-col col-sm-1 text-center">
{% if admin_access %}
<a role="button" class="access-server btn btn-xs btn-primary{% if not spawner.state %} hidden{% endif %}" id="access-{{ spawner.name }}">access server</a>
{% endif %}
</td>
<td class="edit-col col-sm-1 text-center">
<!-- edit user button empty for named servers -->
</td>
<td class="edit-col col-sm-1 text-center">
<!-- delete user button empty for named servers -->
</td>
</tr>
{% endif %}
{% endfor %}
{% endif %}
{% endblock user_row %}
</tr>
{% endfor %}
</tbody>
</table>

View File

@@ -6,20 +6,76 @@
{% block main %}
<div class="container">
{% if user.active %}
<div class="row" style="margin: 1%">
<div class="text-center">
{% if default_server.active %}
<a id="stop" role="button" class="btn btn-lg btn-danger">
{% if allow_named_servers %}
Stop Default Server
{% else %}
Stop My Server
{% endif %}
</a>
{% if allow_named_servers %}
<a id="start" role="button" class="btn btn-lg btn-primary" href="{{ url }}" data-named_servers="true">
Default Server
{% else %}
<a id="start" role="button" class="btn btn-lg btn-primary" href="{{ url }}" data-named_servers="false">
My Server
{% endif %}
</a>
{% else %}
<a id="start" role="button" class="btn btn-lg btn-primary" href="{{ url }}">
Start Default Server
</a>
{% endif %}
</div>
</div>
{% for spawner in spawners %}
{% if spawner.name %}
<div class="row" style="margin: 1%">
<div class="text-center">
{% if spawner.state %}
<a id="stop-{{ spawner.name }}" role="button" class="btn btn-lg btn-danger" data-state="true">
Stop {{ spawner.name }}
</a>
<a id="goto-{{ spawner.name }}" role="button" class="btn btn-lg btn-primary" href="{{ url_path_join(url, spawner.name) }}" data-state="true">
{{ spawner.name }}
</a>
<a id="start-{{ spawner.name }}" role="button" class="btn btn-lg btn-primary" data-state="true" style="display: none">
Start {{ spawner.name }}
</a>
{% else %}
<a id="stop-{{ spawner.name }}" role="button" class="btn btn-lg btn-danger" data-state="false" style="display: none">
Stop {{ spawner.name }}
</a>
<a id="goto-{{ spawner.name }}" role="button" class="btn btn-lg btn-primary" href="{{ url_path_join(url, spawner.name) }}" data-state="false" style="display: none">
{{ spawner.name }}
</a>
<a id="start-{{ spawner.name }}" role="button" class="btn btn-lg btn-primary" data-state="false">
Start {{ spawner.name }}
</a>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
{% else %}
<div class="row">
<div class="text-center">
{% if user.running %}
<a id="stop" role="button" class="btn btn-lg btn-danger">Stop My Server</a>
{% endif %}
<a id="start" role="button" class="btn btn-lg btn-primary" href="{{ url }}">
{% if not user.active %}
Start
{% if allow_named_servers %}
Start a New Server
{% else %}
Start My Server
{% endif %}
My Server
</a>
</div>
</div>
</div>
{% endif %}
{% endblock %}