Browse Source

Add status server (#812)

Co-authored-by: Mayank <mayankchhabra9@gmail.com>
Co-authored-by: Aaron Dewes <aaron.dewes@web.de>
Co-authored-by: Lounès Ksouri <dev@louneskmt.com>
sphinx-v2.1.5
Luke Childs 3 years ago
committed by GitHub
parent
commit
2fb71bedb7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .gitignore
  2. 7
      scripts/start
  3. 3
      scripts/umbrel-os/external-storage/monitor
  4. 7
      scripts/umbrel-os/external-storage/mount
  5. 6
      scripts/umbrel-os/external-storage/update-from-sdcard
  6. 25
      scripts/umbrel-os/services/umbrel-status-server-iptables-update.service
  7. 21
      scripts/umbrel-os/services/umbrel-status-server.service
  8. 31
      scripts/umbrel-os/status-server/set-status
  9. 24
      scripts/umbrel-os/status-server/setup
  10. 33
      scripts/umbrel-os/status-server/setup-iptables
  11. BIN
      scripts/umbrel-os/status-server/static/favicon.png
  12. 114
      scripts/umbrel-os/status-server/static/index.html
  13. 117
      scripts/umbrel-os/status-server/static/script.js
  14. 117
      scripts/umbrel-os/status-server/static/styles.css
  15. 32
      scripts/umbrel-os/status-server/status-server
  16. 9
      scripts/umbrel-os/status-server/util/csrf.py
  17. 88
      scripts/umbrel-os/status-server/util/server.py
  18. 54
      scripts/umbrel-os/status-server/util/status_file.py

4
.gitignore

@ -7,6 +7,10 @@
.ssh
.viminfo
# Python bytecode
__pycache__
*.py[cod]
# umbrel-dev
docker-compose.override.yml

7
scripts/start

@ -31,8 +31,13 @@ check_dependencies rsync jq curl
UMBREL_ROOT="$(dirname $(readlink -f "${BASH_SOURCE[0]}"))/.."
UMBREL_LOGS="${UMBREL_ROOT}/logs"
set_status="${UMBREL_ROOT}/scripts/umbrel-os/status-server/set-status"
$set_status umbrel started
if [[ ! -d "$UMBREL_ROOT" ]]; then
echo "Root dir does not exist '$UMBREL_ROOT'"
$set_status umbrel errored umbrel-root-missing
exit 1
fi
@ -109,3 +114,5 @@ echo " http://${DEVICE_IP}"
if [[ ! -z "${hidden_service_url:-}" ]]; then
echo " http://${hidden_service_url}"
fi
$set_status umbrel completed

3
scripts/umbrel-os/external-storage/monitor

@ -6,6 +6,8 @@ UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../../..)"
block_device="${1}"
mount_point="${2}"
set_status="/sd-root/${UMBREL_ROOT}/scripts/umbrel-os/status-server/set-status"
check_if_not_already_running() {
if ps ax | grep $0 | grep -v $$ | grep bash | grep -v grep
then
@ -42,6 +44,7 @@ main () {
done
echo "Stopping Umbrel due to failed storage device check..."
$set_status mount errored monitor-check
docker kill $(docker ps -aq)
}

7
scripts/umbrel-os/external-storage/mount

@ -21,6 +21,8 @@ EXTERNAL_DOCKER_DIR="${MOUNT_POINT}/docker"
SWAP_DIR="/swap"
SWAP_FILE="${SWAP_DIR}/swapfile"
set_status="${UMBREL_ROOT}/scripts/umbrel-os/status-server/set-status"
check_root () {
if [[ $UID != 0 ]]; then
echo "This script must be run as root"
@ -143,6 +145,7 @@ copy_docker_to_external_storage () {
}
main () {
$set_status mount started
echo "Running external storage mount script..."
check_root
check_dependencies sed wipefs parted mount sync umount
@ -164,6 +167,7 @@ main () {
if [[ $retry_for_block_devices -gt 20 ]]; then
echo "No block devices found in 20 tries..."
echo "Exiting mount script without doing anything"
$set_status mount errored no-block-device
exit 1
fi
@ -172,6 +176,7 @@ main () {
if [[ $no_of_block_devices -gt 1 ]]; then
echo "Multiple block devices found, only one drive is supported"
echo "Exiting mount script without doing anything"
$set_status mount errored multiple-block-devices
exit 1
fi
@ -192,6 +197,7 @@ main () {
if [[ $retry_for_usb_devices -gt 10 ]]; then
echo "USB devices weren't registered after 10 tries..."
echo "Exiting mount script without doing anything"
$set_status mount errored rebinding-failed
exit 1
fi
@ -266,6 +272,7 @@ main () {
${UMBREL_ROOT}/scripts/umbrel-os/external-storage/monitor ${block_device} ${MOUNT_POINT} &
echo "Mount script completed successfully!"
$set_status mount completed
}
main

6
scripts/umbrel-os/external-storage/update-from-sdcard

@ -6,6 +6,8 @@ UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../../..)"
SD_MOUNT_POINT="/sd-root"
SD_UMBREL_ROOT="${SD_MOUNT_POINT}${UMBREL_ROOT}"
set_status="${UMBREL_ROOT}/scripts/umbrel-os/status-server/set-status"
check_root () {
if [[ $UID != 0 ]]; then
echo "This script must be run as root"
@ -31,12 +33,14 @@ check_semver_range () {
main () {
check_root
check_dependencies jq
$set_status sdcard-update started
echo "Checking if SD card Umbrel is newer than external storage..."
local external_version=$(cat "${UMBREL_ROOT}/info.json" | jq -r .version | cut -d "-" -f "1")
local sd_version=$(cat "${SD_UMBREL_ROOT}/info.json" | jq -r .version | cut -d "-" -f "1")
if ! check_semver_range ">${external_version}" "${sd_version}"; then
echo "No, SD version is not newer, exiting."
$set_status sdcard-update completed
exit 0
fi
@ -48,11 +52,13 @@ main () {
echo "Checking if the external storage version \"${external_version}\" satisfies update requirement \"${update_requirement}\"..."
if ! check_semver_range "${update_requirement}" "${external_version}"; then
echo "No, we can't do an automatic update, exiting."
$set_status sdcard-update errored semver-mismatch
exit 0
fi
echo "Yes, it does, attempting an automatic update..."
"${UMBREL_ROOT}/scripts/update/update" --path "${SD_UMBREL_ROOT}"
$set_status sdcard-update completed
}
main

25
scripts/umbrel-os/services/umbrel-status-server-iptables-update.service

@ -0,0 +1,25 @@
# Umbrel Status Server iptables Update
# Installed at /etc/systemd/system/umbrel-status-server-iptables-update.service
# This is needed because when Docker starts it appends its own iptables rules
# after ours. This means traffic on port 80 will never arrive at a Docker container
# because we always redirect it. We can remove the rule and then re-apply it so
# it gets appended after the Docker rule so port 80 will only continue to be
# routed to the status server until a Docker container listens on port 80.
[Unit]
Description=Status Server iptables Update
Wants=docker.service
After=docker.service
[Service]
Type=oneshot
ExecStart=/status-server/setup-iptables
User=root
Group=root
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=status server iptables
[Install]
WantedBy=multi-user.target

21
scripts/umbrel-os/services/umbrel-status-server.service

@ -0,0 +1,21 @@
# Umbrel Status Server
# Installed at /etc/systemd/system/umbrel-status-server.service
[Unit]
Description=Status Server
Before=umbrel-external-storage-sdcard-update.service
Before=umbrel-external-storage.service
Before=umbrel-startup.service
[Service]
Type=exec
ExecStartPre=/home/umbrel/umbrel/scripts/umbrel-os/status-server/setup
ExecStart=/status-server/status-server
User=root
Group=root
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=status server
[Install]
WantedBy=multi-user.target

31
scripts/umbrel-os/status-server/set-status

@ -0,0 +1,31 @@
#!/usr/bin/env bash
STATUS_FILE_PATH="/umbrel-status"
service_id="${1}"
status="${2}"
error_code="${3}"
source /etc/default/umbrel 2> /dev/null
if [[ -z "${UMBREL_OS:-}" ]]; then
echo "Skipping status update when not on Umbrel OS"
exit
fi
if [[ "${service_id}" == "" ]]; then
echo "Error: Missing ID"
exit 1
fi
if [[ "${status}" == "" ]]; then
echo "Error: Missing Status"
exit 1
fi
entry="${service_id}:${status}"
if [[ "${error_code}" != "" ]]; then
entry="${entry}:${error_code}"
fi
echo "${entry}" >> "${STATUS_FILE_PATH}"

24
scripts/umbrel-os/status-server/setup

@ -0,0 +1,24 @@
#!/usr/bin/env bash
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../../..)"
BIND_MOUNT_PATH="/status-server"
STATUS_FILE_PATH="/umbrel-status"
# Bind mount status server to new location so we always run on the SD card
echo "Bind mounting status server to ${BIND_MOUNT_PATH}..."
[[ ! -d "${BIND_MOUNT_PATH}" ]] && mkdir -p "${BIND_MOUNT_PATH}"
mount --bind "${UMBREL_ROOT}/scripts/umbrel-os/status-server/" "${BIND_MOUNT_PATH}"
sync
sleep 1
# Clear status file
echo "clearing status file..."
echo "" > "${STATUS_FILE_PATH}"
# Append iptables rule to forward port 80 to port 8000
# The status server runs on port 8000 but this rule will route all port 80
# HTTP traffic to it.
# When the Umbrel service has started Docker will overwrite this rule and
# instead forward port 80 to the Umbrel HTTP server container.
echo "Setting iptables rules..."
"${BIND_MOUNT_PATH}/setup-iptables"

33
scripts/umbrel-os/status-server/setup-iptables

@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../../..)"
check_root () {
if [[ $UID != 0 ]]; then
echo "This script must be run as root"
exit 1
fi
}
main () {
check_root
# Remove and then re-append iptables rule
rule=(PREROUTING \
--table nat \
--proto tcp \
--dport 80 \
--jump REDIRECT \
--to-port 8000)
if iptables --delete ${rule[@]} 2> /dev/null; then
echo "Removed existing iptables entry."
else
echo "No existing iptables entry found."
fi
iptables --append ${rule[@]}
echo "Appended new iptables entry."
}
main

BIN
scripts/umbrel-os/status-server/static/favicon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

114
scripts/umbrel-os/status-server/static/index.html

@ -0,0 +1,114 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Umbrel</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="robots" content="noindex, nofollow" />
<meta name="referrer" content="no-referrer" />
<link rel="icon" href="/favicon.png">
<link rel="stylesheet" href="styles.css">
<meta name="description" content="Welcome back">
</head>
<body>
<noscript>
<strong>We're sorry but Umbrel
doesn't work properly without JavaScript enabled. Please enable it to
continue.</strong>
</noscript>
<div class="container">
<div class="view starting">
<svg class="logo" width="180" height="200" viewBox="0 0 180 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M90.166 200C93.4473 200 97.373 199.395 101.943 198.184C117.373 192.402 125.088 181.27 125.088 164.785V117.617C125.088 116.719 125.029 115.937 124.912 115.273C124.912 115.156 124.902 115.078 124.883 115.039C124.863 115 124.814 114.922 124.736 114.805C123.135 110.781 120.596 108.77 117.119 108.77H116.65L115.654 108.828H115.127C115.01 108.828 114.873 108.887 114.717 109.004C110.693 110.566 108.682 113.867 108.682 118.906V167.246C108.682 169.863 108.018 172.422 106.689 174.922C102.939 180.82 97.373 183.77 89.9902 183.77C84.4043 183.77 79.6777 182.031 75.8105 178.555C72.6465 175.977 71.0254 171.23 70.9473 164.316L70.8301 116.797C70.8301 116.367 70.791 115.918 70.7129 115.449C70.7129 115.293 70.7031 115.186 70.6836 115.127C70.6641 115.068 70.5957 114.961 70.4785 114.805C68.8379 110.703 66.2402 108.652 62.6855 108.652C61.5527 108.77 60.791 108.906 60.4004 109.062C56.4941 110.625 54.541 113.262 54.541 116.973L54.7168 166.25C54.7559 173.594 56.7871 180.215 60.8105 186.113C68.3105 195.371 78.0176 200 89.9316 200H90.166Z" fill="#5351FB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.0267 78.2397C25.0747 78.2397 19.5903 80.8572 16.1027 84.9716C13.2457 88.3419 8.19753 88.7581 4.82721 85.9011C1.45689 83.0442 1.04072 77.996 3.89766 74.6257C11.2138 65.9949 21.7471 62.2397 34.0267 62.2397C44.3462 62.2397 53.8044 64.8604 62.1984 70.1408C70.8948 64.9859 80.0577 62.2397 89.6031 62.2397C98.9359 62.2397 107.747 64.8667 115.938 69.8647C123.248 64.9426 132.023 62.6809 141.8 62.6809C154.063 62.6809 164.865 66.2318 173.642 73.7085C177.006 76.5734 177.41 81.6226 174.545 84.9861C171.68 88.3496 166.631 88.7538 163.267 85.8888C157.773 81.2084 150.804 78.6809 141.8 78.6809C133.295 78.6809 127.181 80.9311 122.714 84.787C119.22 87.8032 114.116 88.036 110.361 85.3504L110.22 85.2494C103.457 80.448 96.6344 78.2397 89.6031 78.2397C82.445 78.2397 75.1767 80.531 67.6755 85.6062C64.1535 87.9891 59.5099 87.8844 56.099 85.3452L55.8512 85.1607C49.5737 80.5653 42.3951 78.2397 34.0267 78.2397Z" fill="#5351FB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M133.969 32.9107C122.302 22.7916 107.261 17.6512 88.1548 18.0182C68.9596 18.3871 54.0522 23.9035 42.7387 34.0244C31.3296 44.2309 22.8201 59.7981 17.7954 81.5961C16.803 85.9015 12.5082 88.5872 8.20286 87.5947C3.89749 86.6023 1.21181 82.3076 2.20425 78.0022C7.71919 54.0773 17.4796 35.153 32.071 22.0997C46.7579 8.96082 65.5814 2.44902 87.8474 2.0212C110.203 1.59166 129.301 7.68281 144.452 20.8236C159.471 33.8502 169.858 53.1503 176.202 77.8057C177.303 82.0846 174.727 86.4459 170.448 87.5468C166.169 88.6478 161.808 86.0715 160.707 81.7926C154.876 59.1318 145.767 43.1438 133.969 32.9107Z" fill="#5351FB"/>
</svg>
<p><span class="spinner rotate"></span>Starting Umbrel...</p>
</div>
<div class="view restarting">
<svg class="logo" width="180" height="200" viewBox="0 0 180 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M90.166 200C93.4473 200 97.373 199.395 101.943 198.184C117.373 192.402 125.088 181.27 125.088 164.785V117.617C125.088 116.719 125.029 115.937 124.912 115.273C124.912 115.156 124.902 115.078 124.883 115.039C124.863 115 124.814 114.922 124.736 114.805C123.135 110.781 120.596 108.77 117.119 108.77H116.65L115.654 108.828H115.127C115.01 108.828 114.873 108.887 114.717 109.004C110.693 110.566 108.682 113.867 108.682 118.906V167.246C108.682 169.863 108.018 172.422 106.689 174.922C102.939 180.82 97.373 183.77 89.9902 183.77C84.4043 183.77 79.6777 182.031 75.8105 178.555C72.6465 175.977 71.0254 171.23 70.9473 164.316L70.8301 116.797C70.8301 116.367 70.791 115.918 70.7129 115.449C70.7129 115.293 70.7031 115.186 70.6836 115.127C70.6641 115.068 70.5957 114.961 70.4785 114.805C68.8379 110.703 66.2402 108.652 62.6855 108.652C61.5527 108.77 60.791 108.906 60.4004 109.062C56.4941 110.625 54.541 113.262 54.541 116.973L54.7168 166.25C54.7559 173.594 56.7871 180.215 60.8105 186.113C68.3105 195.371 78.0176 200 89.9316 200H90.166Z" fill="#5351FB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.0267 78.2397C25.0747 78.2397 19.5903 80.8572 16.1027 84.9716C13.2457 88.3419 8.19753 88.7581 4.82721 85.9011C1.45689 83.0442 1.04072 77.996 3.89766 74.6257C11.2138 65.9949 21.7471 62.2397 34.0267 62.2397C44.3462 62.2397 53.8044 64.8604 62.1984 70.1408C70.8948 64.9859 80.0577 62.2397 89.6031 62.2397C98.9359 62.2397 107.747 64.8667 115.938 69.8647C123.248 64.9426 132.023 62.6809 141.8 62.6809C154.063 62.6809 164.865 66.2318 173.642 73.7085C177.006 76.5734 177.41 81.6226 174.545 84.9861C171.68 88.3496 166.631 88.7538 163.267 85.8888C157.773 81.2084 150.804 78.6809 141.8 78.6809C133.295 78.6809 127.181 80.9311 122.714 84.787C119.22 87.8032 114.116 88.036 110.361 85.3504L110.22 85.2494C103.457 80.448 96.6344 78.2397 89.6031 78.2397C82.445 78.2397 75.1767 80.531 67.6755 85.6062C64.1535 87.9891 59.5099 87.8844 56.099 85.3452L55.8512 85.1607C49.5737 80.5653 42.3951 78.2397 34.0267 78.2397Z" fill="#5351FB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M133.969 32.9107C122.302 22.7916 107.261 17.6512 88.1548 18.0182C68.9596 18.3871 54.0522 23.9035 42.7387 34.0244C31.3296 44.2309 22.8201 59.7981 17.7954 81.5961C16.803 85.9015 12.5082 88.5872 8.20286 87.5947C3.89749 86.6023 1.21181 82.3076 2.20425 78.0022C7.71919 54.0773 17.4796 35.153 32.071 22.0997C46.7579 8.96082 65.5814 2.44902 87.8474 2.0212C110.203 1.59166 129.301 7.68281 144.452 20.8236C159.471 33.8502 169.858 53.1503 176.202 77.8057C177.303 82.0846 174.727 86.4459 170.448 87.5468C166.169 88.6478 161.808 86.0715 160.707 81.7926C154.876 59.1318 145.767 43.1438 133.969 32.9107Z" fill="#5351FB"/>
</svg>
<p><span class="spinner rotate"></span>Restarting...</p>
<p>Please do not disconnect the power supply while the restart is in progress.</p>
</div>
<div class="view shutting-down">
<svg class="logo" width="180" height="200" viewBox="0 0 180 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M90.166 200C93.4473 200 97.373 199.395 101.943 198.184C117.373 192.402 125.088 181.27 125.088 164.785V117.617C125.088 116.719 125.029 115.937 124.912 115.273C124.912 115.156 124.902 115.078 124.883 115.039C124.863 115 124.814 114.922 124.736 114.805C123.135 110.781 120.596 108.77 117.119 108.77H116.65L115.654 108.828H115.127C115.01 108.828 114.873 108.887 114.717 109.004C110.693 110.566 108.682 113.867 108.682 118.906V167.246C108.682 169.863 108.018 172.422 106.689 174.922C102.939 180.82 97.373 183.77 89.9902 183.77C84.4043 183.77 79.6777 182.031 75.8105 178.555C72.6465 175.977 71.0254 171.23 70.9473 164.316L70.8301 116.797C70.8301 116.367 70.791 115.918 70.7129 115.449C70.7129 115.293 70.7031 115.186 70.6836 115.127C70.6641 115.068 70.5957 114.961 70.4785 114.805C68.8379 110.703 66.2402 108.652 62.6855 108.652C61.5527 108.77 60.791 108.906 60.4004 109.062C56.4941 110.625 54.541 113.262 54.541 116.973L54.7168 166.25C54.7559 173.594 56.7871 180.215 60.8105 186.113C68.3105 195.371 78.0176 200 89.9316 200H90.166Z" fill="#5351FB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.0267 78.2397C25.0747 78.2397 19.5903 80.8572 16.1027 84.9716C13.2457 88.3419 8.19753 88.7581 4.82721 85.9011C1.45689 83.0442 1.04072 77.996 3.89766 74.6257C11.2138 65.9949 21.7471 62.2397 34.0267 62.2397C44.3462 62.2397 53.8044 64.8604 62.1984 70.1408C70.8948 64.9859 80.0577 62.2397 89.6031 62.2397C98.9359 62.2397 107.747 64.8667 115.938 69.8647C123.248 64.9426 132.023 62.6809 141.8 62.6809C154.063 62.6809 164.865 66.2318 173.642 73.7085C177.006 76.5734 177.41 81.6226 174.545 84.9861C171.68 88.3496 166.631 88.7538 163.267 85.8888C157.773 81.2084 150.804 78.6809 141.8 78.6809C133.295 78.6809 127.181 80.9311 122.714 84.787C119.22 87.8032 114.116 88.036 110.361 85.3504L110.22 85.2494C103.457 80.448 96.6344 78.2397 89.6031 78.2397C82.445 78.2397 75.1767 80.531 67.6755 85.6062C64.1535 87.9891 59.5099 87.8844 56.099 85.3452L55.8512 85.1607C49.5737 80.5653 42.3951 78.2397 34.0267 78.2397Z" fill="#5351FB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M133.969 32.9107C122.302 22.7916 107.261 17.6512 88.1548 18.0182C68.9596 18.3871 54.0522 23.9035 42.7387 34.0244C31.3296 44.2309 22.8201 59.7981 17.7954 81.5961C16.803 85.9015 12.5082 88.5872 8.20286 87.5947C3.89749 86.6023 1.21181 82.3076 2.20425 78.0022C7.71919 54.0773 17.4796 35.153 32.071 22.0997C46.7579 8.96082 65.5814 2.44902 87.8474 2.0212C110.203 1.59166 129.301 7.68281 144.452 20.8236C159.471 33.8502 169.858 53.1503 176.202 77.8057C177.303 82.0846 174.727 86.4459 170.448 87.5468C166.169 88.6478 161.808 86.0715 160.707 81.7926C154.876 59.1318 145.767 43.1438 133.969 32.9107Z" fill="#5351FB"/>
</svg>
<p><span class="spinner rotate"></span>Shutting down...</p>
<p>Please do not disconnect the power supply until the shutdown is complete.</p>
</div>
<div class="view shutdown-complete">
<svg class="logo dark" width="180" height="200" viewBox="0 0 180 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M90.166 200C93.4473 200 97.373 199.395 101.943 198.184C117.373 192.402 125.088 181.27 125.088 164.785V117.617C125.088 116.719 125.029 115.937 124.912 115.273C124.912 115.156 124.902 115.078 124.883 115.039C124.863 115 124.814 114.922 124.736 114.805C123.135 110.781 120.596 108.77 117.119 108.77H116.65L115.654 108.828H115.127C115.01 108.828 114.873 108.887 114.717 109.004C110.693 110.566 108.682 113.867 108.682 118.906V167.246C108.682 169.863 108.018 172.422 106.689 174.922C102.939 180.82 97.373 183.77 89.9902 183.77C84.4043 183.77 79.6777 182.031 75.8105 178.555C72.6465 175.977 71.0254 171.23 70.9473 164.316L70.8301 116.797C70.8301 116.367 70.791 115.918 70.7129 115.449C70.7129 115.293 70.7031 115.186 70.6836 115.127C70.6641 115.068 70.5957 114.961 70.4785 114.805C68.8379 110.703 66.2402 108.652 62.6855 108.652C61.5527 108.77 60.791 108.906 60.4004 109.062C56.4941 110.625 54.541 113.262 54.541 116.973L54.7168 166.25C54.7559 173.594 56.7871 180.215 60.8105 186.113C68.3105 195.371 78.0176 200 89.9316 200H90.166Z" fill="#5351FB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.0267 78.2397C25.0747 78.2397 19.5903 80.8572 16.1027 84.9716C13.2457 88.3419 8.19753 88.7581 4.82721 85.9011C1.45689 83.0442 1.04072 77.996 3.89766 74.6257C11.2138 65.9949 21.7471 62.2397 34.0267 62.2397C44.3462 62.2397 53.8044 64.8604 62.1984 70.1408C70.8948 64.9859 80.0577 62.2397 89.6031 62.2397C98.9359 62.2397 107.747 64.8667 115.938 69.8647C123.248 64.9426 132.023 62.6809 141.8 62.6809C154.063 62.6809 164.865 66.2318 173.642 73.7085C177.006 76.5734 177.41 81.6226 174.545 84.9861C171.68 88.3496 166.631 88.7538 163.267 85.8888C157.773 81.2084 150.804 78.6809 141.8 78.6809C133.295 78.6809 127.181 80.9311 122.714 84.787C119.22 87.8032 114.116 88.036 110.361 85.3504L110.22 85.2494C103.457 80.448 96.6344 78.2397 89.6031 78.2397C82.445 78.2397 75.1767 80.531 67.6755 85.6062C64.1535 87.9891 59.5099 87.8844 56.099 85.3452L55.8512 85.1607C49.5737 80.5653 42.3951 78.2397 34.0267 78.2397Z" fill="#5351FB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M133.969 32.9107C122.302 22.7916 107.261 17.6512 88.1548 18.0182C68.9596 18.3871 54.0522 23.9035 42.7387 34.0244C31.3296 44.2309 22.8201 59.7981 17.7954 81.5961C16.803 85.9015 12.5082 88.5872 8.20286 87.5947C3.89749 86.6023 1.21181 82.3076 2.20425 78.0022C7.71919 54.0773 17.4796 35.153 32.071 22.0997C46.7579 8.96082 65.5814 2.44902 87.8474 2.0212C110.203 1.59166 129.301 7.68281 144.452 20.8236C159.471 33.8502 169.858 53.1503 176.202 77.8057C177.303 82.0846 174.727 86.4459 170.448 87.5468C166.169 88.6478 161.808 86.0715 160.707 81.7926C154.876 59.1318 145.767 43.1438 133.969 32.9107Z" fill="#5351FB"/>
</svg>
<span class="text">Shutdown complete</span>
</div>
<div class="view error">
<svg class="logo danger" width="180" height="200" viewBox="0 0 180 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M90.166 200C93.4473 200 97.373 199.395 101.943 198.184C117.373 192.402 125.088 181.27 125.088 164.785V117.617C125.088 116.719 125.029 115.937 124.912 115.273C124.912 115.156 124.902 115.078 124.883 115.039C124.863 115 124.814 114.922 124.736 114.805C123.135 110.781 120.596 108.77 117.119 108.77H116.65L115.654 108.828H115.127C115.01 108.828 114.873 108.887 114.717 109.004C110.693 110.566 108.682 113.867 108.682 118.906V167.246C108.682 169.863 108.018 172.422 106.689 174.922C102.939 180.82 97.373 183.77 89.9902 183.77C84.4043 183.77 79.6777 182.031 75.8105 178.555C72.6465 175.977 71.0254 171.23 70.9473 164.316L70.8301 116.797C70.8301 116.367 70.791 115.918 70.7129 115.449C70.7129 115.293 70.7031 115.186 70.6836 115.127C70.6641 115.068 70.5957 114.961 70.4785 114.805C68.8379 110.703 66.2402 108.652 62.6855 108.652C61.5527 108.77 60.791 108.906 60.4004 109.062C56.4941 110.625 54.541 113.262 54.541 116.973L54.7168 166.25C54.7559 173.594 56.7871 180.215 60.8105 186.113C68.3105 195.371 78.0176 200 89.9316 200H90.166Z" fill="#5351FB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.0267 78.2397C25.0747 78.2397 19.5903 80.8572 16.1027 84.9716C13.2457 88.3419 8.19753 88.7581 4.82721 85.9011C1.45689 83.0442 1.04072 77.996 3.89766 74.6257C11.2138 65.9949 21.7471 62.2397 34.0267 62.2397C44.3462 62.2397 53.8044 64.8604 62.1984 70.1408C70.8948 64.9859 80.0577 62.2397 89.6031 62.2397C98.9359 62.2397 107.747 64.8667 115.938 69.8647C123.248 64.9426 132.023 62.6809 141.8 62.6809C154.063 62.6809 164.865 66.2318 173.642 73.7085C177.006 76.5734 177.41 81.6226 174.545 84.9861C171.68 88.3496 166.631 88.7538 163.267 85.8888C157.773 81.2084 150.804 78.6809 141.8 78.6809C133.295 78.6809 127.181 80.9311 122.714 84.787C119.22 87.8032 114.116 88.036 110.361 85.3504L110.22 85.2494C103.457 80.448 96.6344 78.2397 89.6031 78.2397C82.445 78.2397 75.1767 80.531 67.6755 85.6062C64.1535 87.9891 59.5099 87.8844 56.099 85.3452L55.8512 85.1607C49.5737 80.5653 42.3951 78.2397 34.0267 78.2397Z" fill="#5351FB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M133.969 32.9107C122.302 22.7916 107.261 17.6512 88.1548 18.0182C68.9596 18.3871 54.0522 23.9035 42.7387 34.0244C31.3296 44.2309 22.8201 59.7981 17.7954 81.5961C16.803 85.9015 12.5082 88.5872 8.20286 87.5947C3.89749 86.6023 1.21181 82.3076 2.20425 78.0022C7.71919 54.0773 17.4796 35.153 32.071 22.0997C46.7579 8.96082 65.5814 2.44902 87.8474 2.0212C110.203 1.59166 129.301 7.68281 144.452 20.8236C159.471 33.8502 169.858 53.1503 176.202 77.8057C177.303 82.0846 174.727 86.4459 170.448 87.5468C166.169 88.6478 161.808 86.0715 160.707 81.7926C154.876 59.1318 145.767 43.1438 133.969 32.9107Z" fill="#5351FB"/>
</svg>
<div class="error-text monitor-check">
<p><b class="text-dark">Error: External drive disconnected</b></p>
<p>The external drive was disconnected while Umbrel was running. This can sometimes happen when using an unofficial Raspberry Pi power supply.</p>
</div>
<div class="error-text semver-mismatch">
<p><b class="text-dark">Error: Version mismatch</b></p>
<p>The version of UmbrelOS on your microSD card is not compatible with the version of Umbrel on your external drive.</p>
</div>
<div class="error-text no-block-device">
<p><b class="text-dark">Error: No external drive found</b></p>
<p>Please connect an external drive (at least 1TB) to a USB 3.0 port (blue color) on your Raspberry Pi and restart your Umbrel.</p>
</div>
<div class="error-text multiple-block-devices">
<p><b class="text-dark">Error: Multiple external drives found</b></p>
<p>Umbrel only works with one external drive of atleast 1TB size. Please disconnect any additional external drives and restart your Umbrel.</p>
</div>
<div class="error-text rebinding-failed">
<p><b class="text-dark">Error: Failed to connect external drive</b></p>
<p>There was an error connecting your external drive. Please consider using the recommended hardware listed on getumbrel.com.</p>
</div>
<br/><br/>
<a href="#" class="button restart"><svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.7449 4.90934C11.4816 4.73287 11.1258 4.80433 10.9503 5.06888L10.9332 5.09455C10.726 2.25044 8.3568 0 5.47409 0C2.45566 3.83785e-05 0 2.46731 0 5.50002C0 8.53273 2.45566 11 5.47409 11C5.89602 11 6.23805 10.6564 6.23805 10.2324C6.23805 9.8085 5.89602 9.46486 5.47409 9.46486C3.29815 9.46482 1.52791 7.68621 1.52791 5.50002C1.52791 3.31383 3.29815 1.53518 5.47409 1.53518C7.49086 1.53518 9.15884 3.06322 9.39208 5.02747C9.20823 4.79754 8.87694 4.74262 8.62812 4.90934C8.36482 5.08572 8.29374 5.44318 8.46929 5.70769L9.60384 7.4171C9.73773 7.61882 9.95557 7.73925 10.1866 7.73925C10.4175 7.73925 10.6353 7.61878 10.7691 7.4171L11.9037 5.70769C12.0792 5.44314 12.0081 5.08572 11.7449 4.90934Z" fill="#5351FB"/>
</svg>
Restart
</a>
<a href="#" class="button shutdown">
<svg width="11" height="12" viewBox="0 0 11 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.0243996 7.11592C-0.15516 5.21752 0.670316 3.49794 2.03946 2.40545C2.57315 1.97923 3.3712 2.34421 3.3712 3.01783C3.3712 3.26524 3.25149 3.49305 3.05697 3.64737C2.08684 4.42387 1.50328 5.64864 1.64543 7.00079C1.82997 8.76201 3.26645 10.1876 5.05707 10.3836C7.39384 10.6408 9.37897 8.84284 9.37897 6.59906C9.37897 5.40859 8.81785 4.34304 7.94249 3.64492C7.74797 3.4906 7.63076 3.26279 7.63076 3.01783C7.63076 2.35156 8.41633 1.97433 8.94504 2.39075C10.197 3.38282 11 4.89908 11 6.59906C11 9.70753 8.31159 12.2134 5.09697 11.9856C2.43848 11.8019 0.271294 9.72223 0.0243996 7.11592ZM5.49848 0C5.04958 0 4.68797 0.357633 4.68797 0.796101V4.46306C4.68797 4.90398 5.05208 5.25916 5.49848 5.25916C5.94489 5.25916 6.309 4.90153 6.309 4.46306V0.796101C6.309 0.357633 5.94738 0 5.49848 0Z" fill="#5351FB"/>
</svg>
Shutdown
</a>
<a class="button community" href="https://community.getumbrel.com" target="_blank">community</a>
</div>
</div>
<script src="/script.js"></script>
</body>
</html>

117
scripts/umbrel-os/status-server/static/script.js

@ -0,0 +1,117 @@
const isIframe = (window.self !== window.top);
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const on = (selector, eventName, callback) => {
for (element of document.querySelectorAll(selector)) {
element.addEventListener(eventName, event => {
event.preventDefault();
callback();
});
}
};
const setState = (key, value) => document.body.dataset[key] = value;
const getState = key => document.body.dataset[key];
const isUmbrelUp = async () => {
const response = await fetch('/manager-api/ping');
return response.status === 200 && response.redirected === false;
};
const checkForError = async () => {
const response = await fetch('/status');
const status = await response.json();
const errorCode = (status.find(service => service.status === 'errored') || {}).error;
return errorCode;
};
const isStatusServerUp = async () => {
try {
await checkForError();
return true;
} catch (e) {
return false;
}
};
const power = async action => {
try {
const token = await (await fetch('/token')).json();
const response = await fetch(`/${action}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({token}),
});
if (!response.ok) {
return false;
}
} catch (e) {} finally {
return true;
}
};
on('.shutdown', 'click', async () => {
if (! await power('shutdown')) {
alert('Failed to shutdown Umbrel');
return;
}
setState('status', 'shutting-down');
await delay(30000);
setState('status', 'shutdown-complete');
});
on('.restart', 'click', async () => {
if (! await power('restart')) {
alert('Failed to restart Umbrel');
return;
}
setState('status', 'restarting');
await delay(10000);
// Wait for Umbrel to come back up then reload the page.
while (true) {
try {
if (await isStatusServerUp() || await isUmbrelUp()) {
window.location.reload();
}
} catch (e) {}
await delay(1000);
}
});
const main = async () => {
// Protect against clickjacking
if (isIframe) {
document.body.innerText = 'For security reasons Umbrel doesn\'t work in an iframe.';
return;
}
// Set initial loading state
setState('status', 'starting');
// Start loop
while (getState('status') === 'starting') {
try {
// If Umbrel is ready, reload
if (await isUmbrelUp()) {
window.location.reload();
}
// If there are errors, set error state
const error = await checkForError();
if (error) {
setState('status', 'error');
setState('error', error);
}
} catch (e) {
console.error(e);
}
await delay(1000);
}
};
main();

117
scripts/umbrel-os/status-server/static/styles.css

@ -0,0 +1,117 @@
body {
background-color: #F7F9FB;
font-family: system-ui, -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", "Segoe UI", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
margin: 0;
padding: 0;
font-size: 0.9rem;
font-weight: 400;
line-height: 1.5;
color: #6c757d;
text-align: center;
}
span, p {
margin: 0 0 1em 0;
}
p {
display: flex;
}
.container {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0 20px;
}
.view {
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
}
[data-status="starting"] .view.starting,
[data-status="restarting"] .view.restarting,
[data-status="shutting-down"] .view.shutting-down,
[data-status="shutdown-complete"] .view.shutdown-complete,
[data-status="error"] .view.error {
display: flex;
}
.error-text {
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
}
[data-error="monitor-check"] .error-text.monitor-check,
[data-error="semver-mismatch"] .error-text.semver-mismatch,
[data-error="no-block-device"] .error-text.no-block-device,
[data-error="multiple-block-devices"] .error-text.multiple-block-devices,
[data-error="rebinding-failed"] .error-text.rebinding-failed {
display: flex;
}
.logo {
max-height: 200px;
width: auto;
display: block;
margin-bottom: 80px;
}
.logo.danger path {
fill: #F46E6E;
}
.logo.dark path {
fill: #858997;
}
.text-dark {
color: #141821;
}
.spinner {
font-size: 80%;
font-weight: 400;
box-sizing: border-box;
display: inline-block;
vertical-align: text-bottom;
border: 0.25em solid currentColor;
border-right-color: transparent;
border-radius: 50%;
width: 1rem;
height: 1rem;
border-width: 0.2em;
margin: 0.17em 1em 0 0;
}
.button {
display: block;
margin-bottom: 1rem;
text-decoration: none;
color: #5351FB;
}
.button svg {
margin-right: 0.1rem;
}
.button.community {
position: absolute;
bottom: 0;
left: auto;
right: auto;
}
.rotate {
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from {transform: rotate(0deg)}
to {transform: rotate(360deg)}
}

32
scripts/umbrel-os/status-server/status-server

@ -0,0 +1,32 @@
#!/usr/bin/env -S python3 -u
from pathlib import Path
import subprocess
from util.server import Server
from util.csrf import get_csrf_token, verify_csrf_token
import util.status_file as status_file
STATUS_FILE_PATH='/umbrel-status'
def csrf(request):
if not verify_csrf_token(request['post_data']['token']):
raise Exception("Invalid CSRF token")
def get_relative_path(path):
script_dir = Path(__file__).resolve().parent
return str(script_dir.joinpath(path))
def run(command):
if not status_file.contains_errors(STATUS_FILE_PATH):
raise Exception("Running commands is disabled if there are no status errors")
subprocess.run(command, check=True)
def main():
server = Server(directory=get_relative_path('static'))
server.get('/status', lambda _: status_file.parse(STATUS_FILE_PATH))
server.get('/token', lambda _: get_csrf_token())
server.post('/shutdown', csrf, lambda _: run(['poweroff']))
server.post('/restart', csrf, lambda _: run(['reboot']))
server.listen(8000)
main()

9
scripts/umbrel-os/status-server/util/csrf.py

@ -0,0 +1,9 @@
import secrets
csrf_token = secrets.token_hex(32)
def get_csrf_token():
return csrf_token
def verify_csrf_token(token):
return token == get_csrf_token()

88
scripts/umbrel-os/status-server/util/server.py

@ -0,0 +1,88 @@
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
import json
class Server():
def __init__(self, directory='.'):
self.directory = directory
self.get_routes = []
self.post_routes = []
# Register a GET handler
def get(self, path, *handlers):
self.get_routes.append({'path': path, 'handlers': handlers})
# Register a POST handler
def post(self, path, *handlers):
self.post_routes.append({'path': path, 'handlers': handlers})
# Create the server
def listen(self, port):
directory = self.directory
post_routes = self.post_routes
get_routes = self.get_routes
# Create request handler class
class Handler(SimpleHTTPRequestHandler):
# Patch the constructor with our directory
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=directory, **kwargs)
def send_error(self, code, message=None):
if code == 404 and self.path != '/':
self.send_response(302)
self.send_header('Location', '/')
self.end_headers()
else:
super().send_error(code, message)
# JSON helper
def send_json_response(self, status_code, data=None):
if status_code >= 400:
data = {'error': True}
self.send_response(status_code)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(bytes(json.dumps(data), 'utf8'))
# Loop over routes and execute one if there's a match
def handle_routes(self, routes):
# Loop over the registered routes
for route in routes:
# Check if we have a match
if self.path == route['path']:
try:
request = {}
# Parse post data
if self.headers['Content-Length'] and self.headers['Content-Type'] == 'application/json':
content_length = int(self.headers['Content-Length'])
raw_post_data = self.rfile.read(content_length)
request['post_data'] = json.loads(raw_post_data.decode('utf-8'))
# Execute handlers
for handler in route['handlers']:
response = handler(request)
self.send_json_response(200, response)
except Exception as e:
# If it failed, send internal server error
print(f'Exception: {e}')
self.send_json_response(500)
# Route matched, return True
return True
# No routes matched, return False
return False
# Try to match a route aganst a GET request
# else fall back to static server
def do_GET(self):
if not self.handle_routes(get_routes):
super().do_GET()
# Try to match a route aganst a POST request
# else return 404
def do_POST(self):
if not self.handle_routes(post_routes):
self.send_json_response(404)
# Start HTTP server and attach handler
print(f'Server listening on port {port}...')
with ThreadingHTTPServer(('', port), Handler) as server:
server.serve_forever()

54
scripts/umbrel-os/status-server/util/status_file.py

@ -0,0 +1,54 @@
#!/usr/bin/env python3
from pathlib import Path
# A status file has status entries, one per line, in the format:
# $id:$status[:$error-code]
# e.g:
# foo:started
# foo:completed
# bar:started
# fizz:started
# fizz:completed
# bar:errored:no-network-access
# Creates an empty status file
def create_empty(status_file_path):
Path(status_file_path).open('w').close()
# Returns the index of the dict with a matching ID from a list
def _get_index(list, id):
for i, dict in enumerate(list):
if dict['id'] == id:
return i
return None
# Parses a status file
def parse(status_file_path):
# Empty list for holding the status entries
statuses = []
# Read the status file and loop over the entries
for entry in Path(status_file_path).read_text().split():
# Decode parts with ":" seperator
parts = entry.split(':')
parsed_entry = {
'id': parts[0],
'status': parts[1],
'error': parts[2] if len(parts) >2 else None
}
# Check if we already have an entry for this id
index = _get_index(statuses, parsed_entry['id'])
# If we don't, append the entry
if index == None:
statuses.append(parsed_entry)
# If we do, update the entry
else:
statuses[index] = parsed_entry
return statuses
# Checks if a status file contains errors
def contains_errors(status_file_path):
status = parse(status_file_path)
for entry in status:
if entry['status'] == 'errored':
return True
return False
Loading…
Cancel
Save