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:
|
- 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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
===================
|
===================
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
subprocess.run([
|
|
||||||
'docker', 'rm', '-f', container_name
|
|
||||||
])
|
|
||||||
|
|
||||||
# Start a detached Ubuntu 16.04 container
|
def install_pkgs(container_name, show_progress_page):
|
||||||
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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user