+
+
+
+
+
+
+
+"""
logger = logging.getLogger(__name__)
@@ -90,7 +174,65 @@ def validate_host():
print("For local development, see http://tljh.jupyter.org/en/latest/contributing/dev-setup.html")
sys.exit(1)
+class LoaderPageRequestHandler(SimpleHTTPRequestHandler):
+ def do_GET(self):
+ if self.path == "/logs":
+ with open("/opt/tljh/installer.log", "r") as log_file:
+ logs = log_file.read()
+
+ self.send_response(200)
+ self.send_header('Content-Type', 'text/plain; charset=utf-8')
+ self.end_headers()
+ self.wfile.write(logs.encode('utf-8'))
+ elif self.path == "/index.html":
+ self.path = "/var/run/index.html"
+ return SimpleHTTPRequestHandler.do_GET(self)
+ elif self.path == "/favicon.ico":
+ self.path = "/var/run/favicon.ico"
+ return SimpleHTTPRequestHandler.do_GET(self)
+ elif self.path == "/":
+ self.send_response(302)
+ self.send_header('Location','/index.html')
+ self.end_headers()
+ else:
+ SimpleHTTPRequestHandler.send_error(self, code=403)
+
+def serve_forever(server):
+ try:
+ server.serve_forever()
+ except KeyboardInterrupt:
+ pass
+
def main():
+ flags = sys.argv[1:]
+ temp_page_flag = "--show-progress-page"
+
+ # Check for flag in the argv list. This doesn't use argparse
+ # because it's the only argument that's meant for the boostrap script.
+ # All the other flags will be passed to and parsed by the installer.
+ if temp_page_flag in flags:
+ with open("/var/run/index.html", "w+") as f:
+ f.write(html)
+ favicon_url="https://raw.githubusercontent.com/jupyterhub/jupyterhub/master/share/jupyterhub/static/favicon.ico"
+ urllib.request.urlretrieve(favicon_url, "/var/run/favicon.ico")
+
+ # If the bootstrap is run to upgrade TLJH, then this will raise an "Address already in use" error
+ try:
+ loading_page_server = HTTPServer(("", 80), LoaderPageRequestHandler)
+ p = multiprocessing.Process(target=serve_forever, args=(loading_page_server,))
+ # Serves the loading page until TLJH builds
+ p.start()
+
+ # Remove the flag from the args list, since it was only relevant to this script.
+ flags.remove("--show-progress-page")
+
+ # Pass the server's pid as a flag to the istaller
+ pid_flag = "--progress-page-server-pid"
+ flags.extend([pid_flag, str(p.pid)])
+ except OSError:
+ # Only serve the loading page when installing TLJH
+ pass
+
validate_host()
install_prefix = os.environ.get('TLJH_INSTALL_PREFIX', '/opt/tljh')
hub_prefix = os.path.join(install_prefix, 'hub')
@@ -175,9 +317,8 @@ def main():
os.path.join(hub_prefix, 'bin', 'python3'),
'-m',
'tljh.installer',
- ] + sys.argv[1:]
+ ] + flags
)
-
if __name__ == '__main__':
main()
diff --git a/docs/topic/customizing-installer.rst b/docs/topic/customizing-installer.rst
index dd45d2b..b18febc 100644
--- a/docs/topic/customizing-installer.rst
+++ b/docs/topic/customizing-installer.rst
@@ -17,6 +17,35 @@ This page documents the various options you can pass as commandline parameters t
.. _topic/customizing-installer/admin:
+Serving a temporary "TLJH is building" page
+===========================================
+``--show-progress-page`` serves a temporary "TLJH is building" progress page while TLJH is building.
+
+.. image:: ../images/tljh-is-building-page.gif
+ :alt: Temporary progress page while TLJH is building
+
+* The page will be accessible at ``http:///index.html`` in your browser.
+ When TLJH installation is complete, the progress page page will stop and you will be able
+ to access TLJH as usually at ``http://``.
+* From the progress page, you will also be able to access the installation logs, by clicking the
+ **Logs** button or by going directly to ``http:///logs`` in your browser.
+ To update the logs, refresh the page.
+
+.. note::
+
+ The ``http:///index.html`` page refreshes itself automatically every 30s.
+ When JupyterHub starts, a JupyterHub 404 HTTP error message (*Jupyter has lots of moons, but this is not one...*)
+ will be shown instead of the progress page. This means JupyterHub was started succesfully and you can access it
+ either by clicking the `Control Panel` button or by going to ``http:///`` directly.
+
+For example, to enable the progress page and add the first *admin* user, you would run:
+
+.. code-block:: bash
+
+ curl -L https://tljh.jupyter.org/bootstrap.py \
+ | sudo python3 - \
+ --admin admin --showprogress-page
+
Adding admin users
===================
diff --git a/docs/topic/installer-actions.rst b/docs/topic/installer-actions.rst
index e5991a2..e655f66 100644
--- a/docs/topic/installer-actions.rst
+++ b/docs/topic/installer-actions.rst
@@ -139,6 +139,24 @@ to be used and modified solely by these services.
sudo rm -rf /opt/tljh/state
+Progress page files
+===================
+
+If you ran the TLJH installer with the `--show-progress-page` flag, then two files have been
+added to your system to help serving the progress page:
+
+* ``/var/run/index.html`` - the main progress page
+* ``/var/run/favicon.ico`` - the JupyterHub icon
+
+.. note::
+ If you try to remove TLJH, revert this action using:
+
+ .. code-block:: bash
+
+ sudo rm /var/run/index.html
+ sudo rm /var/run/favicon.ico
+
+
User groups
===========
diff --git a/integration-tests/test_bootstrap.py b/integration-tests/test_bootstrap.py
index fa02b68..4d3aea5 100644
--- a/integration-tests/test_bootstrap.py
+++ b/integration-tests/test_bootstrap.py
@@ -1,56 +1,148 @@
"""
Test running bootstrap script in different circumstances
"""
+import concurrent.futures
+import os
import subprocess
from textwrap import dedent
+import time
-def run_bootstrap(container_name, image):
- # stop container if it is already running
- subprocess.run([
- 'docker', 'rm', '-f', container_name
- ])
-
- # Start a detached Ubuntu 16.04 container
- subprocess.check_call([
- 'docker', 'run', '--detach', '--name', container_name, image,
- '/bin/bash', '-c', 'sleep 1000s'
- ])
+
+def install_pkgs(container_name, show_progress_page):
# Install python3 inside the ubuntu container
# There is no trusted Ubuntu+Python3 container we can use
- subprocess.check_output([
- 'docker', 'exec', container_name, 'apt-get', 'update'
- ])
- subprocess.check_output([
- 'docker', 'exec', container_name, 'apt-get', 'install', '--yes', 'python3'
- ])
- # Copy only the bootstrap script to container, so this is faster
- subprocess.check_call([
- 'docker',
- 'cp',
- 'bootstrap/', f'{container_name}:/srv'
- ])
+ pkgs = ["python3"]
+ if show_progress_page:
+ pkgs += ["systemd", "git", "curl"]
+ # Create the sudoers dir, so that the installer succesfully gets to the
+ # point of starting jupyterhub and stopping the progress page server.
+ subprocess.check_output(
+ ["docker", "exec", container_name, "mkdir", "-p", "etc/sudoers.d"]
+ )
+
+ subprocess.check_output(["docker", "exec", container_name, "apt-get", "update"])
+ subprocess.check_output(
+ ["docker", "exec", container_name, "apt-get", "install", "--yes"] + pkgs
+ )
+
+
+def get_bootstrap_script_location(container_name, show_progress_page):
+ # Copy only the bootstrap script to container when progress page not enabled, to be faster
+ source_path = "bootstrap/"
+ bootstrap_script = "/srv/src/bootstrap.py"
+ if show_progress_page:
+ source_path = os.path.abspath(
+ os.path.join(os.path.dirname(__file__), os.pardir)
+ )
+ bootstrap_script = "/srv/src/bootstrap/bootstrap.py"
+
+ subprocess.check_call(["docker", "cp", source_path, f"{container_name}:/srv/src"])
+ return bootstrap_script
+
+
+def run_bootstrap(container_name, image, show_progress_page=False):
+ # stop container if it is already running
+ subprocess.run(["docker", "rm", "-f", container_name])
+
+ # Start a detached container
+ subprocess.check_call(
+ [
+ "docker",
+ "run",
+ "--detach",
+ "--name",
+ container_name,
+ image,
+ "/bin/bash",
+ "-c",
+ "sleep 1000s",
+ ]
+ )
+
+ install_pkgs(container_name, show_progress_page)
+
+ bootstrap_script = get_bootstrap_script_location(container_name, show_progress_page)
+
+ exec_flags = ["-i", container_name, "python3", bootstrap_script]
+ if show_progress_page:
+ exec_flags = (
+ ["-e", "TLJH_BOOTSTRAP_DEV=yes", "-e", "TLJH_BOOTSTRAP_PIP_SPEC=/srv/src"]
+ + exec_flags
+ + ["--show-progress-page"]
+ )
# Run bootstrap script, return the output
- return subprocess.run([
- 'docker', 'exec', '-i', container_name,
- 'python3', '/srv/bootstrap/bootstrap.py'
- ], check=False, stdout=subprocess.PIPE, encoding='utf-8')
+ return subprocess.run(
+ ["docker", "exec"] + exec_flags,
+ check=False,
+ stdout=subprocess.PIPE,
+ encoding="utf-8",
+ )
+
def test_ubuntu_too_old():
"""
Error with a useful message when running in older Ubuntu
"""
- output = run_bootstrap('old-distro-test', 'ubuntu:16.04')
- assert output.stdout == 'The Littlest JupyterHub requires Ubuntu 18.04 or higher\n'
+ output = run_bootstrap("old-distro-test", "ubuntu:16.04")
+ assert output.stdout == "The Littlest JupyterHub requires Ubuntu 18.04 or higher\n"
assert output.returncode == 1
def test_inside_no_systemd_docker():
- output = run_bootstrap('plain-docker-test', 'ubuntu:18.04')
- assert output.stdout.strip() == dedent("""
+ output = run_bootstrap("plain-docker-test", "ubuntu:18.04")
+ assert (
+ output.stdout.strip()
+ == dedent(
+ """
Systemd is required to run TLJH
Running inside a docker container without systemd isn't supported
We recommend against running a production TLJH instance inside a docker container
For local development, see http://tljh.jupyter.org/en/latest/contributing/dev-setup.html
- """).strip()
+ """
+ ).strip()
+ )
assert output.returncode == 1
+
+
+def verify_progress_page(expected_status_code, timeout):
+ progress_page_status = False
+ start = time.time()
+ while not progress_page_status and (time.time() - start < timeout):
+ try:
+ resp = subprocess.check_output(
+ [
+ "docker",
+ "exec",
+ "progress-page",
+ "curl",
+ "-i",
+ "http://localhost/index.html",
+ ]
+ )
+ if b"HTTP/1.0 200 OK" in resp:
+ progress_page_status = True
+ break
+ except Exception as e:
+ time.sleep(2)
+ continue
+
+ return progress_page_status
+
+
+def test_progress_page():
+ with concurrent.futures.ThreadPoolExecutor() as executor:
+ installer = executor.submit(
+ run_bootstrap, "progress-page", "ubuntu:18.04", True
+ )
+
+ # Check if progress page started
+ started = verify_progress_page(expected_status_code=200, timeout=120)
+ assert started
+
+ # This will fail start tljh but should successfully get to the point
+ # Where it stops the progress page server.
+ output = installer.result()
+
+ # Check if progress page stopped
+ assert "Progress page server stopped successfully." in output.stdout
diff --git a/tljh/installer.py b/tljh/installer.py
index 3b9e0fb..de56be5 100644
--- a/tljh/installer.py
+++ b/tljh/installer.py
@@ -6,6 +6,7 @@ import itertools
import logging
import os
import secrets
+import signal
import subprocess
import sys
import time
@@ -485,6 +486,11 @@ def main():
nargs='*',
help='Plugin pip-specs to install'
)
+ argparser.add_argument(
+ '--progress-page-server-pid',
+ type=int,
+ help='The pid of the progress page server'
+ )
args = argparser.parse_args()
@@ -499,6 +505,17 @@ def main():
ensure_node()
ensure_jupyterhub_package(HUB_ENV_PREFIX)
ensure_jupyterlab_extensions()
+
+ # Stop the http server with the progress page before traefik starts
+ if args.progress_page_server_pid:
+ try:
+ os.kill(args.progress_page_server_pid, signal.SIGINT)
+ # Log and print the message to make testing easier
+ print("Progress page server stopped successfully.")
+ except Exception as e:
+ logger.error(f"Couldn't stop the progress page server. Exception was {e}.")
+ pass
+
ensure_jupyterhub_service(HUB_ENV_PREFIX)
ensure_jupyterhub_running()
ensure_symlinks(HUB_ENV_PREFIX)