mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-16 06:22:59 +00:00
add basic CLI and config file support
See `jupyterhub -h` for common shortcuts default config file: `jupyter_hub_config.py` generate config file with: `jupyterhub --generate-config` non-default config file: `jupyterhub -f myconfig.py`
This commit is contained in:
@@ -66,4 +66,13 @@ and then visit `http://localhost:8000`, and sign in with your unix credentials.
|
|||||||
If you want multiple users to be able to sign into the server, you will need to run the
|
If you want multiple users to be able to sign into the server, you will need to run the
|
||||||
`jupyterhub` command as a privileged user, such as root.
|
`jupyterhub` command as a privileged user, such as root.
|
||||||
|
|
||||||
|
### Some examples
|
||||||
|
|
||||||
|
generate a default config file:
|
||||||
|
|
||||||
|
jupyterhub --generate-config
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
@@ -4,11 +4,17 @@
|
|||||||
# Copyright (c) IPython Development Team.
|
# Copyright (c) IPython Development Team.
|
||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
|
import io
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw_input
|
||||||
|
except NameError:
|
||||||
|
# py3
|
||||||
|
raw_input = input
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
import tornado.httpserver
|
import tornado.httpserver
|
||||||
@@ -32,15 +38,77 @@ from . import orm
|
|||||||
from ._data import DATA_FILES_PATH
|
from ._data import DATA_FILES_PATH
|
||||||
from .utils import url_path_join
|
from .utils import url_path_join
|
||||||
|
|
||||||
|
# classes for config
|
||||||
|
from .auth import Authenticator, PAMAuthenticator
|
||||||
|
from .spawner import Spawner, LocalProcessSpawner
|
||||||
|
|
||||||
|
aliases = {
|
||||||
|
'log-level': 'Application.log_level',
|
||||||
|
'f': 'JupyterHubApp.config_file',
|
||||||
|
'config': 'JupyterHubApp.config_file',
|
||||||
|
'y': 'JupyterHubApp.answer_yes',
|
||||||
|
'ssl-key': 'JupyterHubApp.ssl_key',
|
||||||
|
'ssl-cert': 'JupyterHubApp.ssl_cert',
|
||||||
|
'ip': 'JupyterHubApp.ip',
|
||||||
|
'port': 'JupyterHubApp.port',
|
||||||
|
'db': 'JupyterHubApp.db_url',
|
||||||
|
'pid-file': 'JupyterHubApp.pid_file',
|
||||||
|
}
|
||||||
|
|
||||||
|
flags = {
|
||||||
|
'debug': ({'Application' : {'log_level' : logging.DEBUG}},
|
||||||
|
"set log level to logging.DEBUG (maximize logging output)"),
|
||||||
|
'generate-config': ({'JupyterHubApp': {'generate_config' : True}},
|
||||||
|
"generate default config file")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class JupyterHubApp(Application):
|
class JupyterHubApp(Application):
|
||||||
"""An Application for starting a Multi-User Jupyter Notebook server."""
|
"""An Application for starting a Multi-User Jupyter Notebook server."""
|
||||||
|
|
||||||
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 = """
|
||||||
|
|
||||||
|
generate default config file:
|
||||||
|
|
||||||
|
jupyterhub --generate-config -f myconfig.py
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
|
aliases = Dict(aliases)
|
||||||
|
flags = Dict(flags)
|
||||||
|
|
||||||
|
classes = List([
|
||||||
|
Spawner,
|
||||||
|
LocalProcessSpawner,
|
||||||
|
Authenticator,
|
||||||
|
PAMAuthenticator,
|
||||||
|
])
|
||||||
|
|
||||||
|
config_file = Unicode('jupyter_hub_config.py', config=True,
|
||||||
|
help="The config file to load",
|
||||||
|
)
|
||||||
|
generate_config = Bool(False, config=True,
|
||||||
|
help="Generate default config file",
|
||||||
|
)
|
||||||
|
answer_yes = Bool(False, config=True,
|
||||||
|
help="Answer yes to any questions (e.g. confirm overwrite)"
|
||||||
|
)
|
||||||
|
pid_file = Unicode('', config=True,
|
||||||
|
help="""File to write PID
|
||||||
|
Useful for daemonizing jupyterhub.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
data_files_path = Unicode(DATA_FILES_PATH, config=True,
|
data_files_path = Unicode(DATA_FILES_PATH, config=True,
|
||||||
help="The location of jupyter data files (e.g. /usr/local/share/jupyter)"
|
help="The location of jupyter data files (e.g. /usr/local/share/jupyter)"
|
||||||
)
|
)
|
||||||
@@ -279,7 +347,7 @@ class JupyterHubApp(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])
|
||||||
|
self.log.info("Starting proxy: %s", cmd)
|
||||||
self.proxy_process = Popen(cmd, env=env)
|
self.proxy_process = Popen(cmd, env=env)
|
||||||
|
|
||||||
def init_tornado_settings(self):
|
def init_tornado_settings(self):
|
||||||
@@ -315,9 +383,20 @@ class JupyterHubApp(Application):
|
|||||||
"""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):
|
||||||
|
pid = os.getpid()
|
||||||
|
if self.pid_file:
|
||||||
|
self.log.debug("Writing PID %i to %s", pid, self.pid_file)
|
||||||
|
with io.open(self.pid_file, 'w') as f:
|
||||||
|
f.write(u'%i' % pid)
|
||||||
|
|
||||||
@catch_config_error
|
@catch_config_error
|
||||||
def initialize(self, *args, **kwargs):
|
def initialize(self, *args, **kwargs):
|
||||||
super(JupyterHubApp, self).initialize(*args, **kwargs)
|
super(JupyterHubApp, self).initialize(*args, **kwargs)
|
||||||
|
if self.generate_config:
|
||||||
|
return
|
||||||
|
self.load_config_file(self.config_file)
|
||||||
|
self.write_pid_file()
|
||||||
self.init_logging()
|
self.init_logging()
|
||||||
self.init_ports()
|
self.init_ports()
|
||||||
self.init_db()
|
self.init_db()
|
||||||
@@ -329,25 +408,56 @@ class JupyterHubApp(Application):
|
|||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
self.log.info("Cleaning up proxy...")
|
"""Shutdown our various subprocesses and cleanup runtime files."""
|
||||||
self.proxy_process.terminate()
|
|
||||||
self.log.info("Cleaning up single-user servers...")
|
self.log.info("Cleaning up single-user servers...")
|
||||||
|
|
||||||
# request (async) process termination
|
# request (async) process termination
|
||||||
futures = []
|
futures = []
|
||||||
for user in self.db.query(orm.User):
|
for user in self.db.query(orm.User):
|
||||||
if user.spawner is not None:
|
if user.spawner is not None:
|
||||||
futures.append(user.spawner.stop())
|
futures.append(user.spawner.stop())
|
||||||
|
|
||||||
|
# clean up proxy while SUS are shutting down
|
||||||
|
self.log.info("Cleaning up proxy[%i]..." % self.proxy_process.pid)
|
||||||
|
self.proxy_process.terminate()
|
||||||
|
|
||||||
# wait for the requests to stop finish:
|
# wait for the requests to stop finish:
|
||||||
for f in futures:
|
for f in futures:
|
||||||
yield f
|
yield f
|
||||||
|
|
||||||
|
if self.pid_file and os.path.exists(self.pid_file):
|
||||||
|
self.log.info("Cleaning up PID file %s", 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):
|
||||||
|
if os.path.exists(self.config_file) and not self.answer_yes:
|
||||||
|
answer = ''
|
||||||
|
def ask():
|
||||||
|
prompt = "Overwrite %s with default config? [y/N]" % self.config_file
|
||||||
|
try:
|
||||||
|
return raw_input(prompt).lower() or 'n'
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print('') # empty line
|
||||||
|
return 'n'
|
||||||
|
answer = ask()
|
||||||
|
while not answer.startswith(('y', 'n')):
|
||||||
|
print("Please answer 'yes' or 'no'")
|
||||||
|
answer = ask()
|
||||||
|
if answer.startswith('n'):
|
||||||
|
return
|
||||||
|
|
||||||
|
config_text = self.generate_config_file()
|
||||||
|
print("Writing default config to: %s" % self.config_file)
|
||||||
|
with io.open(self.config_file, encoding='utf8', mode='w') as f:
|
||||||
|
f.write(config_text)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
"""Start the whole thing"""
|
"""Start the whole thing"""
|
||||||
|
if self.generate_config:
|
||||||
|
self.write_config_file()
|
||||||
|
return
|
||||||
# start the proxy
|
# start the proxy
|
||||||
self.start_proxy()
|
self.start_proxy()
|
||||||
# start the webserver
|
# start the webserver
|
||||||
|
@@ -49,11 +49,9 @@ class Spawner(LoggingConfigurable):
|
|||||||
self._env_key(env, 'API_TOKEN', self.api_token)
|
self._env_key(env, 'API_TOKEN', self.api_token)
|
||||||
return env
|
return env
|
||||||
|
|
||||||
cmd = List(Unicode, config=True,
|
cmd = List(Unicode, default_value=['jupyterhub-singleuser'], config=True,
|
||||||
help="""The command used for starting notebooks."""
|
help="""The command used for starting notebooks."""
|
||||||
)
|
)
|
||||||
def _cmd_default(self):
|
|
||||||
return ['jupyterhub-singleuser']
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fromJSON(cls, state, **kwargs):
|
def fromJSON(cls, state, **kwargs):
|
||||||
@@ -183,8 +181,8 @@ class LocalProcessSpawner(Spawner):
|
|||||||
set_user = Enum(['sudo', 'setuid'], default_value='setuid', config=True,
|
set_user = Enum(['sudo', 'setuid'], default_value='setuid', config=True,
|
||||||
help="""scheme for setting the user of the spawned process
|
help="""scheme for setting the user of the spawned process
|
||||||
|
|
||||||
sudo can be more prudently restricted,
|
'sudo' can be more prudently restricted,
|
||||||
but setuid is simpler for a server run as root
|
but 'setuid' is simpler for a server run as root
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
def _set_user_changed(self, name, old, new):
|
def _set_user_changed(self, name, old, new):
|
||||||
|
11
jupyterhub/tests/test_app.py
Normal file
11
jupyterhub/tests/test_app.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"""Test the JupyterHubApp entry point"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from subprocess import check_output
|
||||||
|
|
||||||
|
def test_help_all():
|
||||||
|
out = check_output([sys.executable, '-m', 'jupyterhub', '--help-all']).decode('utf8', 'replace')
|
||||||
|
assert u'--ip' in out
|
||||||
|
assert u'--JupyterHubApp.ip' in out
|
||||||
|
|
||||||
|
|
Reference in New Issue
Block a user