move proxy management to Proxy object

out of the Application
This commit is contained in:
Min RK
2017-04-21 15:16:55 +02:00
parent acc31b8441
commit be8f847309
12 changed files with 474 additions and 276 deletions

View File

@@ -21,7 +21,7 @@ class ProxyAPIHandler(APIHandler):
This is the same as fetching the routing table directly from the proxy,
but without clients needing to maintain separate
"""
routes = yield self.proxy.get_routes()
routes = yield self.proxy.get_all_routes()
self.write(json.dumps(routes))
@admin_only

View File

@@ -30,7 +30,6 @@ from sqlalchemy.orm import scoped_session
import tornado.httpserver
import tornado.options
from tornado.httpclient import HTTPError
from tornado.ioloop import IOLoop, PeriodicCallback
from tornado.log import app_log, access_log, gen_log
from tornado import gen, web
@@ -54,7 +53,8 @@ from .user import User, UserDict
from .oauth.store import make_provider
from ._data import DATA_FILES_PATH
from .log import CoroutineLogFormatter, log_request
from .traitlets import URLPrefix, Command
from .proxy import Proxy, ConfigurableHTTPProxy
from .traitlets import URLPrefix
from .utils import (
url_path_join,
ISO8601_ms, ISO8601_s,
@@ -62,7 +62,7 @@ from .utils import (
# classes for config
from .auth import Authenticator, PAMAuthenticator
from .spawner import Spawner, LocalProcessSpawner
from .objects import Server, Hub
from .objects import Hub
# For faking stats
from .emptyclass import EmptyClass
@@ -141,7 +141,6 @@ class NewToken(Application):
hub = JupyterHub(parent=self)
hub.load_config_file(hub.config_file)
hub.init_db()
hub.hub = hub.db.query(orm.Hub).first()
hub.init_users()
user = orm.User.find(hub.db, self.name)
if user is None:
@@ -350,45 +349,16 @@ class JupyterHub(Application):
help="Supply extra arguments that will be passed to Jinja environment."
).tag(config=True)
proxy_cmd = Command('configurable-http-proxy',
help="""The command to start the http proxy.
Only override if configurable-http-proxy is not on your PATH
"""
).tag(config=True)
debug_proxy = Bool(False,
help="show debug output in configurable-http-proxy"
).tag(config=True)
proxy_auth_token = Unicode(
help="""The Proxy Auth token.
Loaded from the CONFIGPROXY_AUTH_TOKEN env variable by default.
"""
proxy_class = Type(ConfigurableHTTPProxy, Proxy,
help="""Select the Proxy API implementation."""
).tag(config=True)
@default('proxy_auth_token')
def _proxy_auth_token_default(self):
token = os.environ.get('CONFIGPROXY_AUTH_TOKEN', None)
if not token:
self.log.warning('\n'.join([
"",
"Generating CONFIGPROXY_AUTH_TOKEN. Restarting the Hub will require restarting the proxy.",
"Set CONFIGPROXY_AUTH_TOKEN env or JupyterHub.proxy_auth_token config to avoid this message.",
"",
]))
token = orm.new_token()
return token
proxy_api_ip = Unicode('127.0.0.1',
help="The ip for the proxy API handlers"
).tag(config=True)
proxy_api_port = Integer(
help="The port for the proxy API handlers"
).tag(config=True)
@default('proxy_api_port')
def _proxy_api_port_default(self):
return self.port + 1
# FIXME: deprecated proxy config should map to CHP config with a warning
# proxy_cmd
# debug_proxy
# proxy_auth_token
# proxy_api_ip
# proxy_api_port
hub_port = Integer(8081,
help="The port for this process"
@@ -683,10 +653,6 @@ class JupyterHub(Application):
def init_ports(self):
if self.hub_port == self.port:
raise TraitError("The hub and proxy cannot both listen on port %i" % self.port)
if self.hub_port == self.proxy_api_port:
raise TraitError("The hub and proxy API cannot both listen on port %i" % self.hub_port)
if self.proxy_api_port == self.port:
raise TraitError("The proxy's public and API ports cannot both be %i" % self.port)
@staticmethod
def add_url_prefix(prefix, handlers):
@@ -839,6 +805,7 @@ class JupyterHub(Application):
cookie_name='jupyter-hub-token',
public_host=self.subdomain_host,
)
print(self.hub)
@gen.coroutine
def init_users(self):
@@ -1138,120 +1105,23 @@ class JupyterHub(Application):
)
def init_proxy(self):
"""Load the Proxy config into the database"""
self.proxy = self.db.query(orm.Proxy).first()
if self.proxy is None:
self.proxy = orm.Proxy(
public_server=orm.Server(),
api_server=orm.Server(),
"""Load the Proxy config"""
# FIXME: handle deprecated config here
public_url = 'http{s}://{ip}:{port}'.format(
s='s' if self.ssl_cert else '',
ip=self.ip,
port=self.port,
)
self.db.add(self.proxy)
self.db.commit()
self.proxy.auth_token = self.proxy_auth_token # not persisted
self.proxy.log = self.log
self.proxy.public_server.ip = self.ip
self.proxy.public_server.port = self.port
self.proxy.public_server.base_url = self.base_url
self.proxy.api_server.ip = self.proxy_api_ip
self.proxy.api_server.port = self.proxy_api_port
self.proxy.api_server.base_url = '/api/routes/'
self.db.commit()
@gen.coroutine
def start_proxy(self):
"""Actually start the configurable-http-proxy"""
# check for proxy
if self.proxy.public_server.is_up() or self.proxy.api_server.is_up():
# check for *authenticated* access to the proxy (auth token can change)
try:
routes = yield self.proxy.get_routes()
except (HTTPError, OSError, socket.error) as e:
if isinstance(e, HTTPError) and e.code == 403:
msg = "Did CONFIGPROXY_AUTH_TOKEN change?"
else:
msg = "Is something else using %s?" % self.proxy.public_server.bind_url
self.log.error("Proxy appears to be running at %s, but I can't access it (%s)\n%s",
self.proxy.public_server.bind_url, e, msg)
self.exit(1)
return
else:
self.log.info("Proxy already running at: %s", self.proxy.public_server.bind_url)
yield self.proxy.check_routes(self.users, self._service_map, routes)
self.proxy_process = None
return
env = os.environ.copy()
env['CONFIGPROXY_AUTH_TOKEN'] = self.proxy.auth_token
cmd = self.proxy_cmd + [
'--ip', self.proxy.public_server.ip,
'--port', str(self.proxy.public_server.port),
'--api-ip', self.proxy.api_server.ip,
'--api-port', str(self.proxy.api_server.port),
'--default-target', self.hub.host,
'--error-target', url_path_join(self.hub.url, 'error'),
]
if self.subdomain_host:
cmd.append('--host-routing')
if self.debug_proxy:
cmd.extend(['--log-level', 'debug'])
if self.ssl_key:
cmd.extend(['--ssl-key', self.ssl_key])
if self.ssl_cert:
cmd.extend(['--ssl-cert', self.ssl_cert])
if self.statsd_host:
cmd.extend([
'--statsd-host', self.statsd_host,
'--statsd-port', str(self.statsd_port),
'--statsd-prefix', self.statsd_prefix + '.chp'
])
# Warn if SSL is not used
if ' --ssl' not in ' '.join(cmd):
self.log.warning("Running JupyterHub without SSL."
" I hope there is SSL termination happening somewhere else...")
self.log.info("Starting proxy @ %s", self.proxy.public_server.bind_url)
self.log.debug("Proxy cmd: %s", cmd)
try:
self.proxy_process = Popen(cmd, env=env, start_new_session=True)
except FileNotFoundError as e:
self.log.error(
"Failed to find proxy %r\n"
"The proxy can be installed with `npm install -g configurable-http-proxy`"
% self.proxy_cmd
self.proxy = self.proxy_class(
db=self.db,
public_url=public_url,
parent=self,
app=self,
log=self.log,
hub=self.hub,
ssl_cert=self.ssl_cert,
ssl_key=self.ssl_key,
)
self.exit(1)
def _check():
status = self.proxy_process.poll()
if status is not None:
e = RuntimeError("Proxy failed to start with exit code %i" % status)
# py2-compatible `raise e from None`
e.__cause__ = None
raise e
for server in (self.proxy.public_server, self.proxy.api_server):
for i in range(10):
_check()
try:
yield server.wait_up(1)
except TimeoutError:
continue
else:
break
yield server.wait_up(1)
self.log.debug("Proxy started and appears to be up")
@gen.coroutine
def check_proxy(self):
if self.proxy_process.poll() is None:
return
self.log.error("Proxy stopped with exit code %r",
'unknown' if self.proxy_process is None else self.proxy_process.poll()
)
yield self.start_proxy()
self.log.info("Setting up routes on new proxy")
yield self.proxy.add_all_users(self.users)
yield self.proxy.add_all_services(self.services)
self.log.info("New proxy back up, and good to go")
def init_tornado_settings(self):
"""Set up the tornado settings dict."""
@@ -1378,13 +1248,8 @@ class JupyterHub(Application):
# clean up proxy while single-user servers are shutting down
if self.cleanup_proxy:
if self.proxy_process:
self.log.info("Cleaning up proxy[%i]...", self.proxy_process.pid)
if self.proxy_process.poll() is None:
try:
self.proxy_process.terminate()
except Exception as e:
self.log.error("Failed to terminate proxy process: %s", e)
if self.proxy.should_start:
yield gen.maybe_future(self.proxy.stop())
else:
self.log.info("I didn't start the proxy, I can't clean it up")
else:
@@ -1440,13 +1305,17 @@ class JupyterHub(Application):
@gen.coroutine
def update_last_activity(self):
"""Update User.last_activity timestamps from the proxy"""
routes = yield self.proxy.get_routes()
routes = yield self.proxy.get_all_routes()
users_count = 0
active_users_count = 0
for prefix, route in routes.items():
if 'user' not in route:
if 'user' not in route['data']:
# not a user route, ignore it
continue
users_count += 1
if 'last_activity' not in route['data']:
# no last activity data (possibly proxy other than CHP)
continue
user = orm.User.find(self.db, route['user'])
if user is None:
self.log.warning("Found no user for route: %s", route)
@@ -1459,7 +1328,6 @@ class JupyterHub(Application):
# FIXME: Make this configurable duration. 30 minutes for now!
if (datetime.now() - user.last_activity).total_seconds() < 30 * 60:
active_users_count += 1
users_count += 1
self.statsd.gauge('users.running', users_count)
self.statsd.gauge('users.active', active_users_count)
@@ -1492,11 +1360,14 @@ class JupyterHub(Application):
self.log.info("Hub API listening on %s", self.hub.bind_url)
# start the proxy
if self.proxy.should_start:
try:
yield self.start_proxy()
yield self.proxy.start()
except Exception as e:
self.log.critical("Failed to start proxy", exc_info=True)
self.exit(1)
else:
self.log.info("Not starting proxy")
# start the service(s)
for service_name, service in self._service_map.items():
@@ -1530,12 +1401,6 @@ class JupyterHub(Application):
loop.add_callback(self.proxy.add_all_users, self.users)
loop.add_callback(self.proxy.add_all_services, self._service_map)
if self.proxy_process:
# only check / restart the proxy if we started it in the first place.
# this means a restarted Hub cannot restart a Proxy that its
# predecessor started.
pc = PeriodicCallback(self.check_proxy, 1e3 * self.proxy_check_interval)
pc.start()
if self.service_check_interval and any(s.url for s in self._service_map.values()):
pc = PeriodicCallback(self.check_services_health, 1e3 * self.service_check_interval)
@@ -1545,7 +1410,7 @@ class JupyterHub(Application):
pc = PeriodicCallback(self.update_last_activity, 1e3 * self.last_activity_interval)
pc.start()
self.log.info("JupyterHub is now running at %s", self.proxy.public_server.url)
self.log.info("JupyterHub is now running at %s", self.proxy.public_url)
# register cleanup on both TERM and INT
atexit.register(self.atexit)
self.init_signal()

View File

@@ -528,12 +528,12 @@ class UserSpawnHandler(BaseHandler):
port = host_info.port
if not port:
port = 443 if host_info.scheme == 'https' else 80
if port != self.proxy.public_server.port and port == self.hub.port:
if port != Server.from_url(self.proxy.public_url).port and port == self.hub.port:
self.log.warning("""
Detected possible direct connection to Hub's private ip: %s, bypassing proxy.
This will result in a redirect loop.
Make sure to connect to the proxied public URL %s
""", self.request.full_url(), self.proxy.public_server.url)
""", self.request.full_url(), self.proxy.public_url)
# logged in as correct user, spawn the server
if current_user.spawner:

