mirror of
https://github.com/jupyterhub/the-littlest-jupyterhub.git
synced 2025-12-18 21:54:05 +08:00
Merge pull request #605 from GeorgianaElena/in-progress-page
Temporary page while tljh is building
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
===================
|
||||
|
||||
|
||||
@@ -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
|
||||
===========
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user