mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-14 21:43:01 +00:00
Allow specifying statsd host/port/prefix info
Currently only passes it through to CHP. This is needed for the cases when JupyterHub spawns and maintains CHP.
This commit is contained in:
@@ -100,22 +100,22 @@ class NewToken(Application):
|
|||||||
"""Generate and print a new API token"""
|
"""Generate and print a new API token"""
|
||||||
name = 'jupyterhub-token'
|
name = 'jupyterhub-token'
|
||||||
description = """Generate and return new API token for a user.
|
description = """Generate and return new API token for a user.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
jupyterhub token [username]
|
jupyterhub token [username]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
examples = """
|
examples = """
|
||||||
$> jupyterhub token kaylee
|
$> jupyterhub token kaylee
|
||||||
ab01cd23ef45
|
ab01cd23ef45
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = Unicode(getuser())
|
name = Unicode(getuser())
|
||||||
|
|
||||||
aliases = token_aliases
|
aliases = token_aliases
|
||||||
classes = []
|
classes = []
|
||||||
|
|
||||||
def parse_command_line(self, argv=None):
|
def parse_command_line(self, argv=None):
|
||||||
super().parse_command_line(argv=argv)
|
super().parse_command_line(argv=argv)
|
||||||
if not self.extra_args:
|
if not self.extra_args:
|
||||||
@@ -124,7 +124,7 @@ class NewToken(Application):
|
|||||||
print("Must specify exactly one username", file=sys.stderr)
|
print("Must specify exactly one username", file=sys.stderr)
|
||||||
self.exit(1)
|
self.exit(1)
|
||||||
self.name = self.extra_args[0]
|
self.name = self.extra_args[0]
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
hub = JupyterHub(parent=self)
|
hub = JupyterHub(parent=self)
|
||||||
hub.load_config_file(hub.config_file)
|
hub.load_config_file(hub.config_file)
|
||||||
@@ -143,39 +143,39 @@ class JupyterHub(Application):
|
|||||||
"""An Application for starting a Multi-User Jupyter Notebook server."""
|
"""An Application for starting a Multi-User Jupyter Notebook server."""
|
||||||
name = 'jupyterhub'
|
name = 'jupyterhub'
|
||||||
version = jupyterhub.__version__
|
version = jupyterhub.__version__
|
||||||
|
|
||||||
description = """Start a multi-user Jupyter Notebook server
|
description = """Start a multi-user Jupyter Notebook server
|
||||||
|
|
||||||
Spawns a configurable-http-proxy and multi-user Hub,
|
Spawns a configurable-http-proxy and multi-user Hub,
|
||||||
which authenticates users and spawns single-user Notebook servers
|
which authenticates users and spawns single-user Notebook servers
|
||||||
on behalf of users.
|
on behalf of users.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
examples = """
|
examples = """
|
||||||
|
|
||||||
generate default config file:
|
generate default config file:
|
||||||
|
|
||||||
jupyterhub --generate-config -f /etc/jupyterhub/jupyterhub.py
|
jupyterhub --generate-config -f /etc/jupyterhub/jupyterhub.py
|
||||||
|
|
||||||
spawn the server on 10.0.1.2:443 with https:
|
spawn the server on 10.0.1.2:443 with https:
|
||||||
|
|
||||||
jupyterhub --ip 10.0.1.2 --port 443 --ssl-key my_ssl.key --ssl-cert my_ssl.cert
|
jupyterhub --ip 10.0.1.2 --port 443 --ssl-key my_ssl.key --ssl-cert my_ssl.cert
|
||||||
"""
|
"""
|
||||||
|
|
||||||
aliases = Dict(aliases)
|
aliases = Dict(aliases)
|
||||||
flags = Dict(flags)
|
flags = Dict(flags)
|
||||||
|
|
||||||
subcommands = {
|
subcommands = {
|
||||||
'token': (NewToken, "Generate an API token for a user")
|
'token': (NewToken, "Generate an API token for a user")
|
||||||
}
|
}
|
||||||
|
|
||||||
classes = List([
|
classes = List([
|
||||||
Spawner,
|
Spawner,
|
||||||
LocalProcessSpawner,
|
LocalProcessSpawner,
|
||||||
Authenticator,
|
Authenticator,
|
||||||
PAMAuthenticator,
|
PAMAuthenticator,
|
||||||
])
|
])
|
||||||
|
|
||||||
config_file = Unicode('jupyterhub_config.py',
|
config_file = Unicode('jupyterhub_config.py',
|
||||||
help="The config file to load",
|
help="The config file to load",
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
@@ -201,7 +201,7 @@ class JupyterHub(Application):
|
|||||||
proxy_check_interval = Integer(30,
|
proxy_check_interval = Integer(30,
|
||||||
help="Interval (in seconds) at which to check if the proxy is running."
|
help="Interval (in seconds) at which to check if the proxy is running."
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
data_files_path = Unicode(DATA_FILES_PATH,
|
data_files_path = Unicode(DATA_FILES_PATH,
|
||||||
help="The location of jupyterhub data files (e.g. /usr/local/share/jupyter/hub)"
|
help="The location of jupyterhub data files (e.g. /usr/local/share/jupyter/hub)"
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
@@ -213,7 +213,7 @@ class JupyterHub(Application):
|
|||||||
@default('template_paths')
|
@default('template_paths')
|
||||||
def _template_paths_default(self):
|
def _template_paths_default(self):
|
||||||
return [os.path.join(self.data_files_path, 'templates')]
|
return [os.path.join(self.data_files_path, 'templates')]
|
||||||
|
|
||||||
confirm_no_ssl = Bool(False,
|
confirm_no_ssl = Bool(False,
|
||||||
help="""Confirm that JupyterHub should be run without SSL.
|
help="""Confirm that JupyterHub should be run without SSL.
|
||||||
This is **NOT RECOMMENDED** unless SSL termination is being handled by another layer.
|
This is **NOT RECOMMENDED** unless SSL termination is being handled by another layer.
|
||||||
@@ -221,20 +221,20 @@ class JupyterHub(Application):
|
|||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
ssl_key = Unicode('',
|
ssl_key = Unicode('',
|
||||||
help="""Path to SSL key file for the public facing interface of the proxy
|
help="""Path to SSL key file for the public facing interface of the proxy
|
||||||
|
|
||||||
Use with ssl_cert
|
Use with ssl_cert
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
ssl_cert = Unicode('',
|
ssl_cert = Unicode('',
|
||||||
help="""Path to SSL certificate file for the public facing interface of the proxy
|
help="""Path to SSL certificate file for the public facing interface of the proxy
|
||||||
|
|
||||||
Use with ssl_key
|
Use with ssl_key
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
ip = Unicode('',
|
ip = Unicode('',
|
||||||
help="The public facing ip of the whole application (the proxy)"
|
help="The public facing ip of the whole application (the proxy)"
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
subdomain_host = Unicode('',
|
subdomain_host = Unicode('',
|
||||||
help="""Run single-user servers on subdomains of this host.
|
help="""Run single-user servers on subdomains of this host.
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ class JupyterHub(Application):
|
|||||||
# host should include '://'
|
# host should include '://'
|
||||||
# if not specified, assume https: You have to be really explicit about HTTP!
|
# if not specified, assume https: You have to be really explicit about HTTP!
|
||||||
self.subdomain_host = 'https://' + new
|
self.subdomain_host = 'https://' + new
|
||||||
|
|
||||||
port = Integer(8000,
|
port = Integer(8000,
|
||||||
help="The public facing port of the proxy"
|
help="The public facing port of the proxy"
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
@@ -268,14 +268,14 @@ class JupyterHub(Application):
|
|||||||
@default('logo_file')
|
@default('logo_file')
|
||||||
def _logo_file_default(self):
|
def _logo_file_default(self):
|
||||||
return os.path.join(self.data_files_path, 'static', 'images', 'jupyter.png')
|
return os.path.join(self.data_files_path, 'static', 'images', 'jupyter.png')
|
||||||
|
|
||||||
jinja_environment_options = Dict(
|
jinja_environment_options = Dict(
|
||||||
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_cmd = Command('configurable-http-proxy',
|
||||||
help="""The command to start the http proxy.
|
help="""The command to start the http proxy.
|
||||||
|
|
||||||
Only override if configurable-http-proxy is not on your PATH
|
Only override if configurable-http-proxy is not on your PATH
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
@@ -288,7 +288,7 @@ class JupyterHub(Application):
|
|||||||
Loaded from the CONFIGPROXY_AUTH_TOKEN env variable by default.
|
Loaded from the CONFIGPROXY_AUTH_TOKEN env variable by default.
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
@default('proxy_auth_token')
|
@default('proxy_auth_token')
|
||||||
def _proxy_auth_token_default(self):
|
def _proxy_auth_token_default(self):
|
||||||
token = os.environ.get('CONFIGPROXY_AUTH_TOKEN', None)
|
token = os.environ.get('CONFIGPROXY_AUTH_TOKEN', None)
|
||||||
@@ -301,18 +301,18 @@ class JupyterHub(Application):
|
|||||||
]))
|
]))
|
||||||
token = orm.new_token()
|
token = orm.new_token()
|
||||||
return token
|
return token
|
||||||
|
|
||||||
proxy_api_ip = Unicode('127.0.0.1',
|
proxy_api_ip = Unicode('127.0.0.1',
|
||||||
help="The ip for the proxy API handlers"
|
help="The ip for the proxy API handlers"
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
proxy_api_port = Integer(
|
proxy_api_port = Integer(
|
||||||
help="The port for the proxy API handlers"
|
help="The port for the proxy API handlers"
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
@default('proxy_api_port')
|
@default('proxy_api_port')
|
||||||
def _proxy_api_port_default(self):
|
def _proxy_api_port_default(self):
|
||||||
return self.port + 1
|
return self.port + 1
|
||||||
|
|
||||||
hub_port = Integer(8081,
|
hub_port = Integer(8081,
|
||||||
help="The port for this process"
|
help="The port for this process"
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
@@ -322,18 +322,18 @@ class JupyterHub(Application):
|
|||||||
hub_prefix = URLPrefix('/hub/',
|
hub_prefix = URLPrefix('/hub/',
|
||||||
help="The prefix for the hub server. Must not be '/'"
|
help="The prefix for the hub server. Must not be '/'"
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
@default('hub_prefix')
|
@default('hub_prefix')
|
||||||
def _hub_prefix_default(self):
|
def _hub_prefix_default(self):
|
||||||
return url_path_join(self.base_url, '/hub/')
|
return url_path_join(self.base_url, '/hub/')
|
||||||
|
|
||||||
@observe('hub_prefix')
|
@observe('hub_prefix')
|
||||||
def _hub_prefix_changed(self, name, old, new):
|
def _hub_prefix_changed(self, name, old, new):
|
||||||
if new == '/':
|
if new == '/':
|
||||||
raise TraitError("'/' is not a valid hub prefix")
|
raise TraitError("'/' is not a valid hub prefix")
|
||||||
if not new.startswith(self.base_url):
|
if not new.startswith(self.base_url):
|
||||||
self.hub_prefix = url_path_join(self.base_url, new)
|
self.hub_prefix = url_path_join(self.base_url, new)
|
||||||
|
|
||||||
cookie_secret = Bytes(
|
cookie_secret = Bytes(
|
||||||
help="""The cookie secret to use to encrypt cookies.
|
help="""The cookie secret to use to encrypt cookies.
|
||||||
|
|
||||||
@@ -343,18 +343,18 @@ class JupyterHub(Application):
|
|||||||
config=True,
|
config=True,
|
||||||
env='JPY_COOKIE_SECRET',
|
env='JPY_COOKIE_SECRET',
|
||||||
)
|
)
|
||||||
|
|
||||||
cookie_secret_file = Unicode('jupyterhub_cookie_secret',
|
cookie_secret_file = Unicode('jupyterhub_cookie_secret',
|
||||||
help="""File in which to store the cookie secret."""
|
help="""File in which to store the cookie secret."""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
authenticator_class = Type(PAMAuthenticator, Authenticator,
|
authenticator_class = Type(PAMAuthenticator, Authenticator,
|
||||||
help="""Class for authenticating users.
|
help="""Class for authenticating users.
|
||||||
|
|
||||||
This should be a class with the following form:
|
This should be a class with the following form:
|
||||||
|
|
||||||
- constructor takes one kwarg: `config`, the IPython config object.
|
- constructor takes one kwarg: `config`, the IPython config object.
|
||||||
|
|
||||||
- is a tornado.gen.coroutine
|
- is a tornado.gen.coroutine
|
||||||
- returns username on success, None on failure
|
- returns username on success, None on failure
|
||||||
- takes two arguments: (handler, data),
|
- takes two arguments: (handler, data),
|
||||||
@@ -362,7 +362,7 @@ class JupyterHub(Application):
|
|||||||
and `data` is the POST form data from the login page.
|
and `data` is the POST form data from the login page.
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
authenticator = Instance(Authenticator)
|
authenticator = Instance(Authenticator)
|
||||||
|
|
||||||
@default('authenticator')
|
@default('authenticator')
|
||||||
@@ -372,11 +372,11 @@ class JupyterHub(Application):
|
|||||||
# class for spawning single-user servers
|
# class for spawning single-user servers
|
||||||
spawner_class = Type(LocalProcessSpawner, Spawner,
|
spawner_class = Type(LocalProcessSpawner, Spawner,
|
||||||
help="""The class to use for spawning single-user servers.
|
help="""The class to use for spawning single-user servers.
|
||||||
|
|
||||||
Should be a subclass of Spawner.
|
Should be a subclass of Spawner.
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
db_url = Unicode('sqlite:///jupyterhub.sqlite',
|
db_url = Unicode('sqlite:///jupyterhub.sqlite',
|
||||||
help="url for the database. e.g. `sqlite:///jupyterhub.sqlite`"
|
help="url for the database. e.g. `sqlite:///jupyterhub.sqlite`"
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
@@ -401,65 +401,79 @@ class JupyterHub(Application):
|
|||||||
help="log all database transactions. This has A LOT of output"
|
help="log all database transactions. This has A LOT of output"
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
session_factory = Any()
|
session_factory = Any()
|
||||||
|
|
||||||
users = Instance(UserDict)
|
users = Instance(UserDict)
|
||||||
|
|
||||||
@default('users')
|
@default('users')
|
||||||
def _users_default(self):
|
def _users_default(self):
|
||||||
assert self.tornado_settings
|
assert self.tornado_settings
|
||||||
return UserDict(db_factory=lambda : self.db, settings=self.tornado_settings)
|
return UserDict(db_factory=lambda : self.db, settings=self.tornado_settings)
|
||||||
|
|
||||||
admin_access = Bool(False,
|
admin_access = Bool(False,
|
||||||
help="""Grant admin users permission to access single-user servers.
|
help="""Grant admin users permission to access single-user servers.
|
||||||
|
|
||||||
Users should be properly informed if this is enabled.
|
Users should be properly informed if this is enabled.
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
admin_users = Set(
|
admin_users = Set(
|
||||||
help="""DEPRECATED, use Authenticator.admin_users instead."""
|
help="""DEPRECATED, use Authenticator.admin_users instead."""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
tornado_settings = Dict(
|
tornado_settings = Dict(
|
||||||
help="Extra settings overrides to pass to the tornado application."
|
help="Extra settings overrides to pass to the tornado application."
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
cleanup_servers = Bool(True,
|
cleanup_servers = Bool(True,
|
||||||
help="""Whether to shutdown single-user servers when the Hub shuts down.
|
help="""Whether to shutdown single-user servers when the Hub shuts down.
|
||||||
|
|
||||||
Disable if you want to be able to teardown the Hub while leaving the single-user servers running.
|
Disable if you want to be able to teardown the Hub while leaving the single-user servers running.
|
||||||
|
|
||||||
If both this and cleanup_proxy are False, sending SIGINT to the Hub will
|
If both this and cleanup_proxy are False, sending SIGINT to the Hub will
|
||||||
only shutdown the Hub, leaving everything else running.
|
only shutdown the Hub, leaving everything else running.
|
||||||
|
|
||||||
The Hub should be able to resume from database state.
|
The Hub should be able to resume from database state.
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
cleanup_proxy = Bool(True,
|
cleanup_proxy = Bool(True,
|
||||||
help="""Whether to shutdown the proxy when the Hub shuts down.
|
help="""Whether to shutdown the proxy when the Hub shuts down.
|
||||||
|
|
||||||
Disable if you want to be able to teardown the Hub while leaving the proxy running.
|
Disable if you want to be able to teardown the Hub while leaving the proxy running.
|
||||||
|
|
||||||
Only valid if the proxy was starting by the Hub process.
|
Only valid if the proxy was starting by the Hub process.
|
||||||
|
|
||||||
If both this and cleanup_servers are False, sending SIGINT to the Hub will
|
If both this and cleanup_servers are False, sending SIGINT to the Hub will
|
||||||
only shutdown the Hub, leaving everything else running.
|
only shutdown the Hub, leaving everything else running.
|
||||||
|
|
||||||
The Hub should be able to resume from database state.
|
The Hub should be able to resume from database state.
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
|
statsd_host = Unicode(
|
||||||
|
help="Host to send statds metrics to"
|
||||||
|
).tag(config=True)
|
||||||
|
|
||||||
|
statsd_port = Int(
|
||||||
|
8125,
|
||||||
|
help="Port on which to send statsd metrics about the hub"
|
||||||
|
).tag(config=True)
|
||||||
|
|
||||||
|
statsd_prefix = Unicode(
|
||||||
|
'jupyterhub',
|
||||||
|
help="Prefix to use for all metrics sent by jupyterhub to statsd"
|
||||||
|
)
|
||||||
|
|
||||||
handlers = List()
|
handlers = List()
|
||||||
|
|
||||||
_log_formatter_cls = CoroutineLogFormatter
|
_log_formatter_cls = CoroutineLogFormatter
|
||||||
http_server = None
|
http_server = None
|
||||||
proxy_process = None
|
proxy_process = None
|
||||||
io_loop = None
|
io_loop = None
|
||||||
|
|
||||||
@default('log_level')
|
@default('log_level')
|
||||||
def _log_level_default(self):
|
def _log_level_default(self):
|
||||||
return logging.INFO
|
return logging.INFO
|
||||||
|
|
||||||
@default('log_datefmt')
|
@default('log_datefmt')
|
||||||
def _log_datefmt_default(self):
|
def _log_datefmt_default(self):
|
||||||
"""Exclude date from default date format"""
|
"""Exclude date from default date format"""
|
||||||
@@ -514,7 +528,7 @@ class JupyterHub(Application):
|
|||||||
raise TraitError("The hub and proxy API cannot both listen on port %i" % self.hub_port)
|
raise TraitError("The hub and proxy API cannot both listen on port %i" % self.hub_port)
|
||||||
if self.proxy_api_port == self.port:
|
if self.proxy_api_port == self.port:
|
||||||
raise TraitError("The proxy's public and API ports cannot both be %i" % 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):
|
||||||
"""add a url prefix to handlers"""
|
"""add a url prefix to handlers"""
|
||||||
@@ -523,7 +537,7 @@ class JupyterHub(Application):
|
|||||||
lis[0] = url_path_join(prefix, tup[0])
|
lis[0] = url_path_join(prefix, tup[0])
|
||||||
handlers[i] = tuple(lis)
|
handlers[i] = tuple(lis)
|
||||||
return handlers
|
return handlers
|
||||||
|
|
||||||
def init_handlers(self):
|
def init_handlers(self):
|
||||||
h = []
|
h = []
|
||||||
# load handlers from the authenticator
|
# load handlers from the authenticator
|
||||||
@@ -531,7 +545,7 @@ class JupyterHub(Application):
|
|||||||
# set default handlers
|
# set default handlers
|
||||||
h.extend(handlers.default_handlers)
|
h.extend(handlers.default_handlers)
|
||||||
h.extend(apihandlers.default_handlers)
|
h.extend(apihandlers.default_handlers)
|
||||||
|
|
||||||
h.append((r'/logo', LogoHandler, {'path': self.logo_file}))
|
h.append((r'/logo', LogoHandler, {'path': self.logo_file}))
|
||||||
self.handlers = self.add_url_prefix(self.hub_prefix, h)
|
self.handlers = self.add_url_prefix(self.hub_prefix, h)
|
||||||
# some extra handlers, outside hub_prefix
|
# some extra handlers, outside hub_prefix
|
||||||
@@ -545,7 +559,7 @@ class JupyterHub(Application):
|
|||||||
(r"(?!%s).*" % self.hub_prefix, handlers.PrefixRedirectHandler),
|
(r"(?!%s).*" % self.hub_prefix, handlers.PrefixRedirectHandler),
|
||||||
(r'(.*)', handlers.Template404),
|
(r'(.*)', handlers.Template404),
|
||||||
])
|
])
|
||||||
|
|
||||||
def _check_db_path(self, path):
|
def _check_db_path(self, path):
|
||||||
"""More informative log messages for failed filesystem access"""
|
"""More informative log messages for failed filesystem access"""
|
||||||
path = os.path.abspath(path)
|
path = os.path.abspath(path)
|
||||||
@@ -557,7 +571,7 @@ class JupyterHub(Application):
|
|||||||
self.log.error("%s cannot create files in %s", user, parent)
|
self.log.error("%s cannot create files in %s", user, parent)
|
||||||
if os.path.exists(path) and not os.access(path, os.W_OK):
|
if os.path.exists(path) and not os.access(path, os.W_OK):
|
||||||
self.log.error("%s cannot edit %s", user, path)
|
self.log.error("%s cannot edit %s", user, path)
|
||||||
|
|
||||||
def init_secrets(self):
|
def init_secrets(self):
|
||||||
trait_name = 'cookie_secret'
|
trait_name = 'cookie_secret'
|
||||||
trait = self.traits()[trait_name]
|
trait = self.traits()[trait_name]
|
||||||
@@ -589,7 +603,7 @@ class JupyterHub(Application):
|
|||||||
secret_from = 'new'
|
secret_from = 'new'
|
||||||
self.log.debug("Generating new %s", trait_name)
|
self.log.debug("Generating new %s", trait_name)
|
||||||
secret = os.urandom(SECRET_BYTES)
|
secret = os.urandom(SECRET_BYTES)
|
||||||
|
|
||||||
if secret_file and secret_from == 'new':
|
if secret_file and secret_from == 'new':
|
||||||
# if we generated a new secret, store it in the secret_file
|
# if we generated a new secret, store it in the secret_file
|
||||||
self.log.info("Writing %s to %s", trait_name, secret_file)
|
self.log.info("Writing %s to %s", trait_name, secret_file)
|
||||||
@@ -602,7 +616,7 @@ class JupyterHub(Application):
|
|||||||
self.log.warning("Failed to set permissions on %s", secret_file)
|
self.log.warning("Failed to set permissions on %s", secret_file)
|
||||||
# store the loaded trait value
|
# store the loaded trait value
|
||||||
self.cookie_secret = secret
|
self.cookie_secret = secret
|
||||||
|
|
||||||
# thread-local storage of db objects
|
# thread-local storage of db objects
|
||||||
_local = Instance(threading.local, ())
|
_local = Instance(threading.local, ())
|
||||||
@property
|
@property
|
||||||
@@ -610,7 +624,7 @@ class JupyterHub(Application):
|
|||||||
if not hasattr(self._local, 'db'):
|
if not hasattr(self._local, 'db'):
|
||||||
self._local.db = scoped_session(self.session_factory)()
|
self._local.db = scoped_session(self.session_factory)()
|
||||||
return self._local.db
|
return self._local.db
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hub(self):
|
def hub(self):
|
||||||
if not getattr(self._local, 'hub', None):
|
if not getattr(self._local, 'hub', None):
|
||||||
@@ -620,13 +634,13 @@ class JupyterHub(Application):
|
|||||||
if self.subdomain_host and self._local.hub:
|
if self.subdomain_host and self._local.hub:
|
||||||
self._local.hub.host = self.subdomain_host
|
self._local.hub.host = self.subdomain_host
|
||||||
return self._local.hub
|
return self._local.hub
|
||||||
|
|
||||||
@hub.setter
|
@hub.setter
|
||||||
def hub(self, hub):
|
def hub(self, hub):
|
||||||
self._local.hub = hub
|
self._local.hub = hub
|
||||||
if hub and self.subdomain_host:
|
if hub and self.subdomain_host:
|
||||||
hub.host = self.subdomain_host
|
hub.host = self.subdomain_host
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def proxy(self):
|
def proxy(self):
|
||||||
if not getattr(self._local, 'proxy', None):
|
if not getattr(self._local, 'proxy', None):
|
||||||
@@ -636,11 +650,11 @@ class JupyterHub(Application):
|
|||||||
if p:
|
if p:
|
||||||
p.auth_token = self.proxy_auth_token
|
p.auth_token = self.proxy_auth_token
|
||||||
return self._local.proxy
|
return self._local.proxy
|
||||||
|
|
||||||
@proxy.setter
|
@proxy.setter
|
||||||
def proxy(self, proxy):
|
def proxy(self, proxy):
|
||||||
self._local.proxy = proxy
|
self._local.proxy = proxy
|
||||||
|
|
||||||
def init_db(self):
|
def init_db(self):
|
||||||
"""Create the database connection"""
|
"""Create the database connection"""
|
||||||
self.log.debug("Connecting to db: %s", self.db_url)
|
self.log.debug("Connecting to db: %s", self.db_url)
|
||||||
@@ -659,7 +673,7 @@ class JupyterHub(Application):
|
|||||||
if self.db_url.startswith('sqlite:///'):
|
if self.db_url.startswith('sqlite:///'):
|
||||||
self._check_db_path(self.db_url.split(':///', 1)[1])
|
self._check_db_path(self.db_url.split(':///', 1)[1])
|
||||||
self.exit(1)
|
self.exit(1)
|
||||||
|
|
||||||
def init_hub(self):
|
def init_hub(self):
|
||||||
"""Load the Hub config into the database"""
|
"""Load the Hub config into the database"""
|
||||||
self.hub = self.db.query(orm.Hub).first()
|
self.hub = self.db.query(orm.Hub).first()
|
||||||
@@ -684,12 +698,12 @@ class JupyterHub(Application):
|
|||||||
" This should be the public domain[:port] of the Hub.")
|
" This should be the public domain[:port] of the Hub.")
|
||||||
|
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def init_users(self):
|
def init_users(self):
|
||||||
"""Load users into and from the database"""
|
"""Load users into and from the database"""
|
||||||
db = self.db
|
db = self.db
|
||||||
|
|
||||||
if self.admin_users and not self.authenticator.admin_users:
|
if self.admin_users and not self.authenticator.admin_users:
|
||||||
self.log.warning(
|
self.log.warning(
|
||||||
"\nJupyterHub.admin_users is deprecated."
|
"\nJupyterHub.admin_users is deprecated."
|
||||||
@@ -704,11 +718,11 @@ class JupyterHub(Application):
|
|||||||
for username in admin_users:
|
for username in admin_users:
|
||||||
if not self.authenticator.validate_username(username):
|
if not self.authenticator.validate_username(username):
|
||||||
raise ValueError("username %r is not valid" % username)
|
raise ValueError("username %r is not valid" % username)
|
||||||
|
|
||||||
if not admin_users:
|
if not admin_users:
|
||||||
self.log.warning("No admin users, admin interface will be unavailable.")
|
self.log.warning("No admin users, admin interface will be unavailable.")
|
||||||
self.log.warning("Add any administrative users to `c.Authenticator.admin_users` in config.")
|
self.log.warning("Add any administrative users to `c.Authenticator.admin_users` in config.")
|
||||||
|
|
||||||
new_users = []
|
new_users = []
|
||||||
|
|
||||||
for name in admin_users:
|
for name in admin_users:
|
||||||
@@ -759,11 +773,11 @@ class JupyterHub(Application):
|
|||||||
# From this point on, any user changes should be done simultaneously
|
# From this point on, any user changes should be done simultaneously
|
||||||
# to the whitelist set and user db, unless the whitelist is empty (all users allowed).
|
# to the whitelist set and user db, unless the whitelist is empty (all users allowed).
|
||||||
|
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def init_spawners(self):
|
def init_spawners(self):
|
||||||
db = self.db
|
db = self.db
|
||||||
|
|
||||||
user_summaries = ['']
|
user_summaries = ['']
|
||||||
def _user_summary(user):
|
def _user_summary(user):
|
||||||
parts = ['{0: >8}'.format(user.name)]
|
parts = ['{0: >8}'.format(user.name)]
|
||||||
@@ -772,7 +786,7 @@ class JupyterHub(Application):
|
|||||||
if user.server:
|
if user.server:
|
||||||
parts.append('running at %s' % user.server)
|
parts.append('running at %s' % user.server)
|
||||||
return ' '.join(parts)
|
return ' '.join(parts)
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def user_stopped(user):
|
def user_stopped(user):
|
||||||
status = yield user.spawner.poll()
|
status = yield user.spawner.poll()
|
||||||
@@ -781,7 +795,7 @@ class JupyterHub(Application):
|
|||||||
)
|
)
|
||||||
yield self.proxy.delete_user(user)
|
yield self.proxy.delete_user(user)
|
||||||
yield user.stop()
|
yield user.stop()
|
||||||
|
|
||||||
for orm_user in db.query(orm.User):
|
for orm_user in db.query(orm.User):
|
||||||
self.users[orm_user.id] = user = User(orm_user, self.tornado_settings)
|
self.users[orm_user.id] = user = User(orm_user, self.tornado_settings)
|
||||||
if not user.state:
|
if not user.state:
|
||||||
@@ -827,7 +841,7 @@ class JupyterHub(Application):
|
|||||||
self.proxy.api_server.port = self.proxy_api_port
|
self.proxy.api_server.port = self.proxy_api_port
|
||||||
self.proxy.api_server.base_url = '/api/routes/'
|
self.proxy.api_server.base_url = '/api/routes/'
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def start_proxy(self):
|
def start_proxy(self):
|
||||||
"""Actually start the configurable-http-proxy"""
|
"""Actually start the configurable-http-proxy"""
|
||||||
@@ -867,7 +881,13 @@ class JupyterHub(Application):
|
|||||||
cmd.extend(['--ssl-key', self.ssl_key])
|
cmd.extend(['--ssl-key', self.ssl_key])
|
||||||
if self.ssl_cert:
|
if self.ssl_cert:
|
||||||
cmd.extend(['--ssl-cert', self.ssl_cert])
|
cmd.extend(['--ssl-cert', self.ssl_cert])
|
||||||
# Require SSL to be used or `--no-ssl` to confirm no SSL on
|
if self.statsd_host:
|
||||||
|
cmd.extend([
|
||||||
|
'--statsd-host', self.statsd_host,
|
||||||
|
'--statsd-port', self.statsd_port,
|
||||||
|
'--statsd-prefix', self.statsd_prefix + '.chp'
|
||||||
|
])
|
||||||
|
# Require SSL to be used or `--no-ssl` to confirm no SSL on
|
||||||
if ' --ssl' not in ' '.join(cmd):
|
if ' --ssl' not in ' '.join(cmd):
|
||||||
if self.confirm_no_ssl:
|
if self.confirm_no_ssl:
|
||||||
self.log.warning("Running JupyterHub without SSL."
|
self.log.warning("Running JupyterHub without SSL."
|
||||||
@@ -897,7 +917,7 @@ class JupyterHub(Application):
|
|||||||
# py2-compatible `raise e from None`
|
# py2-compatible `raise e from None`
|
||||||
e.__cause__ = None
|
e.__cause__ = None
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
for server in (self.proxy.public_server, self.proxy.api_server):
|
for server in (self.proxy.public_server, self.proxy.api_server):
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
_check()
|
_check()
|
||||||
@@ -909,7 +929,7 @@ class JupyterHub(Application):
|
|||||||
break
|
break
|
||||||
yield server.wait_up(1)
|
yield server.wait_up(1)
|
||||||
self.log.debug("Proxy started and appears to be up")
|
self.log.debug("Proxy started and appears to be up")
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def check_proxy(self):
|
def check_proxy(self):
|
||||||
if self.proxy_process.poll() is None:
|
if self.proxy_process.poll() is None:
|
||||||
@@ -921,7 +941,7 @@ class JupyterHub(Application):
|
|||||||
self.log.info("Setting up routes on new proxy")
|
self.log.info("Setting up routes on new proxy")
|
||||||
yield self.proxy.add_all_users(self.users)
|
yield self.proxy.add_all_users(self.users)
|
||||||
self.log.info("New proxy back up, and good to go")
|
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."""
|
||||||
base_url = self.hub.server.base_url
|
base_url = self.hub.server.base_url
|
||||||
@@ -933,10 +953,10 @@ class JupyterHub(Application):
|
|||||||
loader=FileSystemLoader(self.template_paths),
|
loader=FileSystemLoader(self.template_paths),
|
||||||
**jinja_options
|
**jinja_options
|
||||||
)
|
)
|
||||||
|
|
||||||
login_url = self.authenticator.login_url(base_url)
|
login_url = self.authenticator.login_url(base_url)
|
||||||
logout_url = self.authenticator.logout_url(base_url)
|
logout_url = self.authenticator.logout_url(base_url)
|
||||||
|
|
||||||
# if running from git, disable caching of require.js
|
# if running from git, disable caching of require.js
|
||||||
# otherwise cache based on server start time
|
# otherwise cache based on server start time
|
||||||
parent = os.path.dirname(os.path.dirname(jupyterhub.__file__))
|
parent = os.path.dirname(os.path.dirname(jupyterhub.__file__))
|
||||||
@@ -944,7 +964,7 @@ class JupyterHub(Application):
|
|||||||
version_hash = ''
|
version_hash = ''
|
||||||
else:
|
else:
|
||||||
version_hash=datetime.now().strftime("%Y%m%d%H%M%S"),
|
version_hash=datetime.now().strftime("%Y%m%d%H%M%S"),
|
||||||
|
|
||||||
subdomain_host = self.subdomain_host
|
subdomain_host = self.subdomain_host
|
||||||
domain = urlparse(subdomain_host).hostname
|
domain = urlparse(subdomain_host).hostname
|
||||||
settings = dict(
|
settings = dict(
|
||||||
@@ -977,18 +997,18 @@ class JupyterHub(Application):
|
|||||||
self.tornado_settings = settings
|
self.tornado_settings = settings
|
||||||
# constructing users requires access to tornado_settings
|
# constructing users requires access to tornado_settings
|
||||||
self.tornado_settings['users'] = self.users
|
self.tornado_settings['users'] = self.users
|
||||||
|
|
||||||
def init_tornado_application(self):
|
def init_tornado_application(self):
|
||||||
"""Instantiate the tornado Application object"""
|
"""Instantiate the tornado Application object"""
|
||||||
self.tornado_application = web.Application(self.handlers, **self.tornado_settings)
|
self.tornado_application = web.Application(self.handlers, **self.tornado_settings)
|
||||||
|
|
||||||
def write_pid_file(self):
|
def write_pid_file(self):
|
||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
if self.pid_file:
|
if self.pid_file:
|
||||||
self.log.debug("Writing PID %i to %s", pid, self.pid_file)
|
self.log.debug("Writing PID %i to %s", pid, self.pid_file)
|
||||||
with open(self.pid_file, 'w') as f:
|
with open(self.pid_file, 'w') as f:
|
||||||
f.write('%i' % pid)
|
f.write('%i' % pid)
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
@catch_config_error
|
@catch_config_error
|
||||||
def initialize(self, *args, **kwargs):
|
def initialize(self, *args, **kwargs):
|
||||||
@@ -1017,11 +1037,11 @@ class JupyterHub(Application):
|
|||||||
yield self.init_spawners()
|
yield self.init_spawners()
|
||||||
self.init_handlers()
|
self.init_handlers()
|
||||||
self.init_tornado_application()
|
self.init_tornado_application()
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
"""Shutdown our various subprocesses and cleanup runtime files."""
|
"""Shutdown our various subprocesses and cleanup runtime files."""
|
||||||
|
|
||||||
futures = []
|
futures = []
|
||||||
if self.cleanup_servers:
|
if self.cleanup_servers:
|
||||||
self.log.info("Cleaning up single-user servers...")
|
self.log.info("Cleaning up single-user servers...")
|
||||||
@@ -1031,7 +1051,7 @@ class JupyterHub(Application):
|
|||||||
futures.append(user.stop())
|
futures.append(user.stop())
|
||||||
else:
|
else:
|
||||||
self.log.info("Leaving single-user servers running")
|
self.log.info("Leaving single-user servers running")
|
||||||
|
|
||||||
# clean up proxy while SUS are shutting down
|
# clean up proxy while SUS are shutting down
|
||||||
if self.cleanup_proxy:
|
if self.cleanup_proxy:
|
||||||
if self.proxy_process:
|
if self.proxy_process:
|
||||||
@@ -1045,24 +1065,24 @@ class JupyterHub(Application):
|
|||||||
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:
|
||||||
self.log.info("Leaving proxy running")
|
self.log.info("Leaving proxy running")
|
||||||
|
|
||||||
|
|
||||||
# wait for the requests to stop finish:
|
# wait for the requests to stop finish:
|
||||||
for f in futures:
|
for f in futures:
|
||||||
try:
|
try:
|
||||||
yield f
|
yield f
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error("Failed to stop user: %s", e)
|
self.log.error("Failed to stop user: %s", e)
|
||||||
|
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
if self.pid_file and os.path.exists(self.pid_file):
|
if self.pid_file and os.path.exists(self.pid_file):
|
||||||
self.log.info("Cleaning up PID file %s", self.pid_file)
|
self.log.info("Cleaning up PID file %s", self.pid_file)
|
||||||
os.remove(self.pid_file)
|
os.remove(self.pid_file)
|
||||||
|
|
||||||
# finally stop the loop once we are all cleaned up
|
# finally stop the loop once we are all cleaned up
|
||||||
self.log.info("...done")
|
self.log.info("...done")
|
||||||
|
|
||||||
def write_config_file(self):
|
def write_config_file(self):
|
||||||
"""Write our default config to a .py config file"""
|
"""Write our default config to a .py config file"""
|
||||||
if os.path.exists(self.config_file) and not self.answer_yes:
|
if os.path.exists(self.config_file) and not self.answer_yes:
|
||||||
@@ -1080,14 +1100,14 @@ class JupyterHub(Application):
|
|||||||
answer = ask()
|
answer = ask()
|
||||||
if answer.startswith('n'):
|
if answer.startswith('n'):
|
||||||
return
|
return
|
||||||
|
|
||||||
config_text = self.generate_config_file()
|
config_text = self.generate_config_file()
|
||||||
if isinstance(config_text, bytes):
|
if isinstance(config_text, bytes):
|
||||||
config_text = config_text.decode('utf8')
|
config_text = config_text.decode('utf8')
|
||||||
print("Writing default config to: %s" % self.config_file)
|
print("Writing default config to: %s" % self.config_file)
|
||||||
with open(self.config_file, mode='w') as f:
|
with open(self.config_file, mode='w') as f:
|
||||||
f.write(config_text)
|
f.write(config_text)
|
||||||
|
|
||||||
@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"""
|
||||||
@@ -1108,22 +1128,22 @@ class JupyterHub(Application):
|
|||||||
|
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
yield self.proxy.check_routes(self.users, routes)
|
yield self.proxy.check_routes(self.users, routes)
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def start(self):
|
def start(self):
|
||||||
"""Start the whole thing"""
|
"""Start the whole thing"""
|
||||||
self.io_loop = loop = IOLoop.current()
|
self.io_loop = loop = IOLoop.current()
|
||||||
|
|
||||||
if self.subapp:
|
if self.subapp:
|
||||||
self.subapp.start()
|
self.subapp.start()
|
||||||
loop.stop()
|
loop.stop()
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.generate_config:
|
if self.generate_config:
|
||||||
self.write_config_file()
|
self.write_config_file()
|
||||||
loop.stop()
|
loop.stop()
|
||||||
return
|
return
|
||||||
|
|
||||||
# start the webserver
|
# start the webserver
|
||||||
self.http_server = tornado.httpserver.HTTPServer(self.tornado_application, xheaders=True)
|
self.http_server = tornado.httpserver.HTTPServer(self.tornado_application, xheaders=True)
|
||||||
try:
|
try:
|
||||||
@@ -1133,7 +1153,7 @@ class JupyterHub(Application):
|
|||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
self.log.info("Hub API listening on %s", self.hub.server.bind_url)
|
self.log.info("Hub API listening on %s", self.hub.server.bind_url)
|
||||||
|
|
||||||
# start the proxy
|
# start the proxy
|
||||||
try:
|
try:
|
||||||
yield self.start_proxy()
|
yield self.start_proxy()
|
||||||
@@ -1141,16 +1161,16 @@ class JupyterHub(Application):
|
|||||||
self.log.critical("Failed to start proxy", exc_info=True)
|
self.log.critical("Failed to start proxy", exc_info=True)
|
||||||
self.exit(1)
|
self.exit(1)
|
||||||
return
|
return
|
||||||
|
|
||||||
loop.add_callback(self.proxy.add_all_users, self.users)
|
loop.add_callback(self.proxy.add_all_users, self.users)
|
||||||
|
|
||||||
if self.proxy_process:
|
if self.proxy_process:
|
||||||
# only check / restart the proxy if we started it in the first place.
|
# only check / restart the proxy if we started it in the first place.
|
||||||
# this means a restarted Hub cannot restart a Proxy that its
|
# this means a restarted Hub cannot restart a Proxy that its
|
||||||
# predecessor started.
|
# predecessor started.
|
||||||
pc = PeriodicCallback(self.check_proxy, 1e3 * self.proxy_check_interval)
|
pc = PeriodicCallback(self.check_proxy, 1e3 * self.proxy_check_interval)
|
||||||
pc.start()
|
pc.start()
|
||||||
|
|
||||||
if self.last_activity_interval:
|
if self.last_activity_interval:
|
||||||
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()
|
||||||
@@ -1162,12 +1182,12 @@ class JupyterHub(Application):
|
|||||||
|
|
||||||
def init_signal(self):
|
def init_signal(self):
|
||||||
signal.signal(signal.SIGTERM, self.sigterm)
|
signal.signal(signal.SIGTERM, self.sigterm)
|
||||||
|
|
||||||
def sigterm(self, signum, frame):
|
def sigterm(self, signum, frame):
|
||||||
self.log.critical("Received SIGTERM, shutting down")
|
self.log.critical("Received SIGTERM, shutting down")
|
||||||
self.io_loop.stop()
|
self.io_loop.stop()
|
||||||
self.atexit()
|
self.atexit()
|
||||||
|
|
||||||
_atexit_ran = False
|
_atexit_ran = False
|
||||||
def atexit(self):
|
def atexit(self):
|
||||||
"""atexit callback"""
|
"""atexit callback"""
|
||||||
@@ -1179,8 +1199,8 @@ class JupyterHub(Application):
|
|||||||
loop = IOLoop()
|
loop = IOLoop()
|
||||||
loop.make_current()
|
loop.make_current()
|
||||||
loop.run_sync(self.cleanup)
|
loop.run_sync(self.cleanup)
|
||||||
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
if not self.io_loop:
|
if not self.io_loop:
|
||||||
return
|
return
|
||||||
@@ -1190,7 +1210,7 @@ class JupyterHub(Application):
|
|||||||
else:
|
else:
|
||||||
self.http_server.stop()
|
self.http_server.stop()
|
||||||
self.io_loop.add_callback(self.io_loop.stop)
|
self.io_loop.add_callback(self.io_loop.stop)
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def launch_instance_async(self, argv=None):
|
def launch_instance_async(self, argv=None):
|
||||||
try:
|
try:
|
||||||
@@ -1199,7 +1219,7 @@ class JupyterHub(Application):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.exception("")
|
self.log.exception("")
|
||||||
self.exit(1)
|
self.exit(1)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def launch_instance(cls, argv=None):
|
def launch_instance(cls, argv=None):
|
||||||
self = cls.instance()
|
self = cls.instance()
|
||||||
|
Reference in New Issue
Block a user