View File

@@ -5,9 +5,17 @@
from urllib.parse import urlparse
from traitlets import HasTraits
from . import orm
from tornado import gen
from traitlets import (
HasTraits, Instance, Integer, Unicode,
default, observe,
)
from . import orm
from .utils import (
url_path_join, can_connect, wait_for_server,
wait_for_http_server, random_port,
)
class Server(HasTraits):
"""An object representing an HTTP endpoint.
@@ -28,7 +36,7 @@ class Server(HasTraits):
"""Create a Server from a given URL"""
urlinfo = urlparse(url)
proto = urlinfo.scheme
ip = urlinfo.hostname
ip = urlinfo.hostname or ''
port = urlinfo.port
if not port:
if proto == 'https':
@@ -113,12 +121,12 @@ class Hub(Server):
def server(self):
"""backward-compat"""
return self
host = Unicode()
public_host = Unicode()
@property
def api_url(self):
"""return the full API url (with proto://host...)"""
return url_path_join(self.server.url, 'api')
return url_path_join(self.url, 'api')
def __repr__(self):
return "<%s %s:%s>" % (

View File

@@ -1,21 +1,73 @@
"""API for JupyterHub's proxy."""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
import json
import os
from subprocess import Popen
import time
from tornado import gen
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
from tornado.ioloop import PeriodicCallback
from traitlets import (
Any, Bool, Instance, Integer, Unicode,
default,
)
from jupyterhub.traitlets import Command
from traitlets.config import LoggingConfigurable
from .objects import Server
from .orm import Service, User
from . import utils
from .utils import url_path_join
class Proxy(LoggingConfigurable):
"""
Base class for configurable proxies that JupyterHub can use
"""
def add_route(self, routespec, target, data):
"""
Add a route to the proxy
"""Base class for configurable proxies that JupyterHub can use."""
:param urlspec: A specification for which this route will be matched.
db = Any()
app = Any()
hub = Any()
public_url = Unicode()
ssl_key = Unicode()
ssl_cert = Unicode()
should_start = Bool(True, config=True,
help="""Should the Hub start the proxy.
If True, the Hub will start the proxy and stop it.
Set to False if the proxy is managed externally,
such as by systemd, docker, or another service manager.
""")
def start(self):
"""Start the proxy.
Will be called during startup if should_start is True.
"""
def stop(self):
"""Stop the proxy.
Will be called during teardown if should_start is True.
"""
@gen.coroutine
def add_route(self, routespec, target, data):
"""Add a route to the proxy.
Args:
routespec (str): A specification for which this route will be matched.
Could be either a url_prefix or a fqdn.
:param target: A URL that will be the target of this route.
:param data: A JSONable dict that will be associated with this route, and will
target (str): A URL that will be the target of this route.
data (dict): A JSONable dict that will be associated with this route, and will
be returned when retrieving information about this route.
Will raise an appropriate exception (FIXME: find what?) if the route could
Will raise an appropriate Exception (FIXME: find what?) if the route could
not be added.
The proxy implementation should also have a way to associate the fact that a
@@ -23,33 +75,342 @@ class Proxy(LoggingConfigurable):
"""
pass
def get_all_routes(self):
"""
Fetch and return all the routes associated by JupyterHub from the proxy
Should return a list of dictionaries, where each dictionary has the same
structure as the return value of `get_route`
"""
@gen.coroutine
def delete_route(self, routespec):
"""Delete a route with a given routespec if it exists."""
pass
@gen.coroutine
def get_route(self, routespec):
"""
Return the route info for a given routespec
"""Return the route info for a given routespec.
:param routespec: The route specification that was used to add this routespec
Args:
routespec (str): The route specification that was used to add this routespec
Returns a dict with the following info:
routespec: The normalized route specification passed in to add_route
target: The target for this route
data: The arbitrary data that was passed in by JupyterHub when adding this
Returns:
result (dict): with the following keys:
`routespec`: The normalized route specification passed in to add_route
`target`: The target for this route
`data`: The arbitrary data that was passed in by JupyterHub when adding this
route.
Returns `None` if there are no routes matching the given routespec
None: if there are no routes matching the given routespec
"""
pass
@gen.coroutine
def get_all_routes(self):
"""Fetch and return all the routes associated by JupyterHub from the
proxy.
Should return a dictionary of routes, where the keys are
routespecs and each value is the dict that would be returned by
`get_route(routespec)`.
"""
pass
# Most basic implementers must only implement above methods
@gen.coroutine
def add_service(self, service, client=None):
"""Add a service's server to the proxy table."""
if not service.server:
raise RuntimeError(
"Service %s does not have an http endpoint to add to the proxy.", service.name)
self.log.info("Adding service %s to proxy %s => %s",
service.name, service.proxy_path, service.server.host,
)
yield self.add_route(
service.proxy_path,
service.server.host,
{'service': service.name}
)
@gen.coroutine
def delete_service(self, service, client=None):
"""Remove a service's server from the proxy table."""
self.log.info("Removing service %s from proxy", service.name)
yield self.delete_route(service.proxy_path)
@gen.coroutine
def add_user(self, user, client=None):
"""Add a user's server to the proxy table."""
self.log.info("Adding user %s to proxy %s => %s",
user.name, user.proxy_path, user.server.host,
)
if user.spawn_pending:
raise RuntimeError(
"User %s's spawn is pending, shouldn't be added to the proxy yet!", user.name)
yield self.add_route(
user.proxy_path,
user.server.host,
{'user': user.name}
)
@gen.coroutine
def delete_user(self, user):
"""Remove a user's server from the proxy table."""
self.log.info("Removing user %s from proxy", user.name)
yield self.delete_route(user.proxy_path)
@gen.coroutine
def add_all_services(self, service_dict):
"""Update the proxy table from the database.
Used when loading up a new proxy.
"""
db = self.db
futures = []
for orm_service in db.query(Service):
service = service_dict[orm_service.name]
if service.server:
futures.append(self.add_service(service))
# wait after submitting them all
for f in futures:
yield f
@gen.coroutine
def add_all_users(self, user_dict):
"""Update the proxy table from the database.
Used when loading up a new proxy.
"""
db = self.db
futures = []
for orm_user in db.query(User):
user = user_dict[orm_user]
if user.running:
futures.append(self.add_user(user))
# wait after submitting them all
for f in futures:
yield f
@gen.coroutine
def check_routes(self, user_dict, service_dict, routes=None):
"""Check that all users are properly routed on the proxy."""
if not routes:
routes = yield self.get_all_routes()
user_routes = {r['user'] for r in routes.values() if 'user' in r}
futures = []
db = self.db
for orm_user in db.query(User):
user = user_dict[orm_user]
if user.running:
if user.name not in user_routes:
self.log.warning(
"Adding missing route for %s (%s)", user.name, user.server)
futures.append(self.add_user(user))
else:
# User not running, make sure it's not in the table
if user.name in user_routes:
self.log.warning(
"Removing route for not running %s", user.name)
futures.append(self.delete_user(user))
# check service routes
service_routes = {r['service']
for r in routes.values() if 'service' in r}
for orm_service in db.query(Service).filter(
Service.server is not None):
service = service_dict[orm_service.name]
if service.server is None:
# This should never be True, but seems to be on rare occasion.
# catch filter bug, either in sqlalchemy or my understanding of
# its behavior
self.log.error(
"Service %s has no server, but wasn't filtered out.", service)
continue
if service.name not in service_routes:
self.log.warning("Adding missing route for %s (%s)",
service.name, service.server)
futures.append(self.add_service(service))
for f in futures:
yield f
@gen.coroutine
def restore_routes(self):
self.log.info("Setting up routes on new proxy")
yield self.add_all_users(self.app.users)
yield self.add_all_services(self.app.services)
self.log.info("New proxy back up, and good to go")
class ConfigurableHTTPProxy(Proxy):
"""Proxy implementation for the default configurable-http-proxy."""
proxy_process = Any()
client = Instance(AsyncHTTPClient, ())
debug = Bool(False, config=True)
auth_token = Unicode(
help="""The Proxy Auth token.
Loaded from the CONFIGPROXY_AUTH_TOKEN env variable by default.
""",
).tag(config=True)
check_running_interval = Integer(5, config=True)
@default('auth_token')
def _auth_token_default(self):
token = os.environ.get('CONFIGPROXY_AUTH_TOKEN', None)
if not token:
self.log.warning('\n'.join([
"",
"Generating CONFIGPROXY_AUTH_TOKEN. Restarting the Hub will require restarting the proxy.",
"Set CONFIGPROXY_AUTH_TOKEN env or JupyterHub.proxy_auth_token config to avoid this message.",
"",
]))
token = utils.new_token()
return token
api_url = Unicode('http://127.0.0.1:8001', config=True,
help="""The ip (or hostname) of the proxy's API endpoint"""
)
command = Command('configurable-http-proxy', config=True,
help="""The command to start the proxy"""
)
@gen.coroutine
def start(self):
public_server = Server.from_url(self.public_url)
api_server = Server.from_url(self.api_url)
env = os.environ.copy()
env['CONFIGPROXY_AUTH_TOKEN'] = self.auth_token
cmd = self.command + [
'--ip', public_server.ip,
'--port', str(public_server.port),
'--api-ip', api_server.ip,
'--api-port', str(api_server.port),
'--default-target', self.hub.host,
'--error-target', url_path_join(self.hub.url, 'error'),
]
if self.app.subdomain_host:
cmd.append('--host-routing')
if self.debug:
cmd.extend(['--log-level', 'debug'])
if self.ssl_key:
cmd.extend(['--ssl-key', self.ssl_key])
if self.ssl_cert:
cmd.extend(['--ssl-cert', self.ssl_cert])
if self.app.statsd_host:
cmd.extend([
'--statsd-host', self.app.statsd_host,
'--statsd-port', str(self.app.statsd_port),
'--statsd-prefix', self.app.statsd_prefix + '.chp'
])
# Warn if SSL is not used
if ' --ssl' not in ' '.join(cmd):
self.log.warning("Running JupyterHub without SSL."
" I hope there is SSL termination happening somewhere else...")
self.log.info("Starting proxy @ %s", public_server.bind_url)
self.log.debug("Proxy cmd: %s", cmd)
try:
self.proxy_process = Popen(cmd, env=env, start_new_session=True)
except FileNotFoundError as e:
self.log.error(
"Failed to find proxy %r\n"
"The proxy can be installed with `npm install -g configurable-http-proxy`"
% self.cmd
)
self.exit(1)
def _check_process():
status = self.proxy_process.poll()
if status is not None:
e = RuntimeError(
"Proxy failed to start with exit code %i" % status)
# py2-compatible `raise e from None`
e.__cause__ = None
raise e
for server in (public_server, api_server):
for i in range(10):
_check_process()
try:
yield server.wait_up(1)
except TimeoutError:
continue
else:
break
yield server.wait_up(1)
time.sleep(1)
_check_process()
self.log.debug("Proxy started and appears to be up")
pc = PeriodicCallback(self.check_running, 1e3 * self.check_running_interval)
pc.start()
def stop(self):
self.log.info("Cleaning up proxy[%i]...", self.proxy_process.pid)
if self.proxy_process.poll() is None:
try:
self.proxy_process.terminate()
except Exception as e:
self.log.error("Failed to terminate proxy process: %s", e)
@gen.coroutine
def check_running(self):
"""Check if the proxy is still running"""
if self.proxy_process.poll() is None:
return
self.log.error("Proxy stopped with exit code %r",
'unknown' if self.proxy_process is None else self.proxy_process.poll()
)
yield self.start()
yield self.restore_routes()
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_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)
def add_route(self, routespec, target, data=None):
body = data or {}
body['target'] = target
return self.api_request(routespec,
method='POST',
body=body,
)
def delete_route(self, routespec):
"""
Delete a route with a given routespec if it exists.
"""
pass
return self.api_request(routespec, method='DELETE')
def _reformat_routespec(self, routespec, chp_data):
"""Reformat CHP data format to JupyterHub's proxy API."""
target = chp_data.pop('target')
return {
'routespec': routespec,
'target': target,
'data': chp_data,
}
@gen.coroutine
def get_route(self, routespec):
chp_data = yield self.api_request(routespec, method='DELETE')
return self._reformat_routespec(routespec, chp_data)
@gen.coroutine
def get_all_routes(self, client=None):
"""Fetch the proxy's routes."""
resp = yield self.api_request('', client=client)
chp_routes = json.loads(resp.body.decode('utf8', 'replace'))
all_routes = {}
for routespec, chp_data in chp_routes.items():
all_routes[routespec] = self._reformat_routespec(
routespec, chp_data)
return all_routes

View File

@@ -32,11 +32,7 @@ def db():
name=getuser(),
)
user.servers.append(orm.Server())
hub = orm.Hub(
server=orm.Server(),
)
_db.add(user)
_db.add(hub)
_db.commit()
return _db

