mirror of https://github.com/lukechilds/umbrel.git
Browse Source
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
18 changed files with 695 additions and 3 deletions
@ -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 |
@ -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 |
@ -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}" |
@ -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" |
@ -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 |
After Width: | Height: | Size: 4.2 KiB |
@ -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> |
@ -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(); |
@ -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)} |
|||
} |
@ -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() |
@ -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() |
@ -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() |
@ -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…
Reference in new issue