Merge pull request #605 from GeorgianaElena/in-progress-page

Temporary page while tljh is building
This commit is contained in:
Yuvi Panda
2020-09-14 13:37:34 +05:30
committed by GitHub
6 changed files with 334 additions and 38 deletions

View File

@@ -110,7 +110,7 @@ commands:
- run:
name: Run bootstrap checks
command: |
py.test integration-tests/test_bootstrap.py
py.test integration-tests/test_bootstrap.py -s
jobs:
@@ -169,15 +169,14 @@ jobs:
- build_systemd_image
- bootstrap_checks
- basic_tests
- admin_tests
- plugin_tests
- bootstrap_checks
upgrade-test:
docker:
- image: docker:18.05.0-ce-git

View File

@@ -13,10 +13,94 @@ Constraints:
- Use stdlib modules only
"""
import os
from http.server import SimpleHTTPRequestHandler, HTTPServer
import multiprocessing
import subprocess
import sys
import logging
import shutil
import urllib.request
html = """
<html>
<head>
<title>The Littlest Jupyterhub</title>
</head>
<body>
<meta http-equiv="refresh" content="30" >
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width">
<img class="logo" src="https://raw.githubusercontent.com/jupyterhub/the-littlest-jupyterhub/master/docs/images/logo/logo.png">
<div class="loader center"></div>
<div class="center main-msg">Please wait while your TLJH is building...</div>
<div class="center logs-msg">Click the button below to see the logs</div>
<div class="center tip" >Tip: to update the logs, refresh the page</div>
<button class="logs-button center" onclick="window.location.href='/logs'">View logs</button>
</body>
<style>
button:hover {
background: grey;
}
.logo {
width: 150px;
height: auto;
}
.center {
margin: 0 auto;
margin-top: 50px;
text-align:center;
display: block;
}
.main-msg {
font-size: 30px;
font-weight: bold;
color: grey;
text-align:center;
}
.logs-msg {
font-size: 15px;
color: grey;
}
.tip {
font-size: 13px;
color: grey;
margin-top: 10px;
font-style: italic;
}
.logs-button {
margin-top:15px;
border: 0;
color: white;
padding: 15px 32px;
font-size: 16px;
cursor: pointer;
background: #f5a252;
}
.loader {
width: 150px;
height: 150px;
border-radius: 90%;
border: 7px solid transparent;
animation: spin 2s infinite ease;
animation-direction: alternate;
}
@keyframes spin {
0% {
transform: rotateZ(0deg);
border-top-color: #f17c0e
}
100% {
transform: rotateZ(360deg);
border-top-color: #fce5cf;
}
}
</style>
</head>
<html>
"""
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()

View File

@@ -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://<tljh-public-ip>/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://<tljh-public-ip>``.
* 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://<tljh-public-ip>/logs`` in your browser.
To update the logs, refresh the page.
.. note::
The ``http://<tljh-public-ip>/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://<tljh-public-ip>/`` 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
===================

View File

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

View File

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

View File

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