View File

@@ -207,7 +207,7 @@ def public_host(app):
if app.subdomain_host:
return app.subdomain_host
else:
return app.proxy.public_server.host
return urlparse(app.proxy.public_url).host
def public_url(app, user_or_service=None, path=''):

View File

@@ -28,41 +28,6 @@ def test_server(db):
assert server.url == server.host + '/'
def test_proxy(db):
proxy = orm.Proxy(
auth_token='abc-123',
public_server=orm.Server(
ip='192.168.1.1',
port=8000,
),
api_server=orm.Server(
ip='127.0.0.1',
port=8001,
),
)
db.add(proxy)
db.commit()
assert proxy.public_server.ip == '192.168.1.1'
assert proxy.api_server.ip == '127.0.0.1'
assert proxy.auth_token == 'abc-123'
def test_hub(db):
hub = orm.Hub(
server=orm.Server(
ip = '1.2.3.4',
port = 1234,
base_url='/hubtest/',
),
)
db.add(hub)
db.commit()
assert hub.ip == '1.2.3.4'
assert hub.port == 1234
assert hub.api_url == 'http://1.2.3.4:1234/hubtest/api'
def test_user(db):
user = orm.User(name='kaylee',
state={'pid': 4234},

View File

@@ -25,7 +25,7 @@ def get_page(path, app, hub=True, **kw):
def test_root_no_auth(app, io_loop):
print(app.hub.is_up())
routes = io_loop.run_sync(app.proxy.get_routes)
routes = io_loop.run_sync(app.proxy.get_all_routes)
print(routes)
print(app.hub.server)
url = ujoin(public_host(app), app.hub.base_url)

View File

@@ -60,7 +60,7 @@ def test_external_proxy(request, io_loop):
assert app.proxy_process is None
# test if api service has a root route '/'
routes = io_loop.run_sync(app.proxy.get_routes)
routes = io_loop.run_sync(app.proxy.get_all_routes)
assert list(routes.keys()) == ['/']
# add user to the db and start a single user server
@@ -70,7 +70,7 @@ def test_external_proxy(request, io_loop):
r = api_request(app, 'users', name, 'server', method='post')
r.raise_for_status()
routes = io_loop.run_sync(app.proxy.get_routes)
routes = io_loop.run_sync(app.proxy.get_all_routes)
# sets the desired path result
user_path = unquote(ujoin(app.base_url, 'user/river'))
if app.subdomain_host:
@@ -83,7 +83,8 @@ def test_external_proxy(request, io_loop):
proxy = Popen(cmd, env=env)
wait_for_proxy()
routes = io_loop.run_sync(app.proxy.get_routes)
routes = io_loop.run_sync(app.proxy.get_all_routes)
assert list(routes.keys()) == ['/']
# poke the server to update the proxy
@@ -91,7 +92,7 @@ def test_external_proxy(request, io_loop):
r.raise_for_status()
# check that the routes are correct
routes = io_loop.run_sync(app.proxy.get_routes)
routes = io_loop.run_sync(app.proxy.get_all_routes)
assert sorted(routes.keys()) == ['/', user_path]
# teardown the proxy, and start a new one with different auth and port
@@ -131,7 +132,7 @@ def test_external_proxy(request, io_loop):
app.proxy.auth_token = new_auth_token
# check that the routes are correct
routes = io_loop.run_sync(app.proxy.get_routes)
routes = io_loop.run_sync(app.proxy.get_all_routes)
assert sorted(routes.keys()) == ['/', user_path]
@@ -152,7 +153,7 @@ def test_check_routes(app, io_loop, username, endpoints):
# check a valid route exists for user
test_user = app.users[username]
before = sorted(io_loop.run_sync(app.proxy.get_routes))
before = sorted(io_loop.run_sync(app.proxy.get_all_routes))
assert unquote(test_user.proxy_path) in before
# check if a route is removed when user deleted
@@ -163,7 +164,7 @@ def test_check_routes(app, io_loop, username, endpoints):
# check if a route exists for user
io_loop.run_sync(lambda: app.proxy.check_routes(app.users, app._service_map))
after = sorted(io_loop.run_sync(app.proxy.get_routes))
after = sorted(io_loop.run_sync(app.proxy.get_all_routes))
assert unquote(test_user.proxy_path) in after
# check that before and after state are the same

View File

@@ -64,7 +64,7 @@ def test_managed_service(mockservice):
def test_proxy_service(app, mockservice_url, io_loop):
service = mockservice_url
name = service.name
io_loop.run_sync(app.proxy.get_routes)
io_loop.run_sync(app.proxy.get_all_routes)
url = public_url(app, service) + '/foo'
r = requests.get(url, allow_redirects=False)
path = '/services/{}/foo'.format(name)

View File

@@ -116,10 +116,11 @@ class User(HasTraits):
self.settings = settings
super().__init__(**kwargs)
hub = self.db.query(orm.Hub).first()
self.allow_named_servers = self.settings.get('allow_named_servers', False)
hub = self.settings['hub']
self.cookie_name = '%s-%s' % (hub.cookie_name, quote(self.name, safe=''))
self.base_url = url_path_join(
self.settings.get('base_url', '/'), 'user', self.escaped_name)
@@ -222,14 +223,15 @@ class User(HasTraits):
server_name = ''
base_url = self.base_url
server = orm.Server(
orm_server = orm.Server(
name=server_name,
cookie_name=self.cookie_name,
base_url=base_url,
)
self.servers.append(server)
db.add(self)
db.add(orm_server)
db.commit()
server = Server(orm_server=orm_server)
self.servers.append(server)
api_token = self.new_api_token()
db.commit()