diff --git a/README.md b/README.md index a7f4fd83..c2a9f9e4 100644 --- a/README.md +++ b/README.md @@ -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 `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 diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 4bb0c437..50fc0135 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -4,11 +4,17 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. - +import io import logging import os from subprocess import Popen +try: + raw_input +except NameError: + # py3 + raw_input = input + from jinja2 import Environment, FileSystemLoader import tornado.httpserver @@ -32,15 +38,77 @@ from . import orm from ._data import DATA_FILES_PATH 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): """An Application for starting 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 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, 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]) if self.ssl_cert: cmd.extend(['--ssl-cert', self.ssl_cert]) - + self.log.info("Starting proxy: %s", cmd) self.proxy_process = Popen(cmd, env=env) def init_tornado_settings(self): @@ -315,9 +383,20 @@ class JupyterHubApp(Application): """Instantiate the tornado Application object""" 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 def initialize(self, *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_ports() self.init_db() @@ -329,25 +408,56 @@ class JupyterHubApp(Application): @gen.coroutine def cleanup(self): - self.log.info("Cleaning up proxy...") - self.proxy_process.terminate() + """Shutdown our various subprocesses and cleanup runtime files.""" self.log.info("Cleaning up single-user servers...") - # request (async) process termination futures = [] for user in self.db.query(orm.User): if user.spawner is not None: 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: for f in futures: 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 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): """Start the whole thing""" + if self.generate_config: + self.write_config_file() + return # start the proxy self.start_proxy() # start the webserver diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index ad1dd1be..9e917406 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -49,11 +49,9 @@ class Spawner(LoggingConfigurable): self._env_key(env, 'API_TOKEN', self.api_token) return env - cmd = List(Unicode, config=True, + cmd = List(Unicode, default_value=['jupyterhub-singleuser'], config=True, help="""The command used for starting notebooks.""" ) - def _cmd_default(self): - return ['jupyterhub-singleuser'] @classmethod def fromJSON(cls, state, **kwargs): @@ -183,8 +181,8 @@ class LocalProcessSpawner(Spawner): set_user = Enum(['sudo', 'setuid'], default_value='setuid', config=True, help="""scheme for setting the user of the spawned process - sudo can be more prudently restricted, - but setuid is simpler for a server run as root + 'sudo' can be more prudently restricted, + but 'setuid' is simpler for a server run as root """ ) def _set_user_changed(self, name, old, new): diff --git a/jupyterhub/tests/test_app.py b/jupyterhub/tests/test_app.py new file mode 100644 index 00000000..5332e9e4 --- /dev/null +++ b/jupyterhub/tests/test_app.py @@ -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 + + \ No newline at end of file