mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-17 15:03:02 +00:00
move proxy management to Proxy object
out of the Application
This commit is contained in:
@@ -21,7 +21,7 @@ class ProxyAPIHandler(APIHandler):
|
|||||||
This is the same as fetching the routing table directly from the proxy,
|
This is the same as fetching the routing table directly from the proxy,
|
||||||
but without clients needing to maintain separate
|
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))
|
self.write(json.dumps(routes))
|
||||||
|
|
||||||
@admin_only
|
@admin_only
|
||||||
|
@@ -30,7 +30,6 @@ from sqlalchemy.orm import scoped_session
|
|||||||
|
|
||||||
import tornado.httpserver
|
import tornado.httpserver
|
||||||
import tornado.options
|
import tornado.options
|
||||||
from tornado.httpclient import HTTPError
|
|
||||||
from tornado.ioloop import IOLoop, PeriodicCallback
|
from tornado.ioloop import IOLoop, PeriodicCallback
|
||||||
from tornado.log import app_log, access_log, gen_log
|
from tornado.log import app_log, access_log, gen_log
|
||||||
from tornado import gen, web
|
from tornado import gen, web
|
||||||
@@ -54,7 +53,8 @@ from .user import User, UserDict
|
|||||||
from .oauth.store import make_provider
|
from .oauth.store import make_provider
|
||||||
from ._data import DATA_FILES_PATH
|
from ._data import DATA_FILES_PATH
|
||||||
from .log import CoroutineLogFormatter, log_request
|
from .log import CoroutineLogFormatter, log_request
|
||||||
from .traitlets import URLPrefix, Command
|
from .proxy import Proxy, ConfigurableHTTPProxy
|
||||||
|
from .traitlets import URLPrefix
|
||||||
from .utils import (
|
from .utils import (
|
||||||
url_path_join,
|
url_path_join,
|
||||||
ISO8601_ms, ISO8601_s,
|
ISO8601_ms, ISO8601_s,
|
||||||
@@ -62,7 +62,7 @@ from .utils import (
|
|||||||
# 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
|
||||||
from .objects import Server, Hub
|
from .objects import Hub
|
||||||
|
|
||||||
# For faking stats
|
# For faking stats
|
||||||
from .emptyclass import EmptyClass
|
from .emptyclass import EmptyClass
|
||||||
@@ -141,7 +141,6 @@ class NewToken(Application):
|
|||||||
hub = JupyterHub(parent=self)
|
hub = JupyterHub(parent=self)
|
||||||
hub.load_config_file(hub.config_file)
|
hub.load_config_file(hub.config_file)
|
||||||
hub.init_db()
|
hub.init_db()
|
||||||
hub.hub = hub.db.query(orm.Hub).first()
|
|
||||||
hub.init_users()
|
hub.init_users()
|
||||||
user = orm.User.find(hub.db, self.name)
|
user = orm.User.find(hub.db, self.name)
|
||||||
if user is None:
|
if user is None:
|
||||||
@@ -350,45 +349,16 @@ class JupyterHub(Application):
|
|||||||
help="Supply extra arguments that will be passed to Jinja environment."
|
help="Supply extra arguments that will be passed to Jinja environment."
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
proxy_cmd = Command('configurable-http-proxy',
|
proxy_class = Type(ConfigurableHTTPProxy, Proxy,
|
||||||
help="""The command to start the http proxy.
|
help="""Select the Proxy API implementation."""
|
||||||
|
).tag(config=True)
|
||||||
|
|
||||||
Only override if configurable-http-proxy is not on your PATH
|
# FIXME: deprecated proxy config should map to CHP config with a warning
|
||||||
"""
|
# proxy_cmd
|
||||||
).tag(config=True)
|
# debug_proxy
|
||||||
debug_proxy = Bool(False,
|
# proxy_auth_token
|
||||||
help="show debug output in configurable-http-proxy"
|
# proxy_api_ip
|
||||||
).tag(config=True)
|
# proxy_api_port
|
||||||
proxy_auth_token = Unicode(
|
|
||||||
help="""The Proxy Auth token.
|
|
||||||
|
|
||||||
Loaded from the CONFIGPROXY_AUTH_TOKEN env variable by default.
|
|
||||||
"""
|
|
||||||
).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
|
|
||||||
|
|
||||||
hub_port = Integer(8081,
|
hub_port = Integer(8081,
|
||||||
help="The port for this process"
|
help="The port for this process"
|
||||||
@@ -683,10 +653,6 @@ class JupyterHub(Application):
|
|||||||
def init_ports(self):
|
def init_ports(self):
|
||||||
if self.hub_port == self.port:
|
if self.hub_port == self.port:
|
||||||
raise TraitError("The hub and proxy cannot both listen on port %i" % 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
|
@staticmethod
|
||||||
def add_url_prefix(prefix, handlers):
|
def add_url_prefix(prefix, handlers):
|
||||||
@@ -839,6 +805,7 @@ class JupyterHub(Application):
|
|||||||
cookie_name='jupyter-hub-token',
|
cookie_name='jupyter-hub-token',
|
||||||
public_host=self.subdomain_host,
|
public_host=self.subdomain_host,
|
||||||
)
|
)
|
||||||
|
print(self.hub)
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def init_users(self):
|
def init_users(self):
|
||||||
@@ -1138,120 +1105,23 @@ class JupyterHub(Application):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def init_proxy(self):
|
def init_proxy(self):
|
||||||
"""Load the Proxy config into the database"""
|
"""Load the Proxy config"""
|
||||||
self.proxy = self.db.query(orm.Proxy).first()
|
# FIXME: handle deprecated config here
|
||||||
if self.proxy is None:
|
public_url = 'http{s}://{ip}:{port}'.format(
|
||||||
self.proxy = orm.Proxy(
|
s='s' if self.ssl_cert else '',
|
||||||
public_server=orm.Server(),
|
ip=self.ip,
|
||||||
api_server=orm.Server(),
|
port=self.port,
|
||||||
)
|
)
|
||||||
self.db.add(self.proxy)
|
self.proxy = self.proxy_class(
|
||||||
self.db.commit()
|
db=self.db,
|
||||||
self.proxy.auth_token = self.proxy_auth_token # not persisted
|
public_url=public_url,
|
||||||
self.proxy.log = self.log
|
parent=self,
|
||||||
self.proxy.public_server.ip = self.ip
|
app=self,
|
||||||
self.proxy.public_server.port = self.port
|
log=self.log,
|
||||||
self.proxy.public_server.base_url = self.base_url
|
hub=self.hub,
|
||||||
self.proxy.api_server.ip = self.proxy_api_ip
|
ssl_cert=self.ssl_cert,
|
||||||
self.proxy.api_server.port = self.proxy_api_port
|
ssl_key=self.ssl_key,
|
||||||
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.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):
|
def init_tornado_settings(self):
|
||||||
"""Set up the tornado settings dict."""
|
"""Set up the tornado settings dict."""
|
||||||
@@ -1378,13 +1248,8 @@ class JupyterHub(Application):
|
|||||||
|
|
||||||
# clean up proxy while single-user servers are shutting down
|
# clean up proxy while single-user servers are shutting down
|
||||||
if self.cleanup_proxy:
|
if self.cleanup_proxy:
|
||||||
if self.proxy_process:
|
if self.proxy.should_start:
|
||||||
self.log.info("Cleaning up proxy[%i]...", self.proxy_process.pid)
|
yield gen.maybe_future(self.proxy.stop())
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
self.log.info("I didn't start the proxy, I can't clean it up")
|
self.log.info("I didn't start the proxy, I can't clean it up")
|
||||||
else:
|
else:
|
||||||
@@ -1440,13 +1305,17 @@ class JupyterHub(Application):
|
|||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def update_last_activity(self):
|
def update_last_activity(self):
|
||||||
"""Update User.last_activity timestamps from the proxy"""
|
"""Update User.last_activity timestamps from the proxy"""
|
||||||
routes = yield self.proxy.get_routes()
|
routes = yield self.proxy.get_all_routes()
|
||||||
users_count = 0
|
users_count = 0
|
||||||
active_users_count = 0
|
active_users_count = 0
|
||||||
for prefix, route in routes.items():
|
for prefix, route in routes.items():
|
||||||
if 'user' not in route:
|
if 'user' not in route['data']:
|
||||||
# not a user route, ignore it
|
# not a user route, ignore it
|
||||||
continue
|
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'])
|
user = orm.User.find(self.db, route['user'])
|
||||||
if user is None:
|
if user is None:
|
||||||
self.log.warning("Found no user for route: %s", route)
|
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!
|
# FIXME: Make this configurable duration. 30 minutes for now!
|
||||||
if (datetime.now() - user.last_activity).total_seconds() < 30 * 60:
|
if (datetime.now() - user.last_activity).total_seconds() < 30 * 60:
|
||||||
active_users_count += 1
|
active_users_count += 1
|
||||||
users_count += 1
|
|
||||||
self.statsd.gauge('users.running', users_count)
|
self.statsd.gauge('users.running', users_count)
|
||||||
self.statsd.gauge('users.active', active_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)
|
self.log.info("Hub API listening on %s", self.hub.bind_url)
|
||||||
|
|
||||||
# start the proxy
|
# start the proxy
|
||||||
try:
|
if self.proxy.should_start:
|
||||||
yield self.start_proxy()
|
try:
|
||||||
except Exception as e:
|
yield self.proxy.start()
|
||||||
self.log.critical("Failed to start proxy", exc_info=True)
|
except Exception as e:
|
||||||
self.exit(1)
|
self.log.critical("Failed to start proxy", exc_info=True)
|
||||||
|
self.exit(1)
|
||||||
|
else:
|
||||||
|
self.log.info("Not starting proxy")
|
||||||
|
|
||||||
# start the service(s)
|
# start the service(s)
|
||||||
for service_name, service in self._service_map.items():
|
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_users, self.users)
|
||||||
loop.add_callback(self.proxy.add_all_services, self._service_map)
|
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()):
|
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)
|
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 = PeriodicCallback(self.update_last_activity, 1e3 * self.last_activity_interval)
|
||||||
pc.start()
|
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
|
# register cleanup on both TERM and INT
|
||||||
atexit.register(self.atexit)
|
atexit.register(self.atexit)
|
||||||
self.init_signal()
|
self.init_signal()
|
||||||
|
@@ -528,12 +528,12 @@ class UserSpawnHandler(BaseHandler):
|
|||||||
port = host_info.port
|
port = host_info.port
|
||||||
if not port:
|
if not port:
|
||||||
port = 443 if host_info.scheme == 'https' else 80
|
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("""
|
self.log.warning("""
|
||||||
Detected possible direct connection to Hub's private ip: %s, bypassing proxy.
|
Detected possible direct connection to Hub's private ip: %s, bypassing proxy.
|
||||||
This will result in a redirect loop.
|
This will result in a redirect loop.
|
||||||
Make sure to connect to the proxied public URL %s
|
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
|
# logged in as correct user, spawn the server
|
||||||
if current_user.spawner:
|
if current_user.spawner:
|
||||||
|
@@ -5,9 +5,17 @@
|
|||||||
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from traitlets import HasTraits
|
from tornado import gen
|
||||||
from . import orm
|
|
||||||
|
|
||||||
|
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):
|
class Server(HasTraits):
|
||||||
"""An object representing an HTTP endpoint.
|
"""An object representing an HTTP endpoint.
|
||||||
@@ -28,7 +36,7 @@ class Server(HasTraits):
|
|||||||
"""Create a Server from a given URL"""
|
"""Create a Server from a given URL"""
|
||||||
urlinfo = urlparse(url)
|
urlinfo = urlparse(url)
|
||||||
proto = urlinfo.scheme
|
proto = urlinfo.scheme
|
||||||
ip = urlinfo.hostname
|
ip = urlinfo.hostname or ''
|
||||||
port = urlinfo.port
|
port = urlinfo.port
|
||||||
if not port:
|
if not port:
|
||||||
if proto == 'https':
|
if proto == 'https':
|
||||||
@@ -113,12 +121,12 @@ class Hub(Server):
|
|||||||
def server(self):
|
def server(self):
|
||||||
"""backward-compat"""
|
"""backward-compat"""
|
||||||
return self
|
return self
|
||||||
host = Unicode()
|
public_host = Unicode()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def api_url(self):
|
def api_url(self):
|
||||||
"""return the full API url (with proto://host...)"""
|
"""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):
|
def __repr__(self):
|
||||||
return "<%s %s:%s>" % (
|
return "<%s %s:%s>" % (
|
||||||
|
@@ -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 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):
|
class Proxy(LoggingConfigurable):
|
||||||
"""
|
"""Base class for configurable proxies that JupyterHub can use."""
|
||||||
Base class for configurable proxies that JupyterHub can use
|
|
||||||
"""
|
db = Any()
|
||||||
def add_route(self, routespec, target, data):
|
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.
|
||||||
"""
|
"""
|
||||||
Add a route to the proxy
|
|
||||||
|
|
||||||
:param urlspec: A specification for which this route will be matched.
|
def stop(self):
|
||||||
Could be either a url_prefix or a fqdn.
|
"""Stop the proxy.
|
||||||
: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
|
|
||||||
be returned when retrieving information about this route.
|
|
||||||
|
|
||||||
Will raise an appropriate exception (FIXME: find what?) if the route could
|
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.
|
||||||
|
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
|
||||||
not be added.
|
not be added.
|
||||||
|
|
||||||
The proxy implementation should also have a way to associate the fact that a
|
The proxy implementation should also have a way to associate the fact that a
|
||||||
@@ -23,33 +75,342 @@ class Proxy(LoggingConfigurable):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_all_routes(self):
|
@gen.coroutine
|
||||||
"""
|
def delete_route(self, routespec):
|
||||||
Fetch and return all the routes associated by JupyterHub from the proxy
|
"""Delete a route with a given routespec if it exists."""
|
||||||
|
|
||||||
Should return a list of dictionaries, where each dictionary has the same
|
|
||||||
structure as the return value of `get_route`
|
|
||||||
"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
def get_route(self, routespec):
|
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:
|
Returns:
|
||||||
routespec: The normalized route specification passed in to add_route
|
result (dict): with the following keys:
|
||||||
target: The target for this route
|
`routespec`: The normalized route specification passed in to add_route
|
||||||
data: The arbitrary data that was passed in by JupyterHub when adding this
|
`target`: The target for this route
|
||||||
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
|
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):
|
def delete_route(self, routespec):
|
||||||
"""
|
return self.api_request(routespec, method='DELETE')
|
||||||
Delete a route with a given routespec if it exists.
|
|
||||||
"""
|
def _reformat_routespec(self, routespec, chp_data):
|
||||||
pass
|
"""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
|
||||||
|
@@ -32,11 +32,7 @@ def db():
|
|||||||
name=getuser(),
|
name=getuser(),
|
||||||
)
|
)
|
||||||
user.servers.append(orm.Server())
|
user.servers.append(orm.Server())
|
||||||
hub = orm.Hub(
|
|
||||||
server=orm.Server(),
|
|
||||||
)
|
|
||||||
_db.add(user)
|
_db.add(user)
|
||||||
_db.add(hub)
|
|
||||||
_db.commit()
|
_db.commit()
|
||||||
return _db
|
return _db
|
||||||
|
|
||||||
|
@@ -207,7 +207,7 @@ def public_host(app):
|
|||||||
if app.subdomain_host:
|
if app.subdomain_host:
|
||||||
return app.subdomain_host
|
return app.subdomain_host
|
||||||
else:
|
else:
|
||||||
return app.proxy.public_server.host
|
return urlparse(app.proxy.public_url).host
|
||||||
|
|
||||||
|
|
||||||
def public_url(app, user_or_service=None, path=''):
|
def public_url(app, user_or_service=None, path=''):
|
||||||
|
@@ -28,41 +28,6 @@ def test_server(db):
|
|||||||
assert server.url == server.host + '/'
|
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):
|
def test_user(db):
|
||||||
user = orm.User(name='kaylee',
|
user = orm.User(name='kaylee',
|
||||||
state={'pid': 4234},
|
state={'pid': 4234},
|
||||||
|
@@ -25,7 +25,7 @@ def get_page(path, app, hub=True, **kw):
|
|||||||
|
|
||||||
def test_root_no_auth(app, io_loop):
|
def test_root_no_auth(app, io_loop):
|
||||||
print(app.hub.is_up())
|
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(routes)
|
||||||
print(app.hub.server)
|
print(app.hub.server)
|
||||||
url = ujoin(public_host(app), app.hub.base_url)
|
url = ujoin(public_host(app), app.hub.base_url)
|
||||||
|
@@ -60,7 +60,7 @@ def test_external_proxy(request, io_loop):
|
|||||||
assert app.proxy_process is None
|
assert app.proxy_process is None
|
||||||
|
|
||||||
# test if api service has a root route '/'
|
# 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()) == ['/']
|
assert list(routes.keys()) == ['/']
|
||||||
|
|
||||||
# add user to the db and start a single user server
|
# 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 = api_request(app, 'users', name, 'server', method='post')
|
||||||
r.raise_for_status()
|
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
|
# sets the desired path result
|
||||||
user_path = unquote(ujoin(app.base_url, 'user/river'))
|
user_path = unquote(ujoin(app.base_url, 'user/river'))
|
||||||
if app.subdomain_host:
|
if app.subdomain_host:
|
||||||
@@ -83,7 +83,8 @@ def test_external_proxy(request, io_loop):
|
|||||||
proxy = Popen(cmd, env=env)
|
proxy = Popen(cmd, env=env)
|
||||||
wait_for_proxy()
|
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()) == ['/']
|
assert list(routes.keys()) == ['/']
|
||||||
|
|
||||||
# poke the server to update the proxy
|
# poke the server to update the proxy
|
||||||
@@ -91,7 +92,7 @@ def test_external_proxy(request, io_loop):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
# check that the routes are correct
|
# 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]
|
assert sorted(routes.keys()) == ['/', user_path]
|
||||||
|
|
||||||
# teardown the proxy, and start a new one with different auth and port
|
# 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
|
app.proxy.auth_token = new_auth_token
|
||||||
|
|
||||||
# check that the routes are correct
|
# 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]
|
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
|
# check a valid route exists for user
|
||||||
test_user = app.users[username]
|
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
|
assert unquote(test_user.proxy_path) in before
|
||||||
|
|
||||||
# check if a route is removed when user deleted
|
# 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
|
# check if a route exists for user
|
||||||
io_loop.run_sync(lambda: app.proxy.check_routes(app.users, app._service_map))
|
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
|
assert unquote(test_user.proxy_path) in after
|
||||||
|
|
||||||
# check that before and after state are the same
|
# check that before and after state are the same
|
||||||
|
@@ -64,7 +64,7 @@ def test_managed_service(mockservice):
|
|||||||
def test_proxy_service(app, mockservice_url, io_loop):
|
def test_proxy_service(app, mockservice_url, io_loop):
|
||||||
service = mockservice_url
|
service = mockservice_url
|
||||||
name = service.name
|
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'
|
url = public_url(app, service) + '/foo'
|
||||||
r = requests.get(url, allow_redirects=False)
|
r = requests.get(url, allow_redirects=False)
|
||||||
path = '/services/{}/foo'.format(name)
|
path = '/services/{}/foo'.format(name)
|
||||||
|
@@ -116,10 +116,11 @@ class User(HasTraits):
|
|||||||
self.settings = settings
|
self.settings = settings
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
hub = self.db.query(orm.Hub).first()
|
|
||||||
|
|
||||||
self.allow_named_servers = self.settings.get('allow_named_servers', False)
|
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.cookie_name = '%s-%s' % (hub.cookie_name, quote(self.name, safe=''))
|
||||||
|
|
||||||
self.base_url = url_path_join(
|
self.base_url = url_path_join(
|
||||||
self.settings.get('base_url', '/'), 'user', self.escaped_name)
|
self.settings.get('base_url', '/'), 'user', self.escaped_name)
|
||||||
|
|
||||||
@@ -222,14 +223,15 @@ class User(HasTraits):
|
|||||||
server_name = ''
|
server_name = ''
|
||||||
base_url = self.base_url
|
base_url = self.base_url
|
||||||
|
|
||||||
server = orm.Server(
|
orm_server = orm.Server(
|
||||||
name = server_name,
|
name=server_name,
|
||||||
cookie_name=self.cookie_name,
|
cookie_name=self.cookie_name,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
)
|
)
|
||||||
self.servers.append(server)
|
db.add(orm_server)
|
||||||
db.add(self)
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
server = Server(orm_server=orm_server)
|
||||||
|
self.servers.append(server)
|
||||||
|
|
||||||
api_token = self.new_api_token()
|
api_token = self.new_api_token()
|
||||||
db.commit()
|
db.commit()
|
||||||
|
Reference in New Issue
Block a user