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

View File

@@ -13,10 +13,94 @@ Constraints:
- Use stdlib modules only - Use stdlib modules only
""" """
import os import os
from http.server import SimpleHTTPRequestHandler, HTTPServer
import multiprocessing
import subprocess import subprocess
import sys import sys
import logging import logging
import shutil 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__) 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") print("For local development, see http://tljh.jupyter.org/en/latest/contributing/dev-setup.html")
sys.exit(1) 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(): 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() validate_host()
install_prefix = os.environ.get('TLJH_INSTALL_PREFIX', '/opt/tljh') install_prefix = os.environ.get('TLJH_INSTALL_PREFIX', '/opt/tljh')
hub_prefix = os.path.join(install_prefix, 'hub') hub_prefix = os.path.join(install_prefix, 'hub')
@@ -175,9 +317,8 @@ def main():
os.path.join(hub_prefix, 'bin', 'python3'), os.path.join(hub_prefix, 'bin', 'python3'),
'-m', '-m',
'tljh.installer', 'tljh.installer',
] + sys.argv[1:] ] + flags
) )
if __name__ == '__main__': if __name__ == '__main__':
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: .. _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 Adding admin users
=================== ===================

View File

@@ -139,6 +139,24 @@ to be used and modified solely by these services.
sudo rm -rf /opt/tljh/state 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 User groups
=========== ===========

View File

@@ -1,56 +1,148 @@
""" """
Test running bootstrap script in different circumstances Test running bootstrap script in different circumstances
""" """
import concurrent.futures
import os
import subprocess import subprocess
from textwrap import dedent from textwrap import dedent
import time
def run_bootstrap(container_name, image):
# stop container if it is already running def install_pkgs(container_name, show_progress_page):
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'
])
# Install python3 inside the ubuntu container # Install python3 inside the ubuntu container
# There is no trusted Ubuntu+Python3 container we can use # There is no trusted Ubuntu+Python3 container we can use
subprocess.check_output([ pkgs = ["python3"]
'docker', 'exec', container_name, 'apt-get', 'update' if show_progress_page:
]) pkgs += ["systemd", "git", "curl"]
subprocess.check_output([ # Create the sudoers dir, so that the installer succesfully gets to the
'docker', 'exec', container_name, 'apt-get', 'install', '--yes', 'python3' # point of starting jupyterhub and stopping the progress page server.
]) subprocess.check_output(
# Copy only the bootstrap script to container, so this is faster ["docker", "exec", container_name, "mkdir", "-p", "etc/sudoers.d"]
subprocess.check_call([ )
'docker',
'cp', subprocess.check_output(["docker", "exec", container_name, "apt-get", "update"])
'bootstrap/', f'{container_name}:/srv' 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 # Run bootstrap script, return the output
return subprocess.run([ return subprocess.run(
'docker', 'exec', '-i', container_name, ["docker", "exec"] + exec_flags,
'python3', '/srv/bootstrap/bootstrap.py' check=False,
], check=False, stdout=subprocess.PIPE, encoding='utf-8') stdout=subprocess.PIPE,
encoding="utf-8",
)
def test_ubuntu_too_old(): def test_ubuntu_too_old():
""" """
Error with a useful message when running in older Ubuntu Error with a useful message when running in older Ubuntu
""" """
output = run_bootstrap('old-distro-test', 'ubuntu:16.04') output = run_bootstrap("old-distro-test", "ubuntu:16.04")
assert output.stdout == 'The Littlest JupyterHub requires Ubuntu 18.04 or higher\n' assert output.stdout == "The Littlest JupyterHub requires Ubuntu 18.04 or higher\n"
assert output.returncode == 1 assert output.returncode == 1
def test_inside_no_systemd_docker(): def test_inside_no_systemd_docker():
output = run_bootstrap('plain-docker-test', 'ubuntu:18.04') output = run_bootstrap("plain-docker-test", "ubuntu:18.04")
assert output.stdout.strip() == dedent(""" assert (
output.stdout.strip()
== dedent(
"""
Systemd is required to run TLJH Systemd is required to run TLJH
Running inside a docker container without systemd isn't supported Running inside a docker container without systemd isn't supported
We recommend against running a production TLJH instance inside a docker container 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 For local development, see http://tljh.jupyter.org/en/latest/contributing/dev-setup.html
""").strip() """
).strip()
)
assert output.returncode == 1 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 logging
import os import os
import secrets import secrets
import signal
import subprocess import subprocess
import sys import sys
import time import time
@@ -485,6 +486,11 @@ def main():
nargs='*', nargs='*',
help='Plugin pip-specs to install' 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() args = argparser.parse_args()
@@ -499,6 +505,17 @@ def main():
ensure_node() ensure_node()
ensure_jupyterhub_package(HUB_ENV_PREFIX) ensure_jupyterhub_package(HUB_ENV_PREFIX)
ensure_jupyterlab_extensions() 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_service(HUB_ENV_PREFIX)
ensure_jupyterhub_running() ensure_jupyterhub_running()
ensure_symlinks(HUB_ENV_PREFIX) ensure_symlinks(HUB_ENV_PREFIX)