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:
MinRK
2014-09-16 14:47:48 -07:00
parent e7f67bfc4f
commit 83e9762845
4 changed files with 139 additions and 11 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View 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