mirror of
https://github.com/getumbrel/umbrel.git
synced 2026-10-12 02:14:08 +00:00
Remove unused scripts and comment existing scripts
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"NOTE": "We must keep this file here forever to allow old umbrelOS 0.5.x Umbrel Homes to find the 1.0.0 update and bootstrap themselves into the new mender based update system.",
|
||||
"version": "1.0.0",
|
||||
"name": "umbrelOS 1.0",
|
||||
"requires": ">=0.5.3",
|
||||
|
||||
-594
@@ -1,594 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="0.0.3"
|
||||
|
||||
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
|
||||
USER_FILE="${UMBREL_ROOT}/db/user.json"
|
||||
|
||||
APP_PROXY_SERVICE_NAME="app_proxy"
|
||||
REMOTE_TOR_ACCESS="false"
|
||||
if [[ -f "${USER_FILE}" ]]; then
|
||||
REMOTE_TOR_ACCESS=$(cat "${USER_FILE}" | jq 'has("remoteTorAccess") and .remoteTorAccess')
|
||||
fi
|
||||
|
||||
show_help() {
|
||||
cat << EOF
|
||||
CLI (v${VERSION}) for managing Umbrel apps
|
||||
|
||||
Usage: app <command> <app> [<arguments>]
|
||||
|
||||
Commands:
|
||||
install Pulls down images for an app and starts it
|
||||
uninstall Removes images and destroys all data for an app
|
||||
reinstall Calls 'uninstall', followed by 'install' for an app
|
||||
start Starts an installed app
|
||||
stop Stops an installed app
|
||||
restart Restarts an installed app
|
||||
compose Passes all arguments to docker-compose
|
||||
ls-installed Lists installed apps
|
||||
EOF
|
||||
}
|
||||
|
||||
check_dependencies () {
|
||||
for cmd in "$@"; do
|
||||
if ! command -v $cmd >/dev/null 2>&1; then
|
||||
>&2 echo "This script requires \"${cmd}\" to be installed"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
list_installed_apps() {
|
||||
cat "${USER_FILE}" 2> /dev/null | jq -r 'if has("installedApps") then .installedApps else [] end | join("\n")' || true
|
||||
}
|
||||
|
||||
# Deterministically derives 128 bits of cryptographically secure entropy
|
||||
derive_entropy () {
|
||||
# Make sure we use the seed from the real Umbrel installation if this is
|
||||
# an OTA update.
|
||||
SEED_FILE="${UMBREL_ROOT}/db/umbrel-seed/seed"
|
||||
if [[ ! -f "${SEED_FILE}" ]] && [[ -f "${UMBREL_ROOT}/../.umbrel" ]]; then
|
||||
SEED_FILE="${UMBREL_ROOT}/../db/umbrel-seed/seed"
|
||||
fi
|
||||
|
||||
identifier="${1}"
|
||||
umbrel_seed=$(cat "${SEED_FILE}") || true
|
||||
|
||||
if [[ -z "$umbrel_seed" ]] || [[ -z "$identifier" ]]; then
|
||||
>&2 echo "Missing derivation parameter, this is unsafe, exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# We need `sed 's/^.* //'` to trim the "(stdin)= " prefix from some versions of openssl
|
||||
printf "%s" "${identifier}" | openssl dgst -sha256 -hmac "${umbrel_seed}" | sed 's/^.* //'
|
||||
}
|
||||
|
||||
# Setup env. for this context for a given app
|
||||
source_app() {
|
||||
local -r app="${1}"
|
||||
|
||||
local -r app_domain="$(hostname -s 2>/dev/null || echo "umbrel").local"
|
||||
local -r app_entropy_identifier="app-${app}-seed"
|
||||
|
||||
# Load in existing Umbrel .env
|
||||
# So that apps in their exports.sh can access
|
||||
# e.g. $TOR_PROXY_IP, $TOR_PROXY_PORT
|
||||
[[ -f "${UMBREL_ROOT}/.env" ]] && . "${UMBREL_ROOT}/.env"
|
||||
|
||||
export NETWORK_IP="${NETWORK_IP}"
|
||||
|
||||
# Set other useful vars. used in exports
|
||||
export DEVICE_HOSTNAME="$(cat /proc/sys/kernel/hostname 2>/dev/null || echo "umbrel")"
|
||||
export DEVICE_DOMAIN_NAME="${DEVICE_HOSTNAME}.local"
|
||||
|
||||
# Set env using all installed apps exports.sh
|
||||
# Do this first so that no app exports can
|
||||
# Override any app specific exports defined below
|
||||
EXPORTS_TOR_DATA_DIR="${UMBREL_ROOT}/tor/data"
|
||||
|
||||
APPS_TO_SOURCE="$(list_installed_apps)"
|
||||
|
||||
# $app might not be in the 'installed apps list' yet
|
||||
# i.e. If it is currently being installed
|
||||
# So we'll add it to the list of apps that will be 'sourced'
|
||||
if ! echo "${APPS_TO_SOURCE}" | grep --quiet "^${app}$"; then
|
||||
APPS_TO_SOURCE="${APPS_TO_SOURCE}"$'\n'"${app}"
|
||||
fi
|
||||
|
||||
for EXPORTS_APP_ID in $APPS_TO_SOURCE; do
|
||||
EXPORTS_APP_DIR="${UMBREL_ROOT}/app-data/${EXPORTS_APP_ID}"
|
||||
EXPORTS_APP_FILE="${EXPORTS_APP_DIR}/exports.sh"
|
||||
EXPORTS_APP_DATA_DIR="${EXPORTS_APP_DIR}/data"
|
||||
|
||||
[[ -f "${EXPORTS_APP_FILE}" ]] && . "${EXPORTS_APP_FILE}" || true
|
||||
done
|
||||
|
||||
# App specific exports
|
||||
export APP_ID="${app}"
|
||||
export APP_MANIFEST_FILE="${app_data_dir}/umbrel-app.yml"
|
||||
export APP_VERSION=$(cat "${APP_MANIFEST_FILE}" | yq '.version')
|
||||
|
||||
# This provides the app proxy with context of the app
|
||||
export APP_PROXY_HOSTNAME="app_proxy_${app}"
|
||||
export APP_PROXY_PORT=$(cat "${APP_MANIFEST_FILE}" | yq '.port')
|
||||
|
||||
export APP_DATA_DIR="${app_data_dir}"
|
||||
export APP_DOMAIN="${app_domain}"
|
||||
export APP_HIDDEN_SERVICE="not-enabled.onion"
|
||||
if [[ "${REMOTE_TOR_ACCESS}" == "true" ]]; then
|
||||
export APP_HIDDEN_SERVICE="$(cat "${app_hidden_service_file}" 2>/dev/null || echo "notyetset.onion")"
|
||||
fi
|
||||
export APP_SEED=$(derive_entropy "${app_entropy_identifier}")
|
||||
export APP_PASSWORD=$(derive_entropy "${app_entropy_identifier}-APP_PASSWORD")
|
||||
|
||||
# Tor specific exports
|
||||
export TOR_DATA_DIR="${UMBREL_ROOT}/tor/data"
|
||||
export TOR_ENTRYPOINT_SCRIPT="${UMBREL_ROOT}/scripts/support/tor-entrypoint.sh"
|
||||
export TOR_HS_APP_DIR="/data/app-${app}"
|
||||
export TOR_HS_PORTS="80:${APP_PROXY_HOSTNAME}:${APP_PROXY_PORT}"
|
||||
|
||||
tor_extra_hs_varname=$(echo "APP_${APP_ID^^}_TOR_HS_EXTRA_PORTS" | tr '-' '_')
|
||||
tor_hs_extra_ports="${!tor_extra_hs_varname:-}"
|
||||
|
||||
if [[ ! -z "${tor_hs_extra_ports}" ]]; then
|
||||
export TOR_HS_PORTS="${TOR_HS_PORTS} ${tor_hs_extra_ports}"
|
||||
fi
|
||||
|
||||
# Other
|
||||
export UMBREL_ROOT
|
||||
}
|
||||
|
||||
# Check dependencies
|
||||
check_dependencies docker-compose jq yq openssl envsubst
|
||||
|
||||
if [ -z ${1+x} ]; then
|
||||
command=""
|
||||
else
|
||||
command="$1"
|
||||
fi
|
||||
|
||||
# Lists installed apps
|
||||
if [[ "$command" = "ls-installed" ]]; then
|
||||
list_installed_apps
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ -z ${2+x} ]; then
|
||||
show_help
|
||||
exit 1
|
||||
else
|
||||
app="$2"
|
||||
|
||||
repo=$(cat "${USER_FILE}" 2> /dev/null | jq -r ".appOrigin.\"${app}\"" || true)
|
||||
repo_path=$("${UMBREL_ROOT}/scripts/repo" "path" "${repo}")
|
||||
app_repo_dir="${repo_path}/${app}"
|
||||
app_data_dir="${UMBREL_ROOT}/app-data/${app}"
|
||||
|
||||
app_hidden_service_file="${UMBREL_ROOT}/tor/data/app-${app}/hostname"
|
||||
|
||||
if [[ "${app}" == "installed" ]]; then
|
||||
for app in $(list_installed_apps); do
|
||||
if [[ "${app}" != "" ]]; then
|
||||
"${0}" "${1}" "${app}" "${@:3}" &
|
||||
fi
|
||||
done
|
||||
wait
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ -z "${app}" ]]; then
|
||||
>&2 echo "Error: \"${app}\" is not a valid app"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z ${3+x} ]; then
|
||||
args=""
|
||||
else
|
||||
args="${@:3}"
|
||||
fi
|
||||
|
||||
execute_hook() {
|
||||
local -r app="${1}"
|
||||
local -r name="${2}"
|
||||
|
||||
local -r app_hooks_dir="${UMBREL_ROOT}/app-data/${app}/hooks"
|
||||
local -r hook="${app_hooks_dir}/${name}"
|
||||
|
||||
if [[ -x "${hook}" ]]; then
|
||||
echo "Executing hook: ${hook}"
|
||||
# Swallow non-zero exit code
|
||||
"${hook}" || true
|
||||
fi
|
||||
}
|
||||
|
||||
compose() {
|
||||
local -r app="${1}"
|
||||
shift
|
||||
|
||||
# Source env.
|
||||
source_app "${app}"
|
||||
|
||||
# Define support compose files
|
||||
local -r app_proxy_compose_file="${UMBREL_ROOT}/scripts/support/docker-compose.app_proxy.yml"
|
||||
local -r tor_compose_file="${UMBREL_ROOT}/scripts/support/docker-compose.tor.yml"
|
||||
local -r common_compose_file="${UMBREL_ROOT}/scripts/support/docker-compose.common.yml"
|
||||
local -r app_compose_file="${app_data_dir}/docker-compose.yml"
|
||||
|
||||
local -r umbrel_env_file="${UMBREL_ROOT}/.env"
|
||||
|
||||
# We need to use the proxy compose file first
|
||||
# To allow vars. in the app's compose file to override variables
|
||||
compose_files=()
|
||||
|
||||
# Detect if the 'app_proxy' service has been defined
|
||||
# In the app's docker-compose file
|
||||
has_app_proxy_service=$(cat "${app_compose_file}" | yq ".services | has(\"${APP_PROXY_SERVICE_NAME}\")")
|
||||
|
||||
if [[ "${has_app_proxy_service}" == "true" ]]; then
|
||||
compose_files+=( "--file" "${app_proxy_compose_file}" )
|
||||
fi
|
||||
|
||||
# If remote Tor access is enabled
|
||||
# Then include a compose file for Tor
|
||||
if [[ "${REMOTE_TOR_ACCESS}" == "true" ]]; then
|
||||
compose_files+=( "--file" "${tor_compose_file}" )
|
||||
fi
|
||||
|
||||
# Add app's compose file last so that it can override
|
||||
# Any of the other compose files
|
||||
compose_files+=( "--file" "${common_compose_file}" )
|
||||
compose_files+=( "--file" "${app_compose_file}" )
|
||||
|
||||
# Merge compose files and args. passed into 'compose'
|
||||
compose_args=("${compose_files[@]}" "${@}")
|
||||
|
||||
docker-compose \
|
||||
--env-file "${umbrel_env_file}" \
|
||||
--project-name "${app}" \
|
||||
"${compose_args[@]}"
|
||||
}
|
||||
|
||||
update_installed_apps() {
|
||||
local -r action="${1}"
|
||||
local -r app="${2}"
|
||||
local -r repo="${3:-null}"
|
||||
|
||||
while ! (set -o noclobber; echo "$$" > "${USER_FILE}.lock") 2> /dev/null; do
|
||||
echo "Waiting for JSON lock to be released for ${app} update..."
|
||||
sleep 1
|
||||
done
|
||||
# This will cause the lock-file to be deleted in case of a
|
||||
# premature exit.
|
||||
trap "rm -f "${USER_FILE}.lock"; exit $?" INT TERM EXIT
|
||||
|
||||
[[ "${action}" == "add" ]] && operator="+" || operator="-"
|
||||
updated_json=$(cat "${USER_FILE}" | jq ".installedApps |= (. ${operator} [\"${app}\"] | unique)")
|
||||
echo "${updated_json}" > "${USER_FILE}"
|
||||
|
||||
if [[ "${action}" == "add" ]]; then
|
||||
updated_json=$(cat "${USER_FILE}" | jq ".appOrigin |= (. ${operator} {\"${app}\":\"${repo}\"})")
|
||||
else
|
||||
updated_json=$(cat "${USER_FILE}" | jq "del(.appOrigin.\"${app}\")")
|
||||
fi
|
||||
echo "${updated_json}" > "${USER_FILE}"
|
||||
|
||||
rm -f "${USER_FILE}.lock"
|
||||
}
|
||||
|
||||
template_app() {
|
||||
local -r app="${1}"
|
||||
|
||||
# Loop over all templates within app and populate them
|
||||
APP_TEMPLATE_FILES="${app_data_dir}/*.template"
|
||||
|
||||
shopt -s nullglob
|
||||
for APP_TEMPLATE_INPUT_FILE in $APP_TEMPLATE_FILES; do
|
||||
# Output filename is the same as input with .template stripped off
|
||||
APP_TEMPLATE_OUTPUT_FILE="${APP_TEMPLATE_INPUT_FILE%.*}"
|
||||
|
||||
# First we'll copy the file so we ensure the output
|
||||
# has the same fs permissions as the input
|
||||
cp --archive "${APP_TEMPLATE_INPUT_FILE}" "${APP_TEMPLATE_OUTPUT_FILE}"
|
||||
cat "${APP_TEMPLATE_INPUT_FILE}" | envsubst > "${APP_TEMPLATE_OUTPUT_FILE}"
|
||||
done
|
||||
}
|
||||
|
||||
copy_app_files() {
|
||||
local -r files_to_copy="${1}"
|
||||
|
||||
for filename in $files_to_copy; do
|
||||
APP_FILES="${app_repo_dir}/${filename}"
|
||||
|
||||
for app_file in $APP_FILES; do
|
||||
if [[ -f "${app_file}" ]] || [[ -d "${app_file}" ]]; then
|
||||
cp --archive "${app_file}" "${app_data_dir}"
|
||||
fi
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_tor_hs() {
|
||||
local -r app="${1}"
|
||||
|
||||
# Check if the app's hidden service hostname
|
||||
# Has been already generated and exit early
|
||||
if [[ -f "${app_hidden_service_file}" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Check that the app has the App Proxy service defined
|
||||
local -r app_compose_file="${app_data_dir}/docker-compose.yml"
|
||||
has_app_proxy_service=$(cat "${app_compose_file}" | yq ".services | has(\"${APP_PROXY_SERVICE_NAME}\")")
|
||||
|
||||
if [[ "${has_app_proxy_service}" == "false" ]]; then
|
||||
echo
|
||||
>&2 echo "Warning: \"${app}\" has no '${APP_PROXY_SERVICE_NAME}' defined"
|
||||
>&2 echo " \"${app}\" needs this to generate Tor HS"
|
||||
echo
|
||||
return
|
||||
fi
|
||||
|
||||
# If a tor service will start
|
||||
# and there is no existing tor hs hostname
|
||||
# Let's allow 10 seconds to generate it and then start the app
|
||||
if [[ "${REMOTE_TOR_ACCESS}" == "true" ]]; then
|
||||
echo "Generating hidden services for ${app}..."
|
||||
# We must first start the App Proxy
|
||||
# So that it's hostname is resolvable by Tor
|
||||
# More details here: https://github.com/torproject/tor/blob/01bda6c23f58947ad1e20ea6367a5c260f53dfab/src/feature/hs/hs_common.c#L743
|
||||
# And here: https://github.com/torproject/tor/blob/22552ad88e1e95ef9d2c6655c7602b7b25836075/src/lib/net/resolve.c#L297
|
||||
# Otherwise Tor will throw this error:
|
||||
# Unparseable address in hidden service port configuration.
|
||||
compose "${app}" up --detach app_proxy
|
||||
compose "${app}" up --detach tor_server
|
||||
|
||||
for attempt in $(seq 1 100); do
|
||||
if [[ -f "${app_hidden_service_file}" ]]; then
|
||||
echo "Hidden service file created successfully!"
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if [[ ! -f "${app_hidden_service_file}" ]]; then
|
||||
echo "Hidden service file wasn't created"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
start_app() {
|
||||
local -r app="${1}"
|
||||
|
||||
# Source env.
|
||||
source_app "${app}"
|
||||
|
||||
# Now apply templates
|
||||
template_app "${app}"
|
||||
|
||||
# Wait for Tor's HS hostname to exist
|
||||
wait_for_tor_hs "${app}"
|
||||
|
||||
execute_hook "${app}" "pre-start"
|
||||
|
||||
# Start all the app's containers
|
||||
compose "${app}" up --detach
|
||||
|
||||
execute_hook "${app}" "post-start"
|
||||
}
|
||||
|
||||
# Check that the app is installed
|
||||
must_be_installed_guard() {
|
||||
if ! list_installed_apps | grep --quiet "^${app}$"; then
|
||||
>&2 echo "Error: app \"${app}\" is not installed yet"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Pulls down images for an app and starts it
|
||||
if [[ "$command" = "install" ]]; then
|
||||
|
||||
repo=$("${UMBREL_ROOT}/scripts/repo" "locate" "${app}")
|
||||
|
||||
if [[ -z "${repo}" ]]; then
|
||||
>&2 echo "Error: \"${app}\" not found in any local app repo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
app_repo_dir=$("${UMBREL_ROOT}/scripts/repo" "path" "${repo}")
|
||||
app_repo_dir="${app_repo_dir}/${app}"
|
||||
|
||||
echo "Installing '${app}' from: ${repo}"
|
||||
|
||||
echo "Setting up data dir for app ${app}..."
|
||||
mkdir -p "${app_data_dir}"
|
||||
|
||||
# Copy all app files
|
||||
rsync --archive --verbose --exclude ".gitkeep" "${app_repo_dir}/." "${app_data_dir}"
|
||||
|
||||
execute_hook "${app}" "pre-install"
|
||||
|
||||
# Source env.
|
||||
source_app "${app}"
|
||||
|
||||
# Now apply templates
|
||||
template_app "${app}"
|
||||
|
||||
echo "Pulling images for app ${app}..."
|
||||
compose "${app}" pull
|
||||
|
||||
if [[ "$*" != *"--skip-start"* ]]; then
|
||||
echo "Starting app ${app}..."
|
||||
start_app "${app}"
|
||||
fi
|
||||
|
||||
echo "Saving app ${app} in DB..."
|
||||
update_installed_apps add "${app}" "${repo}"
|
||||
|
||||
execute_hook "${app}" "post-install"
|
||||
|
||||
echo "Successfully installed app ${app}"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Removes images and destroys all data for an app
|
||||
if [[ "$command" = "uninstall" ]]; then
|
||||
|
||||
must_be_installed_guard
|
||||
|
||||
execute_hook "${app}" "pre-uninstall"
|
||||
|
||||
# If a post uninstal hook exists
|
||||
# Then make a copy before it's deleted below
|
||||
app_hooks_dir="${UMBREL_ROOT}/app-data/${app}/hooks"
|
||||
post_uninstall_app_hook="${app_hooks_dir}/post-uninstall"
|
||||
if [[ -x "${post_uninstall_app_hook}" ]]; then
|
||||
temp_post_uninstall_app_hook="/tmp/${app}-post-uninstall"
|
||||
|
||||
cp --archive "${post_uninstall_app_hook}" "${temp_post_uninstall_app_hook}"
|
||||
|
||||
post_uninstall_app_hook="${temp_post_uninstall_app_hook}"
|
||||
else
|
||||
post_uninstall_app_hook=""
|
||||
fi
|
||||
|
||||
echo "Removing images for app ${app}..."
|
||||
compose "${app}" down --rmi all --remove-orphans
|
||||
|
||||
echo "Deleting app data for app ${app}..."
|
||||
if [[ -d "${app_data_dir}" ]]; then
|
||||
rm -rf "${app_data_dir}"
|
||||
fi
|
||||
|
||||
echo "Removing app ${app} from DB..."
|
||||
update_installed_apps remove "${app}"
|
||||
|
||||
if [[ ! -z "${post_uninstall_app_hook}" ]]; then
|
||||
"${post_uninstall_app_hook}" || true
|
||||
|
||||
rm -rf "${post_uninstall_app_hook}"
|
||||
fi
|
||||
|
||||
echo "Successfully uninstalled app ${app}"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Stops an installed app
|
||||
if [[ "$command" = "stop" ]]; then
|
||||
|
||||
must_be_installed_guard
|
||||
|
||||
execute_hook "${app}" "pre-stop"
|
||||
|
||||
echo "Stopping app ${app}..."
|
||||
compose "${app}" rm --force --stop
|
||||
|
||||
execute_hook "${app}" "post-stop"
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "$command" = "reinstall" ]]; then
|
||||
|
||||
"${0}" "uninstall" "${app}"
|
||||
|
||||
echo
|
||||
"${0}" "install" "${app}"
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# Starts an installed app
|
||||
if [[ "$command" = "start" ]]; then
|
||||
|
||||
must_be_installed_guard
|
||||
|
||||
echo "Starting app ${app}..."
|
||||
start_app "${app}"
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# Restarts an installed app
|
||||
if [[ "$command" = "restart" ]]; then
|
||||
|
||||
"${0}" "stop" "${app}"
|
||||
|
||||
"${0}" "start" "${app}"
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# Update an installed app
|
||||
if [[ "$command" = "update" ]]; then
|
||||
|
||||
must_be_installed_guard
|
||||
|
||||
# Check that the app folder still exists
|
||||
# Within the associated local app repo
|
||||
if [[ ! -d "${app_repo_dir}" ]]; then
|
||||
>&2 echo "Error: Local app repo no longer exists for ${app}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Updating '${app}' from: ${repo}"
|
||||
# Save current images to clean up later
|
||||
app_compose_file="${app_data_dir}/docker-compose.yml"
|
||||
app_old_images=$(yq e '.services | map(select(.image != null)) | .[].image' "${app_compose_file}")
|
||||
|
||||
if [[ "$*" != *"--skip-stop"* ]]; then
|
||||
"${0}" "stop" "${app}"
|
||||
fi
|
||||
|
||||
execute_hook "${app}" "pre-update"
|
||||
|
||||
# App updates will only copy files from this whitelist:
|
||||
UPDATE_FILES_WHITELIST_PRE="docker-compose.yml *.template exports.sh torrc hooks"
|
||||
|
||||
# We copy umbrel-app.yml after the app has started
|
||||
# That way the frontend knows the update has finished
|
||||
# And the app is running again
|
||||
UPDATE_FILES_WHITELIST_POST="umbrel-app.yml"
|
||||
|
||||
copy_app_files "${UPDATE_FILES_WHITELIST_PRE}"
|
||||
|
||||
# Ensure remaining files are copied in case of unexpected exit
|
||||
trap "copy_app_files "${UPDATE_FILES_WHITELIST_POST}"; exit $?" INT TERM EXIT
|
||||
|
||||
# Source env. after new exports.sh is copied (done above via 'copy_app_files')
|
||||
source_app "${app}"
|
||||
|
||||
# Now apply templates
|
||||
template_app "${app}"
|
||||
|
||||
echo "Pulling images for app ${app}..."
|
||||
compose "${app}" pull
|
||||
|
||||
# Copy remaining files to mark update as complete
|
||||
copy_app_files "${UPDATE_FILES_WHITELIST_POST}"
|
||||
|
||||
if [[ "$*" != *"--skip-start"* ]]; then
|
||||
"${0}" "start" "${app}"
|
||||
# Remove any old images we don't need anymore
|
||||
docker rmi $app_old_images || true
|
||||
fi
|
||||
|
||||
execute_hook "${app}" "post-update"
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# Passes all arguments to docker-compose
|
||||
if [[ "$command" = "compose" ]]; then
|
||||
|
||||
compose "${app}" ${args}
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# If we get here it means no valid command was supplied
|
||||
# Show help and exit
|
||||
show_help
|
||||
exit 1
|
||||
@@ -1,14 +0,0 @@
|
||||
# Automatic Encrypted Backups
|
||||
|
||||
The backups are encrypted client side before being uploaded over Tor and are padded with random data. Backups are made immediately as soon as the channel state changes. However, Umbrel also makes decoy backups at random intervals to prevent timing-analysis attacks.
|
||||
|
||||
These features combined ensure that the backup server doesn't learn any sensitive information about the user's Umbrel.
|
||||
|
||||
- The IP address of user is hidden due to Tor.
|
||||
- User's channel data are encrypted client side with a key only known to the Umbrel device.
|
||||
- Random interval decoy backups ensure the server can't correlate backup activity with channel state changes on the Lightning network and correlate a backup ID with a channel pubkey.
|
||||
- Random padding obscures if the backup size has increased/decreased or remains unchanged due to it being a decoy.
|
||||
|
||||
Due to the key/id being deterministically derived from the Umbrel seed, all that's needed to fully recover an Umbrel is the mnemonic seed phrase. Upon recovery the device can automatically regenerate the same backup id/encryption key, request the latest backup from the backup server, decrypt it, and restore the user's settings and Lightning network channel data.
|
||||
|
||||
There is currently no way to disable backups or recover from them in the dashboard yet. Both of these features will be introduced in the coming updates.
|
||||
@@ -1,138 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)"
|
||||
BACKUP_ROOT="${UMBREL_ROOT}/.backup/$RANDOM"
|
||||
BACKUP_FOLDER_NAME="backup"
|
||||
BACKUP_FOLDER_PATH="${BACKUP_ROOT}/${BACKUP_FOLDER_NAME}"
|
||||
BACKUP_FILE="${BACKUP_ROOT}/backup.tar.gz.pgp"
|
||||
BACKUP_STATUS_FILE="${UMBREL_ROOT}/statuses/backup-status.json"
|
||||
|
||||
check_dependencies () {
|
||||
for cmd in "$@"; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "This script requires \"${cmd}\" to be installed"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
check_dependencies openssl tar gpg shuf curl
|
||||
|
||||
# Deterministically derives 128 bits of cryptographically secure entropy
|
||||
derive_entropy () {
|
||||
identifier="${1}"
|
||||
|
||||
# Attempt to get new app seed
|
||||
{
|
||||
seed_length=$(jq -r '.seed | length' "${UMBREL_ROOT}/app-data/lightning/data/lightning/state.json")
|
||||
if [[ "${seed_length}" = "24" ]]; then
|
||||
umbrel_seed=$(jq -r '.seed | join(" ")' "${UMBREL_ROOT}/app-data/lightning/data/lightning/state.json")
|
||||
else
|
||||
umbrel_seed=""
|
||||
fi
|
||||
} 2>/dev/null || true
|
||||
|
||||
# Else fall back to umbrel seed
|
||||
if [[ -z "$umbrel_seed" ]]; then
|
||||
umbrel_seed=$(cat "${UMBREL_ROOT}/db/umbrel-seed/seed" 2>/dev/null) || true
|
||||
fi
|
||||
|
||||
if [[ -z "$umbrel_seed" ]] || [[ -z "$identifier" ]]; then
|
||||
>&2 echo "Missing derivation parameter, this is unsafe, exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# We need `sed 's/^.* //'` to trim the "(stdin)= " prefix from some versions of openssl
|
||||
printf "%s" "${identifier}" | openssl dgst -sha256 -hmac "${umbrel_seed}" | sed 's/^.* //'
|
||||
}
|
||||
|
||||
[[ -f "${UMBREL_ROOT}/.env" ]] && source "${UMBREL_ROOT}/.env"
|
||||
BITCOIN_NETWORK=${BITCOIN_NETWORK:-mainnet}
|
||||
|
||||
echo "Deriving keys..."
|
||||
|
||||
backup_id=$(derive_entropy "umbrel_backup_id")
|
||||
encryption_key=$(derive_entropy "umbrel_backup_encryption_key")
|
||||
|
||||
echo "Creating backup..."
|
||||
|
||||
mkdir -p "${BACKUP_FOLDER_PATH}"
|
||||
|
||||
CHANNEL_BACKUP_FILE="${UMBREL_ROOT}/app-data/lightning/data/lnd/data/chain/bitcoin/${BITCOIN_NETWORK}/channel.backup"
|
||||
|
||||
if [[ -f "${CHANNEL_BACKUP_FILE}" ]]; then
|
||||
cp --archive "${CHANNEL_BACKUP_FILE}" "${BACKUP_FOLDER_PATH}/channel.backup"
|
||||
fi
|
||||
|
||||
# We want to back up user settings too, however we currently store the encrypted
|
||||
# mnemonic in this file which is not safe to backup remotely.
|
||||
# Uncomment this in the future once we've ensured there's no critical data in
|
||||
# this file.
|
||||
# cp --archive "${UMBREL_ROOT}/db/user.json" "${BACKUP_FOLDER_PATH}/user.json"
|
||||
|
||||
echo "Adding random padding..."
|
||||
|
||||
# Up to 10KB of random binary data
|
||||
# This prevents the server from being able to tell if the backup has increased
|
||||
# decreased or stayed the sme size. Combined with random interval decoy backups
|
||||
# this makes a (already very difficult) timing analysis attack to correlate backup
|
||||
# activity with channel state changes practically impossible.
|
||||
padding="$(shuf -i 0-10240 -n 1)"
|
||||
dd if=/dev/urandom bs="${padding}" count=1 > "${BACKUP_FOLDER_PATH}/.padding"
|
||||
|
||||
echo "Creating encrypted tarball..."
|
||||
|
||||
tar \
|
||||
--create \
|
||||
--gzip \
|
||||
--verbose \
|
||||
--directory "${BACKUP_FOLDER_PATH}/.." \
|
||||
"${BACKUP_FOLDER_NAME}" \
|
||||
| gpg \
|
||||
--batch \
|
||||
--symmetric \
|
||||
--cipher-algo AES256 \
|
||||
--passphrase "${encryption_key}" \
|
||||
--output "${BACKUP_FILE}"
|
||||
|
||||
# To decrypt:
|
||||
# cat "${BACKUP_FILE}" | gpg \
|
||||
# --batch \
|
||||
# --decrypt \
|
||||
# --passphrase "${encryption_key}" \
|
||||
# | tar \
|
||||
# --extract \
|
||||
# --verbose \
|
||||
# --gzip
|
||||
|
||||
BACKUP_API_URL="https://pvf3ozmmfl.execute-api.us-east-1.amazonaws.com/prod/v1/upload"
|
||||
|
||||
if [[ $BITCOIN_NETWORK == "testnet" ]]; then
|
||||
BACKUP_API_URL="https://as0ot0lg7h.execute-api.us-east-1.amazonaws.com/dev/v1/upload"
|
||||
fi
|
||||
if [[ $BITCOIN_NETWORK == "regtest" ]]; then
|
||||
BACKUP_API_URL="https://5fxwqbum7g.execute-api.us-east-1.amazonaws.com/dev/v1/upload"
|
||||
fi
|
||||
|
||||
echo "Uploading backup..."
|
||||
if curl --socks5 "localhost:${TOR_PROXY_PORT}" -F "file=@/${BACKUP_FILE}" "${BACKUP_API_URL}/${backup_id}"; then
|
||||
status="success"
|
||||
else
|
||||
status="failed"
|
||||
fi
|
||||
echo
|
||||
|
||||
rm -rf "${BACKUP_ROOT}"
|
||||
|
||||
# Update status file
|
||||
cat <<EOF > ${BACKUP_STATUS_FILE}
|
||||
{"status": "${status}", "timestamp": $(date +%s000)}
|
||||
EOF
|
||||
|
||||
echo "============================="
|
||||
echo "====== Backup ${status} ======="
|
||||
echo "============================="
|
||||
|
||||
exit 0
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)"
|
||||
MAX_BACKUP_INTERVAL_IN_HOURS="12"
|
||||
|
||||
check_if_not_already_running() {
|
||||
if ps ax | grep $0 | grep -v $$ | grep bash | grep -v grep
|
||||
then
|
||||
echo "decoy trigger is already running"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_dependencies () {
|
||||
for cmd in "$@"; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "This script requires \"${cmd}\" to be installed"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
check_if_not_already_running
|
||||
|
||||
check_dependencies shuf
|
||||
|
||||
main () {
|
||||
while true; do
|
||||
minutes_in_seconds="60"
|
||||
hours_in_seconds="$((60 * ${minutes_in_seconds}))"
|
||||
max_interval="$((${MAX_BACKUP_INTERVAL_IN_HOURS} * ${hours_in_seconds}))"
|
||||
delay="$(shuf -i 0-${max_interval} -n 1)"
|
||||
echo "Sleeping for ${delay} seconds..."
|
||||
sleep $delay
|
||||
echo "Triggering decoy backup..."
|
||||
touch "${UMBREL_ROOT}/events/signals/backup"
|
||||
done
|
||||
}
|
||||
|
||||
main
|
||||
@@ -1,69 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
check_root () {
|
||||
if [[ $UID != 0 ]]; then
|
||||
echo "Error: This script must be run as root."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_if_not_already_running() {
|
||||
if ps ax | grep $0 | grep -v $$ | grep bash | grep -v grep
|
||||
then
|
||||
echo "backup monitor is already running"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_dependencies () {
|
||||
for cmd in "$@"; do
|
||||
if ! command -v $cmd >/dev/null 2>&1; then
|
||||
echo "This script requires \"${cmd}\" to be installed"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
check_root
|
||||
|
||||
check_if_not_already_running
|
||||
|
||||
check_dependencies fswatch readlink dirname
|
||||
|
||||
UMBREL_ROOT="$(dirname $(readlink -f "${BASH_SOURCE[0]}"))/../.."
|
||||
|
||||
monitor_file () {
|
||||
local file_path="${1}"
|
||||
echo "Monitoring $file_path"
|
||||
echo
|
||||
|
||||
if [[ ! -e "${file_path}" ]]; then
|
||||
echo "$file_path doesn't exist, waiting for it to be created..."
|
||||
echo
|
||||
until [[ -e "${file_path}" ]]; do
|
||||
sleep 1
|
||||
done
|
||||
echo "$file_path created! Triggering backup..."
|
||||
touch "${UMBREL_ROOT}/events/signals/backup"
|
||||
fi
|
||||
|
||||
fswatch -0 --event Updated $file_path | xargs -0 -n 1 -I {} touch "${UMBREL_ROOT}/events/signals/backup"
|
||||
}
|
||||
|
||||
if [[ ! -d "${UMBREL_ROOT}" ]]; then
|
||||
echo "Root dir does not exist '$UMBREL_ROOT'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[[ -f "${UMBREL_ROOT}/.env" ]] && source "${UMBREL_ROOT}/.env"
|
||||
BITCOIN_NETWORK=${BITCOIN_NETWORK:-mainnet}
|
||||
|
||||
# Monitor LND channel.backup
|
||||
monitor_file "${UMBREL_ROOT}/app-data/lightning/data/lnd/data/chain/bitcoin/${BITCOIN_NETWORK}/channel.backup" &
|
||||
|
||||
# Monitor db/user.json
|
||||
# We want to back up user settings too, however we currently store the encrypted
|
||||
# mnemonic in this file which is not safe to backup remotely.
|
||||
# Uncomment this in the future once we've ensured there's no critical data in
|
||||
# this file.
|
||||
# monitor_file "${UMBREL_ROOT}/db/user.json" &
|
||||
Vendored
-227
@@ -1,227 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
##########################################################
|
||||
################## Check dependencies ####################
|
||||
##########################################################
|
||||
|
||||
check_dependencies () {
|
||||
for cmd in "$@"; do
|
||||
if ! command -v $cmd >/dev/null 2>&1; then
|
||||
echo "This script requires \"${cmd}\" to be installed"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
if [ ! "$(uname -s)" == "Linux" ]; then
|
||||
echo "Sorry, Umbrel only supports Linux-based systems at this point."
|
||||
echo
|
||||
echo "You may work around this by modifying the configuration script yourself, but it's highly experimental."
|
||||
echo "If you get it working, we hope you consider making a PR. :)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_dependencies docker docker-compose dirname readlink
|
||||
|
||||
# Switch to Umbrel's root directory
|
||||
UMBREL_ROOT="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")"/..)"
|
||||
if [[ ! -d "$UMBREL_ROOT" ]]; then
|
||||
echo "Root dir does not exist '$UMBREL_ROOT'"
|
||||
exit 1
|
||||
fi
|
||||
cd "$UMBREL_ROOT"
|
||||
|
||||
# Make sure we use the status dir from the real Umbrel installation if this is
|
||||
# an OTA update.
|
||||
STATUS_DIR="${UMBREL_ROOT}/statuses"
|
||||
if [[ -f "${UMBREL_ROOT}/../.umbrel" ]]; then
|
||||
STATUS_DIR="${UMBREL_ROOT}/../statuses"
|
||||
fi
|
||||
|
||||
# Configure for mainnet or testnet or regtest depending
|
||||
# upon the user-supplied value of $NETWORK
|
||||
BITCOIN_NETWORK="${NETWORK:-mainnet}"
|
||||
|
||||
if [ "$BITCOIN_NETWORK" != "mainnet" ] && [ "$BITCOIN_NETWORK" != "testnet" ] && [ "$BITCOIN_NETWORK" != "regtest" ]; then
|
||||
echo "Error: Umbrel can only be configured for mainnet (default), testnet or regtest"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "======================================"
|
||||
if [[ -f "${STATUS_DIR}/configured" ]]; then
|
||||
echo "=========== RECONFIGURING ============"
|
||||
else
|
||||
echo "============ CONFIGURING ============="
|
||||
fi
|
||||
echo "=============== UMBREL ==============="
|
||||
echo "======================================"
|
||||
echo
|
||||
|
||||
|
||||
##########################################################
|
||||
############### Setup configuration files ###############
|
||||
##########################################################
|
||||
|
||||
# Store paths to intermediary config files
|
||||
NGINX_CONF_FILE="./templates/nginx.conf"
|
||||
TOR_PROXY_CONF_FILE="./templates/torrc-proxy"
|
||||
TOR_SERVER_CONF_FILE="./templates/torrc-server"
|
||||
ENV_FILE="./templates/.env"
|
||||
|
||||
# Remove intermediary files if they exist from any
|
||||
# previous unclean configuration run
|
||||
[[ -f "$NGINX_CONF_FILE" ]] && rm -f "$NGINX_CONF_FILE"
|
||||
[[ -f "$TOR_PROXY_CONF_FILE" ]] && rm -f "$TOR_PROXY_CONF_FILE"
|
||||
[[ -f "$TOR_SERVER_CONF_FILE" ]] && rm -f "$TOR_SERVER_CONF_FILE"
|
||||
[[ -f "$ENV_FILE" ]] && rm -f "$ENV_FILE"
|
||||
|
||||
# Copy template configs to intermediary configs
|
||||
[[ -f "./templates/nginx-sample.conf" ]] && cp "./templates/nginx-sample.conf" "$NGINX_CONF_FILE"
|
||||
[[ -f "./templates/torrc-proxy-sample" ]] && cp "./templates/torrc-proxy-sample" "$TOR_PROXY_CONF_FILE"
|
||||
[[ -f "./templates/torrc-server-sample" ]] && cp "./templates/torrc-server-sample" "$TOR_SERVER_CONF_FILE"
|
||||
[[ -f "./templates/.env-sample" ]] && cp "./templates/.env-sample" "$ENV_FILE"
|
||||
|
||||
|
||||
##########################################################
|
||||
############ Generate configuration variables ############
|
||||
##########################################################
|
||||
|
||||
# Load existing credentials if we have some
|
||||
[[ -f "./.env" ]] && source "./.env"
|
||||
[[ ! -z ${PREV_ENV_FILE+x} ]] && [[ -f "${PREV_ENV_FILE}" ]] && source "${PREV_ENV_FILE}"
|
||||
|
||||
# Define Umbrel Networking
|
||||
NETWORK_IP="10.21.0.0"
|
||||
GATEWAY_IP="10.21.0.1"
|
||||
NGINX_IP="10.21.21.2"
|
||||
NGINX_PORT="${NGINX_PORT:-80}"
|
||||
DASHBOARD_IP="10.21.21.3"
|
||||
MANAGER_IP="10.21.21.4"
|
||||
AUTH_IP="10.21.21.6"
|
||||
AUTH_PORT="2000"
|
||||
TOR_PROXY_IP="10.21.21.11"
|
||||
TOR_PROXY_PORT="9050"
|
||||
|
||||
# App specific state
|
||||
BITCOIN_IP="10.21.21.8"
|
||||
BITCOIN_RPC_PORT="8332"
|
||||
BITCOIN_P2P_PORT="8333"
|
||||
BITCOIN_ZMQ_RAWBLOCK_PORT="28332"
|
||||
BITCOIN_ZMQ_RAWTX_PORT="28333"
|
||||
BITCOIN_ZMQ_HASHBLOCK_PORT="28334"
|
||||
BITCOIN_ZMQ_SEQUENCE_PORT="28335"
|
||||
LND_IP="10.21.21.9"
|
||||
LND_GRPC_PORT="10009"
|
||||
LND_REST_PORT="8080"
|
||||
ELECTRUM_IP="10.21.21.10"
|
||||
ELECTRUM_PORT="50001"
|
||||
|
||||
if [[ -z ${UMBREL_AUTH_SECRET+x} ]]; then
|
||||
UMBREL_AUTH_SECRET="$(openssl rand -hex 32)"
|
||||
fi
|
||||
|
||||
# Generate RPC credentials
|
||||
if [[ -z ${BITCOIN_RPC_USER+x} ]] || [[ -z ${BITCOIN_RPC_PASS+x} ]] || [[ -z ${BITCOIN_RPC_AUTH+x} ]]; then
|
||||
echo "Generating auth credentials"
|
||||
echo
|
||||
BITCOIN_RPC_USER="umbrel"
|
||||
BITCOIN_RPC_DETAILS=$("./scripts/rpcauth.py" "$BITCOIN_RPC_USER")
|
||||
BITCOIN_RPC_PASS=$(echo "$BITCOIN_RPC_DETAILS" | tail -1)
|
||||
BITCOIN_RPC_AUTH=$(echo "$BITCOIN_RPC_DETAILS" | head -2 | tail -1 | sed -e "s/^rpcauth=//")
|
||||
fi
|
||||
|
||||
# Pull Tor image and generate Tor password
|
||||
if [[ -z ${TOR_PASSWORD+x} ]] || [[ -z ${TOR_HASHED_PASSWORD+x} ]]; then
|
||||
echo "Generating Tor password"
|
||||
echo
|
||||
TOR_PASSWORD=$("./scripts/rpcauth.py" "itdoesntmatter" | tail -1)
|
||||
TOR_HASHED_PASSWORD=$(docker run --rm getumbrel/tor:0.4.7.8@sha256:2ace83f22501f58857fa9b403009f595137fa2e7986c4fda79d82a8119072b6a --quiet --hash-password "$TOR_PASSWORD")
|
||||
fi
|
||||
|
||||
##########################################################
|
||||
### Update config files with configuration variables #####
|
||||
##########################################################
|
||||
|
||||
# Update RPC, P2P and ZMQ Ports
|
||||
sed -i "s/BITCOIN_RPC_PORT=<port>/BITCOIN_RPC_PORT=$BITCOIN_RPC_PORT/g;" "$ENV_FILE"
|
||||
sed -i "s/BITCOIN_P2P_PORT=<port>/BITCOIN_P2P_PORT=$BITCOIN_P2P_PORT/g;" "$ENV_FILE"
|
||||
|
||||
# Add rpcauth
|
||||
sed -i "s/BITCOIN_RPC_AUTH=<rpcauth>/BITCOIN_RPC_AUTH='$BITCOIN_RPC_AUTH'/g;" "$ENV_FILE"
|
||||
|
||||
# Add RPC credentials to env file
|
||||
sed -i "s/BITCOIN_RPC_USER=<username>/BITCOIN_RPC_USER=$BITCOIN_RPC_USER/g;" "$ENV_FILE"
|
||||
sed -i "s/BITCOIN_RPC_PASS=<password>/BITCOIN_RPC_PASS=$BITCOIN_RPC_PASS/g;" "$ENV_FILE"
|
||||
|
||||
# Add chain to env file
|
||||
sed -i "s/BITCOIN_NETWORK=<network>/BITCOIN_NETWORK=$BITCOIN_NETWORK/g;" "$ENV_FILE"
|
||||
|
||||
# Add Tor password
|
||||
sed -i "s/HashedControlPassword <password>/HashedControlPassword $TOR_HASHED_PASSWORD/g;" "$TOR_PROXY_CONF_FILE"
|
||||
sed -i "s/TOR_PASSWORD=<password>/TOR_PASSWORD=$TOR_PASSWORD/g;" "$ENV_FILE"
|
||||
sed -i "s/TOR_HASHED_PASSWORD=<password>/TOR_HASHED_PASSWORD=$TOR_HASHED_PASSWORD/g;" "$ENV_FILE"
|
||||
|
||||
# Store docker binary path
|
||||
DOCKER_BINARY=$(readlink -f "$(which docker)")
|
||||
sed -i "s#DOCKER_BINARY=<path>#DOCKER_BINARY=$DOCKER_BINARY#g;" "$ENV_FILE"
|
||||
|
||||
# TODO: Update all the above code to use this simpler logic
|
||||
for template in "${NGINX_CONF_FILE}" "${TOR_PROXY_CONF_FILE}" "${TOR_SERVER_CONF_FILE}" "${ENV_FILE}"; do
|
||||
# Umbrel
|
||||
sed -i "s/<network-ip>/${NETWORK_IP}/g" "${template}"
|
||||
sed -i "s/<gateway-ip>/${GATEWAY_IP}/g" "${template}"
|
||||
sed -i "s/<nginx-ip>/${NGINX_IP}/g" "${template}"
|
||||
sed -i "s/<nginx-port>/${NGINX_PORT}/g" "${template}"
|
||||
sed -i "s/<dashboard-ip>/${DASHBOARD_IP}/g" "${template}"
|
||||
sed -i "s/<manager-ip>/${MANAGER_IP}/g" "${template}"
|
||||
sed -i "s/<auth-ip>/${AUTH_IP}/g" "${template}"
|
||||
sed -i "s/<auth-port>/${AUTH_PORT}/g" "${template}"
|
||||
sed -i "s/<umbrel-auth-secret>/${UMBREL_AUTH_SECRET}/g" "${template}"
|
||||
|
||||
# App specific state
|
||||
sed -i "s/<bitcoin-ip>/${BITCOIN_IP}/g" "${template}"
|
||||
sed -i "s/<bitcoin-p2p-port>/${BITCOIN_P2P_PORT}/g" "${template}"
|
||||
sed -i "s/<bitcoin-rpc-port>/$BITCOIN_RPC_PORT/g" "${template}"
|
||||
sed -i "s/<lnd-ip>/${LND_IP}/g" "${template}"
|
||||
sed -i "s/<lnd-grpc-port>/${LND_GRPC_PORT}/g" "${template}"
|
||||
sed -i "s/<lnd-rest-port>/${LND_REST_PORT}/g" "${template}"
|
||||
sed -i "s/<electrum-ip>/${ELECTRUM_IP}/g" "${template}"
|
||||
sed -i "s/<electrum-port>/${ELECTRUM_PORT}/g" "${template}"
|
||||
sed -i "s/<tor-proxy-ip>/${TOR_PROXY_IP}/g" "${template}"
|
||||
sed -i "s/<tor-proxy-port>/${TOR_PROXY_PORT}/g" "${template}"
|
||||
sed -i "s/<zmq-rawblock-port>/${BITCOIN_ZMQ_RAWBLOCK_PORT}/g;" "${template}"
|
||||
sed -i "s/<zmq-rawtx-port>/${BITCOIN_ZMQ_RAWTX_PORT}/g;" "${template}"
|
||||
sed -i "s/<zmq-hashblock-port>/${BITCOIN_ZMQ_HASHBLOCK_PORT}/g;" "${template}"
|
||||
sed -i "s/<zmq-sequence-port>/${BITCOIN_ZMQ_SEQUENCE_PORT}/g;" "${template}"
|
||||
done
|
||||
|
||||
##########################################################
|
||||
############## Override main config files ################
|
||||
##########################################################
|
||||
|
||||
mv -f "$NGINX_CONF_FILE" "./nginx/nginx.conf"
|
||||
mv -f "$TOR_PROXY_CONF_FILE" "./tor/torrc-proxy"
|
||||
mv -f "$TOR_SERVER_CONF_FILE" "./tor/torrc-server"
|
||||
mv -f "$ENV_FILE" "./.env"
|
||||
|
||||
##########################################################
|
||||
################ Configuration complete ##################
|
||||
##########################################################
|
||||
|
||||
echo "Configuring permissions..."
|
||||
echo
|
||||
|
||||
UMBREL_DEV_FILE="${UMBREL_ROOT}/.umbrel-dev"
|
||||
if [[ ! -f "${UMBREL_DEV_FILE}" ]]; then
|
||||
find "$UMBREL_ROOT" -path "$UMBREL_ROOT/app-data" -prune -o -exec chown 1000:1000 {} + || true
|
||||
fi
|
||||
|
||||
# Create configured status
|
||||
touch "${STATUS_DIR}/configured"
|
||||
|
||||
echo "Configuration successful"
|
||||
echo "You can now start Umbrel by running:"
|
||||
echo " sudo ./scripts/start"
|
||||
echo
|
||||
@@ -1,5 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script is exposed via: curl -sL https://export.umbrel.sh | sudo bash
|
||||
# Which resolves to this file at the 1.1.1 tag in this repo
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# This script will:
|
||||
|
||||
-190
@@ -1,190 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script is based on raspinfo: https://github.com/raspberrypi/utils/blob/master/raspinfo/raspinfo
|
||||
# raspinfo is Copyright: 2018-2019, Raspberry Pi (Trading) Ltd.
|
||||
# This fork is Copyright 2020-2021, Umbrel
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. Neither the name of the University nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE HOLDERS OR
|
||||
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
# Try to load Umbrel OS version info to detect Umbrel OS
|
||||
[[ -f "/etc/default/umbrel" ]] && source "/etc/default/umbrel"
|
||||
|
||||
echo "====================="
|
||||
echo "= Umbrel debug info ="
|
||||
echo "====================="
|
||||
|
||||
UMBREL_ROOT="$(dirname $(readlink -f "${BASH_SOURCE[0]}"))/.."
|
||||
cd "${UMBREL_ROOT}"
|
||||
|
||||
echo
|
||||
echo "Umbrel version"
|
||||
echo "--------------"
|
||||
cat info.json | jq .version | sed 's/"//g'
|
||||
|
||||
if [[ ! -z "${UMBREL_OS:-}" ]]; then
|
||||
echo
|
||||
echo "Flashed OS version"
|
||||
echo "-----------------"
|
||||
echo $UMBREL_OS
|
||||
|
||||
echo
|
||||
echo "Raspberry Pi Model"
|
||||
echo "------------------"
|
||||
|
||||
cat /proc/cpuinfo | tail -3
|
||||
|
||||
echo
|
||||
echo "Firmware"
|
||||
echo "--------"
|
||||
|
||||
vcgencmd version
|
||||
|
||||
echo
|
||||
echo "Temperature"
|
||||
echo "-----------"
|
||||
|
||||
vcgencmd measure_temp
|
||||
|
||||
echo
|
||||
echo "Throttling"
|
||||
echo "----------"
|
||||
|
||||
vcgencmd get_throttled
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Memory usage"
|
||||
echo "------------"
|
||||
free --human --mega
|
||||
echo
|
||||
"${UMBREL_ROOT}/scripts/memory-usage"
|
||||
|
||||
echo
|
||||
echo "Memory monitor logs"
|
||||
echo "-------------------"
|
||||
tail -n 10 "${UMBREL_ROOT}/logs/memory-monitor.log"
|
||||
|
||||
echo
|
||||
echo "Filesystem information"
|
||||
echo "----------------------"
|
||||
|
||||
df --human-readable / "${UMBREL_ROOT}"
|
||||
|
||||
if [[ ! -z "${UMBREL_OS:-}" ]]; then
|
||||
echo
|
||||
echo "Startup service logs"
|
||||
echo "--------------------"
|
||||
journalctl --unit=umbrel-startup.service | tail -n 30 | sed '/onion/d'
|
||||
|
||||
echo
|
||||
echo "External storage service logs"
|
||||
echo "-----------------------------"
|
||||
journalctl --unit=umbrel-external-storage.service | tail -n 30
|
||||
|
||||
echo
|
||||
echo "External storage SD card update service logs"
|
||||
echo "--------------------------------------------"
|
||||
journalctl --unit=umbrel-external-storage-sdcard-update.service | tail -n 30
|
||||
fi
|
||||
|
||||
|
||||
echo
|
||||
echo "Karen logs"
|
||||
echo "----------"
|
||||
echo
|
||||
|
||||
tail -n 50 logs/karen.log | sed '/onion/d'
|
||||
|
||||
echo
|
||||
echo "Docker containers"
|
||||
echo "-----------------"
|
||||
|
||||
docker ps --format="table {{.Names}}\t{{.Status}}"
|
||||
|
||||
echo
|
||||
echo "Umbrel logs"
|
||||
echo "-----------"
|
||||
echo
|
||||
docker-compose logs --tail=30 manager
|
||||
|
||||
echo
|
||||
echo "Tor Proxy logs"
|
||||
echo "--------"
|
||||
echo
|
||||
docker-compose logs --tail=10 tor_proxy
|
||||
|
||||
installed_apps=$(./scripts/app ls-installed)
|
||||
if [[ ! -z "${installed_apps:-}" ]]; then
|
||||
echo
|
||||
echo "App logs"
|
||||
echo "--------"
|
||||
for app in $installed_apps; do
|
||||
echo
|
||||
echo "${app}"
|
||||
echo
|
||||
# 10 lines from each container
|
||||
./scripts/app compose $app logs --tail 10 | grep -v 'tor_server_1 '
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ ! -z "${UMBREL_OS:-}" ]]; then
|
||||
list_block_devices () {
|
||||
# Documented in the mount script for Umbrel OS
|
||||
sync
|
||||
(ls -d /sys/block/sd* 2>/dev/null || true) | sed 's!.*/!!'
|
||||
}
|
||||
no_of_block_devices=$(list_block_devices | wc -l)
|
||||
fi
|
||||
|
||||
echo "================"
|
||||
echo "==== Result ===="
|
||||
echo "================"
|
||||
if [[ ! -z "${UMBREL_OS:-}" ]]; then
|
||||
if [[ $(vcgencmd get_throttled) != "throttled=0x0" ]]; then
|
||||
if [[ $(vcgencmd get_throttled) = "throttled=0x5"* ]]; then
|
||||
echo "There is probably issue with your power supply. We recommend using the official power supply to avoid this issue"
|
||||
else
|
||||
echo "The temperature of your Raspberry Pi is too high. We recommend either using a case that lowers the temperature of your Pi or a case with a builtin fan."
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
if [[ $no_of_block_devices -lt 1 ]]; then
|
||||
echo "You have either no external drive connected to your Raspberry Pi or the drive is not being detected."
|
||||
echo "Please shutdown your Raspberry Pi ('sudo shutdown'), then plug the SSD into the other USB3 port."
|
||||
echo "After you've finished that, boot your Raspberry Pi back up. If it still still doesn't work,"
|
||||
echo "you can contact us on Telegram (t.me/getumbrel) and share the output of this script."
|
||||
exit 0
|
||||
fi
|
||||
if [[ $no_of_block_devices -gt 1 ]]; then
|
||||
echo "================"
|
||||
echo "==== Result ===="
|
||||
echo "================"
|
||||
echo "You have multiple external drives connected to your Raspberry Pi. Please remove the drive(s) you don't want to use for Umbrel."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "==== END ====="
|
||||
@@ -1,6 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# This script is exposed via: curl -L https://umbrel.sh | bash
|
||||
# Which resolves to this file in the master branch of this repo
|
||||
|
||||
cat << "EOF"
|
||||
|
||||
,;###GGGGGGGGGGl#Sp
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
|
||||
|
||||
PERCENT_MEMORY_THRESHOLD=90
|
||||
|
||||
check_if_not_already_running() {
|
||||
if ps ax | grep $0 | grep -v $$ | grep bash | grep -v grep
|
||||
then
|
||||
echo "Memory monitor is already running"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
log () {
|
||||
echo "$(date "+%Y-%m-%d %H:%M:%S") ${@}"
|
||||
}
|
||||
|
||||
check_if_not_already_running
|
||||
log "Memory monitor running!"
|
||||
|
||||
while true; do
|
||||
percent_memory_used=$(free | awk 'NR==2{print int($3*100/$2) }')
|
||||
if (( $percent_memory_used > $PERCENT_MEMORY_THRESHOLD )); then
|
||||
log "Warning memory usage at ${percent_memory_used}%"
|
||||
if [[ "$(jq -r '.installedApps | length > 0' ${UMBREL_ROOT}/db/user.json)" == "true" ]]; then
|
||||
touch "${UMBREL_ROOT}/statuses/memory-warning"
|
||||
fi
|
||||
fi
|
||||
sleep 60
|
||||
done
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
|
||||
|
||||
get_container_memory_use () {
|
||||
local container="${1}"
|
||||
|
||||
local container_memory=0
|
||||
|
||||
local container_pids=$(docker top "${container}" | tail -n +2 | awk '{print $2}')
|
||||
for pid in $container_pids; do
|
||||
local pid_memory=$(ps u "${pid}" | awk '{print $4}' | grep -v 'MEM')
|
||||
if [[ ! -z "${pid_memory}" ]]; then
|
||||
container_memory=$(awk "BEGIN {print ${container_memory}+${pid_memory}}")
|
||||
fi
|
||||
done
|
||||
|
||||
echo "${container_memory}"
|
||||
}
|
||||
|
||||
get_app_memory_use () {
|
||||
local app="${1}"
|
||||
|
||||
local app_memory=0
|
||||
|
||||
local app_containers=$("${UMBREL_ROOT}/scripts/app" compose "${app}" ps | awk '{print $1}' | grep -v 'Name\|-----')
|
||||
for container in $app_containers; do
|
||||
local container_memory=$(get_container_memory_use "${container}")
|
||||
app_memory=$(awk "BEGIN {print ${app_memory}+${container_memory}}")
|
||||
done
|
||||
|
||||
echo "${app_memory}"
|
||||
}
|
||||
|
||||
main() {
|
||||
local total_system_memory_usage=$(free | awk 'NR==2{print sprintf("%.1f", $3*100/$2) }')
|
||||
echo "total: ${total_system_memory_usage}%"
|
||||
|
||||
local total_container_memory=0
|
||||
|
||||
# Installed app memory use
|
||||
for app in $("${UMBREL_ROOT}/scripts/app" ls-installed); do
|
||||
local app_memory=$(get_app_memory_use "${app}")
|
||||
total_container_memory=$(awk "BEGIN {print ${total_container_memory}+${app_memory}}")
|
||||
echo "${app}: ${app_memory}%"
|
||||
done
|
||||
|
||||
# Misc system memory use
|
||||
local system_memory=$(awk "function pos(x){return(x<0?0:x)} BEGIN {print pos(${total_system_memory_usage}-${total_container_memory})}")
|
||||
echo "system: ${system_memory}%"
|
||||
}
|
||||
|
||||
main 2> /dev/null | sort --key 2 --numeric-sort --reverse
|
||||
-396
@@ -1,396 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="0.0.2"
|
||||
|
||||
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
|
||||
USER_FILE="${UMBREL_ROOT}/db/user.json"
|
||||
DEFAULT_UMBREL_APP_REPO_URL="https://github.com/getumbrel/umbrel-apps.git"
|
||||
|
||||
show_help() {
|
||||
cat << EOF
|
||||
CLI (v${VERSION}) for managing local Umbrel App Repos
|
||||
|
||||
Usage: repo <command> <arguments...>
|
||||
|
||||
Commands:
|
||||
add Add app repo
|
||||
remove Remove app repo
|
||||
update Update local app repos
|
||||
branch Set the branch for a given local app repo
|
||||
checkout Checkout an app repo (branch is optional) e.g. https://github.com/coolapp/umbrel-apps.git#staging
|
||||
path Get the absolute path for a given local app repo
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
check_dependencies () {
|
||||
for cmd in "$@"; do
|
||||
if ! command -v $cmd >/dev/null 2>&1; then
|
||||
echo "This script requires \"${cmd}\" to be installed"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Check dependencies
|
||||
check_dependencies git jq timeout
|
||||
|
||||
if [ -z ${1+x} ]; then
|
||||
command=""
|
||||
else
|
||||
command="$1"
|
||||
fi
|
||||
|
||||
USER_JSON=$(cat "${USER_FILE}" 2> /dev/null || echo "null")
|
||||
|
||||
function repo_slug() {
|
||||
echo "${1}" | sed 's/[^a-zA-Z0-9]/-/g'
|
||||
}
|
||||
|
||||
function repo_path() {
|
||||
path=$(repo_slug "${1}")
|
||||
echo "${UMBREL_ROOT}/repos/${path}"
|
||||
}
|
||||
|
||||
function list_repos() {
|
||||
cat "${USER_FILE}" 2> /dev/null | jq -r 'if has("repos") then .repos else [] end | join("\n")' || {
|
||||
echo "${DEFAULT_UMBREL_APP_REPO_URL}"
|
||||
}
|
||||
}
|
||||
|
||||
function parse_repo_url() {
|
||||
local -r descriptor="${1}"
|
||||
|
||||
# Return the string before the # character
|
||||
repo_url="${descriptor%%#*}"
|
||||
|
||||
# Check whether this looks like a url searching for '://' or '@'
|
||||
if [[ "${repo_url}" == *://* ]] || [[ "${repo_url}" == *@* ]]; then
|
||||
echo "${repo_url}"
|
||||
exit
|
||||
fi
|
||||
|
||||
# And if not, default to github.com via HTTPS
|
||||
echo "https://github.com/${repo_url}"
|
||||
}
|
||||
|
||||
function list_repo_ids() {
|
||||
local -r ignore_repo_url="${1}"
|
||||
|
||||
# Check if a repo already exists with this id
|
||||
all_repo_urls=$(list_repos)
|
||||
|
||||
# Through existing repos
|
||||
for repo_url in $all_repo_urls; do
|
||||
# Ignore a given repo url
|
||||
if [[ "${repo_url}" != "${ignore_repo_url}" ]]; then
|
||||
local_repo_path=$(repo_path "${repo_url}")
|
||||
repo_manifest_file="${local_repo_path}/umbrel-app-store.yml"
|
||||
|
||||
# Check a manifest file exists
|
||||
if [[ -f "${repo_manifest_file}" ]]; then
|
||||
local_repo_id=$(cat "${repo_manifest_file}" | yq '.id')
|
||||
|
||||
echo "${local_repo_id}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Add a new app repo
|
||||
if [[ "$command" = "add" ]]; then
|
||||
descriptor="${2-}"
|
||||
|
||||
if [[ -z "${descriptor}" ]]; then
|
||||
>&2 echo "A valid remote repo url must be set. e.g. ${DEFAULT_UMBREL_APP_REPO_URL}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
repo_url=$(parse_repo_url "${descriptor}")
|
||||
|
||||
has_repo=$(cat "${USER_FILE}" | jq ".repos | any(. == \"${repo_url}\")")
|
||||
|
||||
if [[ "${has_repo}" == "true" ]]; then
|
||||
>&2 echo "Repo: ${repo_url} already exists."
|
||||
exit
|
||||
fi
|
||||
|
||||
while ! (set -o noclobber; echo "$$" > "${USER_FILE}.lock") 2> /dev/null; do
|
||||
echo "Waiting for JSON lock to be released for ${app} update..."
|
||||
sleep 1
|
||||
done
|
||||
# This will cause the lock-file to be deleted in case of a
|
||||
# premature exit.
|
||||
trap "rm -f "${USER_FILE}.lock"; exit $?" INT TERM EXIT
|
||||
|
||||
updated_json=$(cat "${USER_FILE}" | jq ".repos |= (. + [\"${repo_url}\"] | unique)")
|
||||
echo "${updated_json}" > "${USER_FILE}"
|
||||
|
||||
rm -f "${USER_FILE}.lock"
|
||||
|
||||
echo "Added: ${repo_url}"
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# Remove an app repo
|
||||
if [[ "$command" = "remove" ]]; then
|
||||
repo_url="${2-}"
|
||||
|
||||
if [[ -z "${repo_url}" ]]; then
|
||||
>&2 echo "A valid remote repo url must be set. e.g. ${DEFAULT_UMBREL_APP_REPO_URL}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
has_repo=$(cat "${USER_FILE}" | jq ".repos | any(. == \"${repo_url}\")")
|
||||
|
||||
if [[ "${has_repo}" == "false" ]]; then
|
||||
>&2 echo "Repo: ${repo_url} does not exist locally."
|
||||
exit
|
||||
fi
|
||||
|
||||
while ! (set -o noclobber; echo "$$" > "${USER_FILE}.lock") 2> /dev/null; do
|
||||
echo "Waiting for JSON lock to be released for ${app} update..."
|
||||
sleep 1
|
||||
done
|
||||
# This will cause the lock-file to be deleted in case of a
|
||||
# premature exit.
|
||||
trap "rm -f "${USER_FILE}.lock"; exit $?" INT TERM EXIT
|
||||
|
||||
updated_json=$(cat "${USER_FILE}" | jq ".repos |= (. - [\"${repo_url}\"] | unique)")
|
||||
echo "${updated_json}" > "${USER_FILE}"
|
||||
|
||||
rm -f "${USER_FILE}.lock"
|
||||
|
||||
local_repo_path=$(repo_path "${repo_url}")
|
||||
rm -rf "${local_repo_path}"
|
||||
|
||||
echo "Removed: ${repo_url}"
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# Update local copies of repos from remote
|
||||
if [[ "$command" = "update" ]]; then
|
||||
# Check if an argument exists to
|
||||
# only update a specific repo
|
||||
repo_url="${2-}"
|
||||
if [[ ! -z "${repo_url}" ]]; then
|
||||
all_repo_urls="${repo_url}"
|
||||
else
|
||||
all_repo_urls=$(list_repos)
|
||||
fi
|
||||
|
||||
for repo_url in $all_repo_urls; do
|
||||
echo "Processing: ${repo_url}"
|
||||
|
||||
local_repo_path=$(repo_path "${repo_url}")
|
||||
|
||||
# Check the local app repo is marked as safe by Git
|
||||
if ! git config --global --get safe.directory | grep --quiet "^${local_repo_path}$"; then
|
||||
git config --global --add safe.directory "${local_repo_path}"
|
||||
fi
|
||||
|
||||
# Check integrity of local app repo
|
||||
if [[ -d "${local_repo_path}" ]]; then
|
||||
set +e
|
||||
out=$(git -C "${local_repo_path}" status 2>&1)
|
||||
# 1 = network error (and other errors)
|
||||
# 128 = git repo bad/corrupt/incomplete
|
||||
exit_code=$?
|
||||
set -e
|
||||
if [[ "${exit_code}" == "128" ]]; then
|
||||
echo "${out}"
|
||||
echo
|
||||
echo "Bad local app repo detected. Removing..."
|
||||
|
||||
rm -rf "${local_repo_path}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ignore exit code just in case 1 repo is bad
|
||||
# Which would then prevent other repos getting updated
|
||||
{
|
||||
if [[ -d "${local_repo_path}" ]]; then
|
||||
# Git can leave behind write locks if something fails
|
||||
# These locks should only exist for milliseconds
|
||||
# We'll delete any lock older than 10 seconds
|
||||
# Otherwise git will not proceed
|
||||
epoch_ts=$(date +%s)
|
||||
for file in "${local_repo_path}/.git"/*.lock; do
|
||||
[[ ! -f "${file}" ]] && continue
|
||||
|
||||
age_of_lock=$(stat -c '%X' "${file}")
|
||||
age_seconds=$((epoch_ts-age_of_lock))
|
||||
|
||||
if [[ "${age_seconds}" -gt "10" ]]; then
|
||||
echo "Deleting old lock: ${file}"
|
||||
rm -rf "${file}"
|
||||
fi
|
||||
done
|
||||
|
||||
repo_branch=$(git -C "${local_repo_path}" rev-parse --abbrev-ref HEAD)
|
||||
echo "Updating repo: ${repo_url} (${repo_branch})"
|
||||
|
||||
timeout --foreground 30 git -C "${local_repo_path}" pull
|
||||
else
|
||||
echo "Cloning repo: ${repo_url}"
|
||||
|
||||
timeout --foreground 30 git clone "${repo_url}" "${local_repo_path}"
|
||||
|
||||
# Check we have a valid app repo
|
||||
if [[ "${repo_url}" != "${DEFAULT_UMBREL_APP_REPO_URL}" ]]; then
|
||||
repo_manifest_file="${local_repo_path}/umbrel-app-store.yml"
|
||||
|
||||
if [[ -f "${repo_manifest_file}" ]]; then
|
||||
local_repo_id=$(cat "${repo_manifest_file}" | yq '.id')
|
||||
|
||||
ids=$(list_repo_ids "${repo_url}")
|
||||
if echo "${ids}" | grep --quiet "^${local_repo_id}$"; then
|
||||
echo
|
||||
echo "Error: Repo (${local_repo_id}) already exists"
|
||||
echo
|
||||
"${0}" "remove" "${repo_url}"
|
||||
fi
|
||||
else
|
||||
echo
|
||||
echo "Error: Repo (${repo_url}) missing umbrel-app-store.yml"
|
||||
echo
|
||||
"${0}" "remove" "${repo_url}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
} || true
|
||||
|
||||
# Make sure app repo doesn't end up being owned by root or some apps will fail
|
||||
# with fs permission errors
|
||||
if [[ -d "${local_repo_path}" ]]; then
|
||||
chown -R "1000:1000" "${local_repo_path}"
|
||||
fi
|
||||
|
||||
echo
|
||||
done
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "$command" = "branch" ]]; then
|
||||
repo="${2-}"
|
||||
branch="${3-}"
|
||||
|
||||
if [[ -z "${repo}" ]]; then
|
||||
>&2 echo "A valid repo must be given. e.g. ${DEFAULT_UMBREL_APP_REPO_URL}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${branch}" ]]; then
|
||||
>&2 echo "A valid branch name must be given. e.g. staging"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local_repo_path=$(repo_path "${repo}")
|
||||
|
||||
if [[ ! -d "${local_repo_path}" ]]; then
|
||||
>&2 echo "App repo does not yet exist locally"
|
||||
>&2 echo "Please use the 'update' subcommand"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check whether it's only fetching for master
|
||||
git_config_file="${local_repo_path}/.git/config"
|
||||
if grep -q "refs/heads/master:refs/remotes/origin/master" "${git_config_file}"; then
|
||||
sed -i "s/refs\/heads\/master:refs\/remotes\/origin\/master/refs\/heads\/*:refs\/remotes\/origin\/*/g;" "${git_config_file}"
|
||||
fi
|
||||
|
||||
# Fetch remote commits, files, and refs
|
||||
git -C "${local_repo_path}" fetch
|
||||
|
||||
# Checkout branch
|
||||
git -C "${local_repo_path}" checkout "${branch}"
|
||||
|
||||
# Update local repo
|
||||
"${0}" "update" "${repo}"
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "$command" = "checkout" ]]; then
|
||||
descriptor="${2-}"
|
||||
|
||||
if [[ -z "${descriptor}" ]]; then
|
||||
>&2 echo "Error: A valid remote git repo descriptor must be given. e.g. coolapp/umbrel-apps"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# These are all valid repo descriptors
|
||||
# - http://github.com/coolapp/umbrel-apps
|
||||
# - coolapp/umbrel-apps (will use github.com via https)
|
||||
# - coolapp/umbrel-apps#staging
|
||||
|
||||
# Note: using the .git extension is optional
|
||||
repo=$(parse_repo_url "${descriptor}")
|
||||
|
||||
branch="master"
|
||||
if [[ "${descriptor}" == *"#"* ]]; then
|
||||
branch="${descriptor#*#}"
|
||||
fi
|
||||
|
||||
default_repo_path=$(repo_path "${DEFAULT_UMBREL_APP_REPO_URL}")
|
||||
|
||||
if [[ ! -d "${default_repo_path}" ]]; then
|
||||
>&2 echo "Error: The default Umbrel app repo must be setup first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${repo}" == "${DEFAULT_UMBREL_APP_REPO_URL}" ]]; then
|
||||
origin_name="origin"
|
||||
else
|
||||
origin_name=$(printf "%s" "${repo}" | openssl dgst -sha256 | sed 's/^.* //')
|
||||
fi
|
||||
|
||||
if ! git -C "${default_repo_path}" remote | grep "${origin_name}" > /dev/null; then
|
||||
git -C "${default_repo_path}" remote add "${origin_name}" "${repo}"
|
||||
fi
|
||||
|
||||
git -C "${default_repo_path}" fetch "${origin_name}" "${branch}"
|
||||
|
||||
git -C "${default_repo_path}" checkout "${origin_name}/${branch}"
|
||||
|
||||
# Make sure app repo doesn't end up being owned by root or some apps will fail
|
||||
# with fs permission errors
|
||||
if [[ -d "${default_repo_path}" ]]; then
|
||||
chown -R "1000:1000" "${default_repo_path}"
|
||||
fi
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "$command" = "path" ]]; then
|
||||
repo_url="${2-}"
|
||||
|
||||
repo_path "${repo_url}"
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "$command" = "locate" ]]; then
|
||||
app="${2-}"
|
||||
|
||||
all_repo_urls=$(list_repos)
|
||||
for repo_url in $all_repo_urls; do
|
||||
local_repo_path=$(repo_path "${repo_url}")
|
||||
|
||||
if [[ -d "${local_repo_path}/${app}" ]]; then
|
||||
echo "${repo_url}"
|
||||
exit
|
||||
fi
|
||||
done
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# If we get here it means no valid command was supplied
|
||||
# Show help and exit
|
||||
show_help
|
||||
exit 1
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2015-2018 The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
from argparse import ArgumentParser
|
||||
from base64 import urlsafe_b64encode
|
||||
from binascii import hexlify
|
||||
from getpass import getpass
|
||||
from os import urandom
|
||||
|
||||
import hmac
|
||||
|
||||
def generate_salt(size):
|
||||
"""Create size byte hex salt"""
|
||||
return hexlify(urandom(size)).decode()
|
||||
|
||||
def generate_password():
|
||||
"""Create 32 byte b64 password"""
|
||||
return urlsafe_b64encode(urandom(32)).decode('utf-8')
|
||||
|
||||
def password_to_hmac(salt, password):
|
||||
m = hmac.new(bytearray(salt, 'utf-8'), bytearray(password, 'utf-8'), 'SHA256')
|
||||
return m.hexdigest()
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(description='Create login credentials for a JSON-RPC user')
|
||||
parser.add_argument('username', help='the username for authentication')
|
||||
parser.add_argument('password', help='leave empty to generate a random password or specify "-" to prompt for password', nargs='?')
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.password:
|
||||
args.password = generate_password()
|
||||
elif args.password == '-':
|
||||
args.password = getpass()
|
||||
|
||||
# Create 16 byte hex salt
|
||||
salt = generate_salt(16)
|
||||
password_hmac = password_to_hmac(salt, args.password)
|
||||
|
||||
print('String to be appended to bitcoin.conf:')
|
||||
print('rpcauth={0}:{1}${2}'.format(args.username, salt, password_hmac))
|
||||
print('Your password:\n{0}'.format(args.password))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
-197
@@ -1,197 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Start Umbrel
|
||||
|
||||
if [[ $UID != 0 ]]; then
|
||||
echo "Umbrel must be started as root"
|
||||
echo "Please re-run this script as"
|
||||
echo " sudo ./scripts/start"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_dependencies () {
|
||||
for cmd in "$@"; do
|
||||
if ! command -v $cmd >/dev/null 2>&1; then
|
||||
echo "This script requires \"${cmd}\" to be installed"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Check system's dependencies
|
||||
check_dependencies readlink dirname ip docker docker-compose
|
||||
|
||||
# Check karen's dependencies
|
||||
check_dependencies fswatch
|
||||
|
||||
# Check OTA update scripts' dependencies
|
||||
check_dependencies rsync jq curl
|
||||
|
||||
UMBREL_ROOT="$(dirname $(readlink -f "${BASH_SOURCE[0]}"))/.."
|
||||
UMBREL_LOGS="${UMBREL_ROOT}/logs"
|
||||
USER_FILE="${UMBREL_ROOT}/db/user.json"
|
||||
|
||||
REMOTE_TOR_ACCESS="false"
|
||||
if [[ -f "${USER_FILE}" ]]; then
|
||||
REMOTE_TOR_ACCESS=$(cat "${USER_FILE}" | jq 'has("remoteTorAccess") and .remoteTorAccess')
|
||||
fi
|
||||
|
||||
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
|
||||
|
||||
# Configure Umbrel if it isn't already configured
|
||||
if [[ ! -f "${UMBREL_ROOT}/statuses/configured" ]]; then
|
||||
NGINX_PORT=${NGINX_PORT:-80} NETWORK="${NETWORK:-mainnet}" "${UMBREL_ROOT}/scripts/configure"
|
||||
fi
|
||||
|
||||
UMBREL_UPDATE_REBOOT_PERFORMED="${UMBREL_ROOT}/statuses/umbrel-update-reboot-performed"
|
||||
if [[ -f "${UMBREL_UPDATE_REBOOT_PERFORMED}" ]]; then
|
||||
# Cleanup status file
|
||||
rm "${UMBREL_UPDATE_REBOOT_PERFORMED}"
|
||||
|
||||
# Mark update status as successful
|
||||
cat <<EOF > "${UMBREL_ROOT}/statuses/update-status.json"
|
||||
{"state": "success", "progress": 100, "description": "", "updateTo": ""}
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "======================================"
|
||||
echo "============= STARTING ==============="
|
||||
echo "============== UMBREL ================"
|
||||
echo "======================================"
|
||||
echo
|
||||
|
||||
echo "Setting environment variables..."
|
||||
echo
|
||||
|
||||
# Check Umbrel OS
|
||||
[[ -f "/etc/default/umbrel" ]] && source "/etc/default/umbrel"
|
||||
if [[ -z "${UMBREL_OS:-}" ]]; then
|
||||
export IS_UMBREL_OS="false"
|
||||
else
|
||||
export IS_UMBREL_OS="true"
|
||||
fi
|
||||
|
||||
# Increase default Docker and Compose timeouts to 240s
|
||||
# as bitcoin can take a long while to respond
|
||||
export DOCKER_CLIENT_TIMEOUT=240
|
||||
export COMPOSE_HTTP_TIMEOUT=240
|
||||
|
||||
cd "$UMBREL_ROOT"
|
||||
|
||||
echo "Starting karen..."
|
||||
echo
|
||||
./karen &>> "${UMBREL_LOGS}/karen.log" &
|
||||
|
||||
echo "Starting status monitors..."
|
||||
pkill -f ./scripts/status-monitor || true
|
||||
./scripts/status-monitor memory 60 &>> "${UMBREL_LOGS}/status-monitor.log" &
|
||||
./scripts/status-monitor storage 60 &>> "${UMBREL_LOGS}/status-monitor.log" &
|
||||
./scripts/status-monitor temperature 15 &>> "${UMBREL_LOGS}/status-monitor.log" &
|
||||
./scripts/status-monitor uptime 15 &>> "${UMBREL_LOGS}/status-monitor.log" &
|
||||
|
||||
# Automatically update app repo every 5 minutes
|
||||
# If the app repo has not yet been cloned (ie it's a fresh Umbrel install)
|
||||
# Then this will do the intial pull/initialisation
|
||||
./scripts/status-monitor repo 300 &>> "${UMBREL_LOGS}/status-monitor.log" &
|
||||
|
||||
if [[ "${IS_UMBREL_OS}" == "true" ]]; then
|
||||
# Set a default value for the SD card health (healthy)
|
||||
echo "false" > "${UMBREL_ROOT}/statuses/sd-card-health-status.json"
|
||||
|
||||
# Check the SD card health every 7 days
|
||||
# ./scripts/status-monitor sd-card-health 604800 &>> "${UMBREL_LOGS}/status-monitor.log" &
|
||||
fi
|
||||
|
||||
echo "Starting memory monitor..."
|
||||
echo
|
||||
./scripts/memory-monitor &>> "${UMBREL_LOGS}/memory-monitor.log" &
|
||||
|
||||
echo "Starting backup monitor..."
|
||||
echo
|
||||
./scripts/backup/monitor &>> "${UMBREL_LOGS}/backup-monitor.log" &
|
||||
|
||||
echo "Starting decoy backup trigger..."
|
||||
echo
|
||||
./scripts/backup/decoy-trigger &>> "${UMBREL_LOGS}/backup-decoy-trigger.log" &
|
||||
|
||||
if [[ "${@}" != *"--no-start-server"* ]]; then
|
||||
echo "Starting umbreld..."
|
||||
echo
|
||||
pkill --full 'caxa.*umbreld' || true
|
||||
./bin/umbreld --port 81 --data-directory "${UMBREL_ROOT}" &>> "${UMBREL_LOGS}/umbreld.log" &
|
||||
fi
|
||||
|
||||
compose_files=()
|
||||
|
||||
if [[ "${REMOTE_TOR_ACCESS}" == "true" ]]; then
|
||||
compose_files+=( "--file" "docker-compose.tor.yml" )
|
||||
fi
|
||||
|
||||
compose_files+=( "--file" "docker-compose.yml" )
|
||||
|
||||
UMBREL_DEV_FILE="${UMBREL_ROOT}/.umbrel-dev"
|
||||
if [[ -f "${UMBREL_DEV_FILE}" ]]; then
|
||||
compose_files+=( "--file" "${UMBREL_ROOT}/docker-compose.dev.yml" )
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Starting Docker services..."
|
||||
echo
|
||||
docker-compose "${compose_files[@]}" up --detach --build --remove-orphans || {
|
||||
echo "Failed to start containers"
|
||||
$set_status umbrel errored docker-failed
|
||||
exit 1
|
||||
}
|
||||
echo
|
||||
|
||||
echo "Removing status server iptables entry..."
|
||||
"${UMBREL_ROOT}/scripts/umbrel-os/status-server/setup-iptables" --delete
|
||||
|
||||
echo
|
||||
echo "Starting installed apps..."
|
||||
echo
|
||||
# Unlock the user file on each start of Umbrel to avoid issues
|
||||
# Normally, the user file shouldn't ever be locked, if it is, something went wrong, but it could still be working
|
||||
if [[ -f "${UMBREL_ROOT}/db/user.json.lock" ]]; then
|
||||
echo "WARNING: The user file was locked, Umbrel probably wasn't shut down properly"
|
||||
rm "${UMBREL_ROOT}/db/user.json.lock"
|
||||
fi
|
||||
"${UMBREL_ROOT}/scripts/app" start installed
|
||||
echo
|
||||
|
||||
# If a backup of resolv.conf exists
|
||||
# (that got created during the Umbrel update process)
|
||||
# then we'll now restore this after Umbrel
|
||||
# and the apps have started
|
||||
# That way if e.g. a Docker image is still missing,
|
||||
# we would use public DNS servers
|
||||
RESOLV_CONF_FILE="/etc/resolv.conf"
|
||||
RESOLV_CONF_BACKUP_FILE="/tmp/resolv.conf"
|
||||
if [[ -f "${RESOLV_CONF_BACKUP_FILE}" ]]; then
|
||||
cat "${RESOLV_CONF_BACKUP_FILE}" > "${RESOLV_CONF_FILE}" || true
|
||||
|
||||
rm --force "${RESOLV_CONF_BACKUP_FILE}" || true
|
||||
fi
|
||||
|
||||
DEVICE_HOSTNAME="$(hostname).local"
|
||||
DEVICE_IP="$(ip -o route get to 8.8.8.8 | sed -n 's/.*src \([0-9.]\+\).*/\1/p')"
|
||||
TOR_HS_WEB_HOSTNAME_FILE="${UMBREL_ROOT}/tor/data/web/hostname"
|
||||
|
||||
echo "Umbrel is now accessible at"
|
||||
echo " http://${DEVICE_HOSTNAME}"
|
||||
echo " http://${DEVICE_IP}"
|
||||
if [[ "${REMOTE_TOR_ACCESS}" == "true" ]] && [[ -f "${TOR_HS_WEB_HOSTNAME_FILE}" ]]; then
|
||||
echo " http://$(cat "${TOR_HS_WEB_HOSTNAME_FILE}")"
|
||||
fi
|
||||
|
||||
$set_status umbrel completed
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..)"
|
||||
|
||||
resource="${1}"
|
||||
interval="${2}"
|
||||
|
||||
log () {
|
||||
echo "$(date "+%Y-%m-%d %H:%M:%S") ${@}"
|
||||
}
|
||||
|
||||
log "Status monitor for \"${resource}\" running every ${interval} seconds!"
|
||||
|
||||
status_script="${UMBREL_ROOT}/scripts/status/${resource}"
|
||||
json_path="${UMBREL_ROOT}/statuses/${resource}-status.json"
|
||||
while true; do
|
||||
result=$("${status_script}")
|
||||
echo "$result" > "${json_path}"
|
||||
sleep "${interval}"
|
||||
done
|
||||
@@ -1,64 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)"
|
||||
|
||||
memory_total_bytes() {
|
||||
free --bytes | awk 'NR==2 {print $2}'
|
||||
}
|
||||
|
||||
memory_used_bytes() {
|
||||
free --bytes | awk 'NR==2 {print $3}'
|
||||
}
|
||||
|
||||
get_container_memory_use() {
|
||||
local container="${1}"
|
||||
|
||||
local container_memory=0
|
||||
|
||||
local container_pids=$(docker top "${container}" | tail -n +2 | awk '{print $2}')
|
||||
for pid in $container_pids; do
|
||||
local pid_memory=$(ps u "${pid}" | awk '{print $4}' | grep -v 'MEM')
|
||||
if [[ ! -z "${pid_memory}" ]]; then
|
||||
container_memory=$(awk "BEGIN {print ${container_memory}+${pid_memory}}")
|
||||
fi
|
||||
done
|
||||
|
||||
echo "${container_memory}"
|
||||
}
|
||||
|
||||
get_app_memory_use() {
|
||||
local app="${1}"
|
||||
|
||||
local app_memory=0
|
||||
|
||||
local app_containers=$("${UMBREL_ROOT}/scripts/app" compose "${app}" ps | awk '{print $1}' | grep -v 'Name\|-----')
|
||||
for container in $app_containers; do
|
||||
local container_memory=$(get_container_memory_use "${container}")
|
||||
app_memory=$(awk "BEGIN {print ${app_memory}+${container_memory}}")
|
||||
done
|
||||
|
||||
# Above values return % of total memory used so we need to convert to bytes
|
||||
total=$(memory_total_bytes)
|
||||
awk "BEGIN {printf(\"%.f\", ${total}*${app_memory}/100)}"
|
||||
}
|
||||
|
||||
json="{}"
|
||||
|
||||
total=$(memory_total_bytes)
|
||||
json=$(echo $json | jq --arg total "${total}" '. + {total: $total|tonumber}')
|
||||
|
||||
used=$(memory_used_bytes)
|
||||
json=$(echo $json | jq --arg used "${used}" '. + {used: $used|tonumber}')
|
||||
|
||||
cumulative_app_memory="0"
|
||||
for app in $( "${UMBREL_ROOT}/scripts/app" ls-installed)
|
||||
do
|
||||
app_memory=$(get_app_memory_use "${app}")
|
||||
cumulative_app_memory=$(($cumulative_app_memory+$app_memory))
|
||||
json=$(echo $json | jq --arg app "${app}" --arg app_memory "${app_memory}" '.breakdown |= .+ [{id: $app, used: $app_memory|tonumber}]')
|
||||
done
|
||||
|
||||
umbrel=$(($used-$cumulative_app_memory))
|
||||
json=$(echo $json | jq --arg umbrel "${umbrel}" '.breakdown |= .+ [{id: "umbrel", used: $umbrel|tonumber}]')
|
||||
|
||||
echo $json | jq '.breakdown |= sort_by(-.used)'
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)"
|
||||
|
||||
$UMBREL_ROOT/scripts/repo update
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)"
|
||||
|
||||
# The Raspberry Pi Foundation recommends using Class A1 SD cards
|
||||
# Link here: https://www.raspberrypi.com/news/sd-card-speed-test/
|
||||
|
||||
# According to the standard, defined here: https://www.sdcard.org/developers/sd-standard-overview/application-performance-class/
|
||||
# A Class A1 SD card, we should be able to achieve the following performance metrics:
|
||||
# Seq. write BW 10000 KB/sec
|
||||
# Rand. write 500 IOPS
|
||||
# Rand. read 1500 IOPS
|
||||
|
||||
# Run twice, the first run is a warmup, the second is the test
|
||||
for i in 1 2
|
||||
do
|
||||
RES=$(fio --output-format=terse --max-jobs=4 "${UMBREL_ROOT}/scripts/umbrel-os/sd-card-health/sd_bench.fio" | cut -f 3,7,8,48,49 -d";" -)
|
||||
|
||||
rm -f /var/tmp/sd.test.file
|
||||
done
|
||||
|
||||
swri=$(echo "$RES" | head -n 2 | tail -n 1 | cut -d ";" -f 4)
|
||||
rwri=$(echo "$RES" | head -n 3 | tail -n 1 | cut -d ";" -f 5)
|
||||
rrea=$(echo "$RES" | head -n 4 | tail -n 1 | cut -d ";" -f 3)
|
||||
|
||||
# echo "${swri} ${rwri} ${rrea}"
|
||||
|
||||
# Check if any metric fails the threshold:
|
||||
if [[ "$swri" -lt 10000 ]] || [[ "$rwri" -lt 500 ]] || [[ "$rrea" -lt 1500 ]]; then
|
||||
echo "true"
|
||||
else
|
||||
echo "false"
|
||||
fi
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)"
|
||||
|
||||
filesystem_total_storage_bytes() {
|
||||
local directory="${1}"
|
||||
df --block-size=1 --output=size "${directory}" | tail -n 1
|
||||
}
|
||||
|
||||
filesystem_used_storage_bytes() {
|
||||
local directory="${1}"
|
||||
df --block-size=1 --output=used "${directory}" | tail -n 1
|
||||
}
|
||||
|
||||
directory_size_bytes() {
|
||||
local directory="${1}"
|
||||
du --block-size=1 --max-depth=0 "${directory}" | awk '{print $1}'
|
||||
}
|
||||
|
||||
json="{}"
|
||||
|
||||
total=$(filesystem_total_storage_bytes "${UMBREL_ROOT}")
|
||||
json=$(echo $json | jq --arg total "${total}" '. + {total: $total|tonumber}')
|
||||
|
||||
used=$(filesystem_used_storage_bytes "${UMBREL_ROOT}")
|
||||
json=$(echo $json | jq --arg used "${used}" '. + {used: $used|tonumber}')
|
||||
|
||||
cumulative_app_size="0"
|
||||
for app in $( "${UMBREL_ROOT}/scripts/app" ls-installed)
|
||||
do
|
||||
app_size=$(directory_size_bytes "${UMBREL_ROOT}/app-data/${app}")
|
||||
cumulative_app_size=$(($cumulative_app_size+$app_size))
|
||||
json=$(echo $json | jq --arg app "${app}" --arg app_size "${app_size}" '.breakdown |= .+ [{id: $app, used: $app_size|tonumber}]')
|
||||
done
|
||||
|
||||
umbrel=$(($used-$cumulative_app_size))
|
||||
json=$(echo $json | jq --arg umbrel "${umbrel}" '.breakdown |= .+ [{id: "umbrel", used: $umbrel|tonumber}]')
|
||||
|
||||
echo $json | jq '.breakdown |= sort_by(-.used)'
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This may not always be the CPU sensor on other hardware.
|
||||
# To be sure we need to iterate through /sys/class/thermal/thermal_zone*/type
|
||||
# https://www.kernel.org/doc/Documentation/thermal/sysfs-api.txt
|
||||
temperature_file="/sys/class/thermal/thermal_zone0/temp"
|
||||
|
||||
# Check temperature file exists
|
||||
if [[ ! -f "${temperature_file}" ]]
|
||||
then
|
||||
echo "null"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Read temperature file
|
||||
temperature=$(cat "${temperature_file}")
|
||||
|
||||
# Convert millidegrees celsius to degrees celsius
|
||||
echo "${temperature}" | awk '{print int($1/1000)}'
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
json="{}"
|
||||
|
||||
cat /proc/uptime | awk '{print int($1)}'
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Stop Umbrel
|
||||
|
||||
if [[ $UID != 0 ]]; then
|
||||
echo "Umbrel must be stopped as root"
|
||||
echo "Please re-run this script as"
|
||||
echo " sudo ./scripts/stop"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
UMBREL_ROOT="$(dirname $(readlink -f "${BASH_SOURCE[0]}"))/.."
|
||||
USER_FILE="${UMBREL_ROOT}/db/user.json"
|
||||
|
||||
REMOTE_TOR_ACCESS="false"
|
||||
if [[ -f "${USER_FILE}" ]]; then
|
||||
REMOTE_TOR_ACCESS=$(cat "${USER_FILE}" | jq 'has("remoteTorAccess") and .remoteTorAccess')
|
||||
fi
|
||||
|
||||
if [[ ! -d "$UMBREL_ROOT" ]]; then
|
||||
echo "Root dir does not exist '$UMBREL_ROOT'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$UMBREL_ROOT"
|
||||
|
||||
# Increase default Docker and Compose timeouts to 240s
|
||||
# As bitcoin can take a long while to respond
|
||||
export DOCKER_CLIENT_TIMEOUT=240
|
||||
export COMPOSE_HTTP_TIMEOUT=240
|
||||
|
||||
# TODO: Transition to pidfiles so we can reliably kill these processes
|
||||
# Run this before Docker, as Docker can fail to stop cleanly
|
||||
echo "Killing background services"
|
||||
ps -Af \
|
||||
| grep '\./karen\|\./scripts/status-monitor\|\./scripts/memory-monitor\|\./scripts/backup/\|umbrel/scripts\|fswatch' \
|
||||
| grep -v '\-run.sh\|update\|stop\|umbreld' \
|
||||
| awk '{print $2}' \
|
||||
| xargs sudo kill 2> /dev/null || true
|
||||
|
||||
if [[ "${@}" != *"--no-stop-server"* ]]; then
|
||||
pkill --full 'caxa.*umbreld' || true
|
||||
fi
|
||||
|
||||
echo "Stopping installed apps..."
|
||||
echo
|
||||
"${UMBREL_ROOT}/scripts/app" stop installed
|
||||
echo
|
||||
|
||||
compose_files=()
|
||||
|
||||
if [[ "${REMOTE_TOR_ACCESS}" == "true" ]]; then
|
||||
compose_files+=( "--file" "docker-compose.tor.yml" )
|
||||
fi
|
||||
|
||||
compose_files+=( "--file" "docker-compose.yml" )
|
||||
|
||||
UMBREL_DEV_FILE="${UMBREL_ROOT}/.umbrel-dev"
|
||||
if [[ -f "${UMBREL_DEV_FILE}" ]]; then
|
||||
compose_files+=( "--file" "${UMBREL_ROOT}/docker-compose.dev.yml" )
|
||||
fi
|
||||
|
||||
echo "Stopping Docker services..."
|
||||
echo
|
||||
docker-compose "${compose_files[@]}" down
|
||||
@@ -1,27 +0,0 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
app_proxy:
|
||||
image: getumbrel/app-proxy:v0.5.2@sha256:a2e3e0ddfcf84838bf0ba66f4b839ec958832d51f0ac9ace47962459c838b2d6
|
||||
#build: ../../containers/app-proxy
|
||||
user: "1000:1000"
|
||||
restart: on-failure
|
||||
hostname: $APP_PROXY_HOSTNAME
|
||||
ports:
|
||||
- "${APP_PROXY_PORT}:${APP_PROXY_PORT}"
|
||||
volumes:
|
||||
- "${APP_MANIFEST_FILE}:/extra/umbrel-app.yml:ro"
|
||||
- "${TOR_DATA_DIR}:/var/lib/tor:ro"
|
||||
- "${APP_DATA_DIR}:/data:ro"
|
||||
environment:
|
||||
LOG_LEVEL: info
|
||||
PROXY_PORT: $APP_PROXY_PORT
|
||||
PROXY_AUTH_ADD: "true"
|
||||
PROXY_AUTH_WHITELIST:
|
||||
PROXY_AUTH_BLACKLIST:
|
||||
APP_HOST:
|
||||
APP_PORT:
|
||||
AUTH_SERVICE_PORT: $AUTH_PORT
|
||||
UMBREL_AUTH_SECRET: $UMBREL_AUTH_SECRET
|
||||
MANAGER_IP: $MANAGER_IP
|
||||
MANAGER_PORT: 3006
|
||||
@@ -1,6 +0,0 @@
|
||||
version: "3.7"
|
||||
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: umbrel_main_network
|
||||
@@ -1,15 +0,0 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
tor_server:
|
||||
image: getumbrel/tor:0.4.7.8@sha256:2ace83f22501f58857fa9b403009f595137fa2e7986c4fda79d82a8119072b6a
|
||||
user: "1000:1000"
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- ${TOR_ENTRYPOINT_SCRIPT}:/umbrel/entrypoint.sh
|
||||
- ${TOR_DATA_DIR}:/data
|
||||
environment:
|
||||
HOME: "/tmp"
|
||||
HS_DIR: "${TOR_HS_APP_DIR}"
|
||||
HS_PORTS: "${TOR_HS_PORTS}"
|
||||
entrypoint: "/umbrel/entrypoint.sh"
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
TORRC_PATH="/tmp/torrc"
|
||||
|
||||
echo "HiddenServiceDir ${HS_DIR}" > "${TORRC_PATH}"
|
||||
|
||||
# Loop through all ports we want to expose
|
||||
# On this hidden service
|
||||
for service in $HS_PORTS
|
||||
do
|
||||
virtual_port=$(echo $service | cut -d : -f 1)
|
||||
source_host=$(echo $service | cut -d : -f 2)
|
||||
source_port=$(echo $service | cut -d : -f 3)
|
||||
echo "HiddenServicePort ${virtual_port} ${source_host}:${source_port}" >> "${TORRC_PATH}"
|
||||
done
|
||||
|
||||
tor -f "${TORRC_PATH}"
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)"
|
||||
PASSWORD_FILE="${UMBREL_ROOT}/statuses/password"
|
||||
|
||||
check_root () {
|
||||
if [[ $UID != 0 ]]; then
|
||||
echo "This script must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_umbrel_os () {
|
||||
[[ -f "/etc/default/umbrel" ]] && source "/etc/default/umbrel"
|
||||
if [[ -z "${UMBREL_OS:-}" ]]; then
|
||||
echo "This script must only be run on Umbrel OS"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
password="$(cat ${PASSWORD_FILE})"
|
||||
echo "false" > "${PASSWORD_FILE}"
|
||||
|
||||
check_root
|
||||
check_umbrel_os
|
||||
|
||||
if [[ "${password}" == "" ]] || [[ "${password}" == "false" ]] || [[ "${password}" == "true" ]]; then
|
||||
echo "Password not set in status file: \"${PASSWORD_FILE}\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${password}\n${password}" | passwd umbrel
|
||||
echo "true" > "${PASSWORD_FILE}"
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../../..)"
|
||||
block_device="${1}"
|
||||
mount_point="${2}"
|
||||
|
||||
set_status="/status-server/set-status"
|
||||
setup_iptables="/status-server/setup-iptables"
|
||||
|
||||
check_if_not_already_running() {
|
||||
if ps ax | grep $0 | grep -v $$ | grep bash | grep -v grep
|
||||
then
|
||||
echo "storage monitor is already running"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main () {
|
||||
check_if_not_already_running
|
||||
|
||||
while true; do
|
||||
if ! [[ -e "/dev/${block_device}" ]]; then
|
||||
echo "Mounted block device no longer exists!"
|
||||
break
|
||||
fi
|
||||
|
||||
if ! grep --quiet " ${mount_point} " /proc/mounts; then
|
||||
echo "External storage is no longer mounted!"
|
||||
break
|
||||
fi
|
||||
|
||||
if grep " ${mount_point} " /proc/mounts | grep --quiet '[ ,]ro[ ,]'; then
|
||||
echo "External storage is mounted as read only!"
|
||||
break
|
||||
fi
|
||||
|
||||
if ! df -h "${UMBREL_ROOT}" | grep --quiet "/dev/${block_device}"; then
|
||||
echo "Umbrel root is no longer bind mounted to external storage!"
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Stopping Umbrel due to failed storage device check..."
|
||||
docker kill $(docker ps -aq)
|
||||
$set_status mount errored monitor-check
|
||||
$setup_iptables --force
|
||||
}
|
||||
|
||||
main
|
||||
@@ -1,69 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../../..)"
|
||||
SD_MOUNT_POINT="/sd-root"
|
||||
SD_UMBREL_ROOT="${SD_MOUNT_POINT}${UMBREL_ROOT}"
|
||||
|
||||
set_status="/status-server/set-status"
|
||||
|
||||
check_root () {
|
||||
if [[ $UID != 0 ]]; then
|
||||
echo "This script must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_dependencies () {
|
||||
for cmd in "$@"; do
|
||||
if ! command -v $cmd >/dev/null 2>&1; then
|
||||
echo "This script requires \"${cmd}\" to be installed"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
check_semver_range () {
|
||||
local range="${1}"
|
||||
local version="${2}"
|
||||
"${UMBREL_ROOT}/scripts/umbrel-os/semver" -r "${range}" "${version}" | grep --quiet "^${version}$"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
echo "Yes, SD version is newer."
|
||||
# This will fail if we ever have multiple ranges with pre-release tags.
|
||||
# e.g `1.2.3-beta <2.0.0` will become `1.2.3` due to striping the `-`
|
||||
local update_requirement=$(cat "${SD_UMBREL_ROOT}/info.json" | jq -r .requires | cut -d "-" -f "1")
|
||||
|
||||
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
|
||||
|
||||
# As the system just booted, there cannot be an update in progress...
|
||||
# We'll delete the update lock (update-in-progress)
|
||||
# to avoid the update (from the SD card) aborting
|
||||
rm --force "${UMBREL_ROOT}/statuses/update-in-progress" || true
|
||||
|
||||
echo "Yes, it does, attempting an automatic update..."
|
||||
"${UMBREL_ROOT}/scripts/update/update" --path "${SD_UMBREL_ROOT}"
|
||||
$set_status sdcard-update completed
|
||||
}
|
||||
|
||||
main
|
||||
@@ -1,98 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
DOCKER_ROOT="/var/lib/docker"
|
||||
|
||||
if [[ "$EUID" -ne 0 ]]; then
|
||||
echo "Please run as root"
|
||||
exit
|
||||
fi
|
||||
|
||||
show_help() {
|
||||
cat << EOF
|
||||
fdocker 0.0.1
|
||||
|
||||
CLI for managing Docker
|
||||
|
||||
Usage: docker <command> <arguments...>
|
||||
|
||||
Commands:
|
||||
clean Cleanup containers using 'containers' as argument
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
if [ -z ${2+x} ]; then
|
||||
command=""
|
||||
else
|
||||
command="${1}"
|
||||
fi
|
||||
|
||||
function delete_mount() {
|
||||
id_path="${1}"
|
||||
|
||||
overlay_path="${DOCKER_ROOT}/overlay2"
|
||||
|
||||
if [[ -f "${id_path}" ]]; then
|
||||
overlay_id=$(cat "${id_path}")
|
||||
|
||||
overlay_path="${overlay_path}/${overlay_id}"
|
||||
|
||||
if [[ -d "${overlay_path}" ]]; then
|
||||
echo "Deleting: ${overlay_path}"
|
||||
|
||||
rm -rf "${overlay_path}"
|
||||
fi
|
||||
|
||||
rm -rf "${id_path}"
|
||||
fi
|
||||
}
|
||||
|
||||
function cleanup_containers() {
|
||||
echo "Cleaning up containers..."
|
||||
|
||||
containers_path="${DOCKER_ROOT}/containers"
|
||||
mounts_path="${DOCKER_ROOT}/image/overlay2/layerdb/mounts"
|
||||
|
||||
# Loop through all the containers
|
||||
containers="${containers_path}/*"
|
||||
for dir in $containers; do
|
||||
container_id=$(basename "${dir}")
|
||||
|
||||
if [[ ! -d "${dir}" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Removing container: ${container_id}"
|
||||
|
||||
# Determine the overlay IDs from the mounts
|
||||
delete_mount "${mounts_path}/${container_id}/init-id"
|
||||
delete_mount "${mounts_path}/${container_id}/mount-id"
|
||||
|
||||
# Delete the container folder
|
||||
rm -rf "${dir}"
|
||||
|
||||
echo
|
||||
done
|
||||
|
||||
echo "Done"
|
||||
}
|
||||
|
||||
if [[ "${command}" == "clean" ]]; then
|
||||
resource="${2}"
|
||||
|
||||
if [[ "${resource}" == "containers" ]]; then
|
||||
cleanup_containers
|
||||
else
|
||||
echo "Cannot cleanup: ${resource}"
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit
|
||||
fi
|
||||
|
||||
# If we get here it means no valid command was supplied
|
||||
# Show help and exit
|
||||
show_help
|
||||
exit 1
|
||||
@@ -1,39 +0,0 @@
|
||||
# Full credits: https://github.com/raspberrypi-ui/agnostics
|
||||
|
||||
# Use FIO to emulate the Apps Class A1 performance test.
|
||||
# This isn't an exact benchmark as the card isn't in the state required by the
|
||||
# specification, but is good enough as a sniff test.
|
||||
#
|
||||
[global]
|
||||
ioengine=libaio
|
||||
iodepth=4
|
||||
size=64m
|
||||
direct=1
|
||||
end_fsync=1
|
||||
directory=/var/tmp
|
||||
filename=sd.test.file
|
||||
|
||||
[prepare-file]
|
||||
rw=write
|
||||
bs=512k
|
||||
stonewall
|
||||
|
||||
[seq-write]
|
||||
rw=write
|
||||
bs=512k
|
||||
stonewall
|
||||
|
||||
[rand-4k-write]
|
||||
rw=randwrite
|
||||
bs=4k
|
||||
runtime=10
|
||||
stonewall
|
||||
|
||||
[rand-4k-read]
|
||||
rw=randread
|
||||
bs=4k
|
||||
runtime=10
|
||||
stonewall
|
||||
|
||||
# execute with command $ fio --output-format=terse sd_bench.fio | cut -f 3,7,8,48,49 -d";" -
|
||||
# testname, read bandwidth, read iops, write bandwidth, write iops
|
||||
@@ -1,521 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# https://github.com/qzb/sh-semver
|
||||
# Commit: 2ac2437
|
||||
|
||||
_num_part='([0-9]|[1-9][0-9]*)'
|
||||
_lab_part='([0-9]|[1-9][0-9]*|[0-9]*[a-zA-Z-][a-zA-Z0-9-]*)'
|
||||
_met_part='([0-9A-Za-z-]+)'
|
||||
|
||||
RE_NUM="$_num_part(\.$_num_part)*"
|
||||
RE_LAB="$_lab_part(\.$_lab_part)*"
|
||||
RE_MET="$_met_part(\.$_met_part)*"
|
||||
RE_VER="[ \t]*$RE_NUM(-$RE_LAB)?(\+$RE_MET)?"
|
||||
|
||||
BRE_DIGIT='[0-9]\{1,\}'
|
||||
BRE_ALNUM='[0-9a-zA-Z-]\{1,\}'
|
||||
BRE_IDENT="$BRE_ALNUM\(\.$BRE_ALNUM\)*"
|
||||
|
||||
BRE_MAJOR="$BRE_DIGIT"
|
||||
BRE_MINOR="\(\.$BRE_DIGIT\)\{0,1\}"
|
||||
BRE_PATCH="\(\.$BRE_DIGIT\)\{0,1\}"
|
||||
BRE_PRERE="\(-$BRE_IDENT\)\{0,1\}"
|
||||
BRE_BUILD="\(+$BRE_IDENT\)\{0,1\}"
|
||||
BRE_VERSION="${BRE_MAJOR}${BRE_MINOR}${BRE_PATCH}${BRE_PRERE}${BRE_BUILD}"
|
||||
|
||||
filter()
|
||||
{
|
||||
local text="$1"
|
||||
local regex="$2"
|
||||
shift 2
|
||||
echo "$text" | grep -E "$@" "$regex"
|
||||
}
|
||||
|
||||
# Gets number part from normalized version
|
||||
get_number()
|
||||
{
|
||||
echo "${1%%-*}"
|
||||
}
|
||||
|
||||
# Gets prerelase part from normalized version
|
||||
get_prerelease()
|
||||
{
|
||||
local pre_and_meta=${1%+*}
|
||||
local pre=${pre_and_meta#*-}
|
||||
if [ "$pre" = "$1" ]; then
|
||||
echo
|
||||
else
|
||||
echo "$pre"
|
||||
fi
|
||||
}
|
||||
|
||||
# Gets major number from normalized version
|
||||
get_major()
|
||||
{
|
||||
echo "${1%%.*}"
|
||||
}
|
||||
|
||||
# Gets minor number from normalized version
|
||||
get_minor()
|
||||
{
|
||||
local minor_major_bug=${1%%-*}
|
||||
local minor_major=${minor_major_bug%.*}
|
||||
local minor=${minor_major#*.}
|
||||
|
||||
if [ "$minor" = "$minor_major" ]; then
|
||||
echo
|
||||
else
|
||||
echo "$minor"
|
||||
fi
|
||||
}
|
||||
|
||||
get_bugfix()
|
||||
{
|
||||
local minor_major_bug=${1%%-*}
|
||||
local bugfix=${minor_major_bug##*.*.}
|
||||
|
||||
if [ "$bugfix" = "$minor_major_bug" ]; then
|
||||
echo
|
||||
else
|
||||
echo "$bugfix"
|
||||
fi
|
||||
}
|
||||
|
||||
strip_metadata()
|
||||
{
|
||||
echo "${1%+*}"
|
||||
}
|
||||
|
||||
semver_eq()
|
||||
{
|
||||
local ver1 ver2 part1 part2
|
||||
ver1=$(get_number "$1")
|
||||
ver2=$(get_number "$2")
|
||||
|
||||
local count=1
|
||||
while true; do
|
||||
part1=$(echo "$ver1"'.' | cut -d '.' -f $count)
|
||||
part2=$(echo "$ver2"'.' | cut -d '.' -f $count)
|
||||
|
||||
if [ -z "$part1" ] || [ -z "$part2" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
if [ "$part1" != "$part2" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local count=$(( count + 1 ))
|
||||
done
|
||||
|
||||
if [ "$(get_prerelease "$1")" = "$(get_prerelease "$2")" ]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
semver_lt()
|
||||
{
|
||||
local number_a number_b prerelease_a prerelease_b
|
||||
number_a=$(get_number "$1")
|
||||
number_b=$(get_number "$2")
|
||||
prerelease_a=$(get_prerelease "$1")
|
||||
prerelease_b=$(get_prerelease "$2")
|
||||
|
||||
|
||||
local head_a=''
|
||||
local head_b=''
|
||||
local rest_a=$number_a.
|
||||
local rest_b=$number_b.
|
||||
while [ -n "$rest_a" ] || [ -n "$rest_b" ]; do
|
||||
head_a=${rest_a%%.*}
|
||||
head_b=${rest_b%%.*}
|
||||
rest_a=${rest_a#*.}
|
||||
rest_b=${rest_b#*.}
|
||||
|
||||
if [ -z "$head_a" ] || [ -z "$head_b" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$head_a" -eq "$head_b" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$head_a" -lt "$head_b" ]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$prerelease_a" ] && [ -z "$prerelease_b" ]; then
|
||||
return 0
|
||||
elif [ -z "$prerelease_a" ] && [ -n "$prerelease_b" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local head_a=''
|
||||
local head_b=''
|
||||
local rest_a=$prerelease_a.
|
||||
local rest_b=$prerelease_b.
|
||||
while [ -n "$rest_a" ] || [ -n "$rest_b" ]; do
|
||||
head_a=${rest_a%%.*}
|
||||
head_b=${rest_b%%.*}
|
||||
rest_a=${rest_a#*.}
|
||||
rest_b=${rest_b#*.}
|
||||
|
||||
if [ -z "$head_a" ] && [ -n "$head_b" ]; then
|
||||
return 0
|
||||
elif [ -n "$head_a" ] && [ -z "$head_b" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$head_a" = "$head_b" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# If both are numbers then compare numerically
|
||||
if [ "$head_a" = "${head_a%[!0-9]*}" ] && [ "$head_b" = "${head_b%[!0-9]*}" ]; then
|
||||
[ "$head_a" -lt "$head_b" ] && return 0 || return 1
|
||||
# If only a is a number then return true (number has lower precedence than strings)
|
||||
elif [ "$head_a" = "${head_a%[!0-9]*}" ]; then
|
||||
return 0
|
||||
# If only b is a number then return false
|
||||
elif [ "$head_b" = "${head_b%[!0-9]*}" ]; then
|
||||
return 1
|
||||
# Finally if of identifiers is a number compare them lexically
|
||||
else
|
||||
test "$head_a" \< "$head_b" && return 0 || return 1
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
semver_gt()
|
||||
{
|
||||
if semver_lt "$1" "$2" || semver_eq "$1" "$2"; then
|
||||
return 1
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
semver_le()
|
||||
{
|
||||
semver_gt "$1" "$2" && return 1 || return 0
|
||||
}
|
||||
|
||||
semver_ge()
|
||||
{
|
||||
semver_lt "$1" "$2" && return 1 || return 0
|
||||
}
|
||||
|
||||
semver_sort()
|
||||
{
|
||||
if [ $# -le 1 ]; then
|
||||
echo "$1"
|
||||
return
|
||||
fi
|
||||
|
||||
local pivot=$1
|
||||
local args_a=()
|
||||
local args_b=()
|
||||
|
||||
shift 1
|
||||
|
||||
for ver in "$@"; do
|
||||
if semver_le "$ver" "$pivot"; then
|
||||
args_a=( "${args_a[@]}" "$ver" )
|
||||
else
|
||||
args_b=( "$ver" "${args_b[@]}" )
|
||||
fi
|
||||
done
|
||||
|
||||
args_a=( $(semver_sort "${args_a[@]}") )
|
||||
args_b=( $(semver_sort "${args_b[@]}") )
|
||||
echo "${args_a[@]}" "$pivot" "${args_b[@]}"
|
||||
}
|
||||
|
||||
regex_match()
|
||||
{
|
||||
local string="$1 "
|
||||
local regexp="$2"
|
||||
local match
|
||||
match="$(eval "echo '$string' | grep -E -o '^[ \t]*($regexp)[ \t]+'")";
|
||||
|
||||
for i in $(seq 0 9); do
|
||||
unset "MATCHED_VER_$i"
|
||||
unset "MATCHED_NUM_$i"
|
||||
done
|
||||
unset REST
|
||||
|
||||
if [ -z "$match" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local match_len=${#match}
|
||||
REST="${string:$match_len}"
|
||||
|
||||
local part
|
||||
local i=1
|
||||
for part in $string; do
|
||||
local ver num
|
||||
ver="$(eval "echo '$part' | grep -E -o '$RE_VER' | head -n 1 | sed 's/ \t//g'")";
|
||||
num=$(get_number "$ver")
|
||||
|
||||
if [ -n "$ver" ]; then
|
||||
eval "MATCHED_VER_$i='$ver'"
|
||||
eval "MATCHED_NUM_$i='$num'"
|
||||
i=$(( i + 1 ))
|
||||
fi
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Normalizes rules string
|
||||
#
|
||||
# * replaces chains of whitespaces with single spaces
|
||||
# * replaces whitespaces around hyphen operator with "_"
|
||||
# * removes wildcards from version numbers (1.2.* -> 1.2)
|
||||
# * replaces "x" with "*"
|
||||
# * removes whitespace between operators and version numbers
|
||||
# * removes leading "v" from version numbers
|
||||
# * removes leading and trailing spaces
|
||||
normalize_rules()
|
||||
{
|
||||
echo " $1" \
|
||||
| sed 's/\\t/ /g' \
|
||||
| sed 's/ / /g' \
|
||||
| sed 's/ \{2,\}/ /g' \
|
||||
| sed 's/ - /_-_/g' \
|
||||
| sed 's/\([~^<>=]\) /\1/g' \
|
||||
| sed 's/\([ _~^<>=]\)v/\1/g' \
|
||||
| sed 's/\.[xX*]//g' \
|
||||
| sed 's/[xX]/*/g' \
|
||||
| sed 's/^ //g' \
|
||||
| sed 's/ $//g'
|
||||
}
|
||||
|
||||
# Reads rule from provided string
|
||||
resolve_rule()
|
||||
{
|
||||
local rule operator operands
|
||||
rule="$1"
|
||||
operator="$( echo "$rule" | sed "s/$BRE_VERSION/#/g" )"
|
||||
operands=( $( echo "$rule" | grep -o "$BRE_VERSION") )
|
||||
|
||||
case "$operator" in
|
||||
'*') echo "all" ;;
|
||||
'#') echo "eq ${operands[0]}" ;;
|
||||
'=#') echo "eq ${operands[0]}" ;;
|
||||
'<#') echo "lt ${operands[0]}" ;;
|
||||
'>#') echo "gt ${operands[0]}" ;;
|
||||
'<=#') echo "le ${operands[0]}" ;;
|
||||
'>=#') echo "ge ${operands[0]}" ;;
|
||||
'#_-_#') echo "ge ${operands[0]}"
|
||||
echo "le ${operands[1]}" ;;
|
||||
'~#') echo "tilde ${operands[0]}" ;;
|
||||
'^#') echo "caret ${operands[0]}" ;;
|
||||
*) return 1
|
||||
esac
|
||||
}
|
||||
|
||||
resolve_rules()
|
||||
{
|
||||
local rules
|
||||
rules="$(normalize_rules "$1")"
|
||||
IFS=' ' read -ra rules <<< "${rules:-all}"
|
||||
|
||||
for rule in "${rules[@]}"; do
|
||||
resolve_rule "$rule"
|
||||
done
|
||||
}
|
||||
|
||||
rule_eq()
|
||||
{
|
||||
local rule_ver="$1"
|
||||
local tested_ver="$2"
|
||||
|
||||
semver_eq "$tested_ver" "$rule_ver" && return 0 || return 1;
|
||||
}
|
||||
|
||||
rule_le()
|
||||
{
|
||||
local rule_ver="$1"
|
||||
local tested_ver="$2"
|
||||
|
||||
semver_le "$tested_ver" "$rule_ver" && return 0 || return 1;
|
||||
}
|
||||
|
||||
rule_lt()
|
||||
{
|
||||
local rule_ver="$1"
|
||||
local tested_ver="$2"
|
||||
|
||||
semver_lt "$tested_ver" "$rule_ver" && return 0 || return 1;
|
||||
}
|
||||
|
||||
rule_ge()
|
||||
{
|
||||
local rule_ver="$1"
|
||||
local tested_ver="$2"
|
||||
|
||||
semver_ge "$tested_ver" "$rule_ver" && return 0 || return 1;
|
||||
}
|
||||
|
||||
rule_gt()
|
||||
{
|
||||
local rule_ver="$1"
|
||||
local tested_ver="$2"
|
||||
|
||||
semver_gt "$tested_ver" "$rule_ver" && return 0 || return 1;
|
||||
}
|
||||
|
||||
rule_tilde()
|
||||
{
|
||||
local rule_ver="$1"
|
||||
local tested_ver="$2"
|
||||
|
||||
if rule_ge "$rule_ver" "$tested_ver"; then
|
||||
local rule_major rule_minor
|
||||
rule_major=$(get_major "$rule_ver")
|
||||
rule_minor=$(get_minor "$rule_ver")
|
||||
|
||||
if [ -n "$rule_minor" ] && rule_eq "$rule_major.$rule_minor" "$(get_number "$tested_ver")"; then
|
||||
return 0
|
||||
fi
|
||||
if [ -z "$rule_minor" ] && rule_eq "$rule_major" "$(get_number "$tested_ver")"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
rule_caret()
|
||||
{
|
||||
local rule_ver="$1"
|
||||
local tested_ver="$2"
|
||||
|
||||
if rule_ge "$rule_ver" "$tested_ver"; then
|
||||
local rule_major
|
||||
rule_major="$(get_major "$rule_ver")"
|
||||
|
||||
if [ "$rule_major" != "0" ] && rule_eq "$rule_major" "$(get_number "$tested_ver")"; then
|
||||
return 0
|
||||
fi
|
||||
if [ "$rule_major" = "0" ] && rule_eq "$rule_ver" "$(get_number "$tested_ver")"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
rule_all()
|
||||
{
|
||||
return 0
|
||||
}
|
||||
|
||||
apply_rules()
|
||||
{
|
||||
local rules_string="$1"
|
||||
shift
|
||||
local versions=( "$@" )
|
||||
|
||||
# Loop over sets of rules (sets of rules are separated with ||)
|
||||
for ver in "${versions[@]}"; do
|
||||
rules_tail="$rules_string";
|
||||
|
||||
while [ -n "$rules_tail" ]; do
|
||||
head="${rules_tail%%||*}"
|
||||
|
||||
if [ "$head" = "$rules_tail" ]; then
|
||||
rules_string=""
|
||||
else
|
||||
rules_tail="${rules_tail#*||}"
|
||||
fi
|
||||
|
||||
#if [ -z "$head" ] || [ -n "$(echo "$head" | grep -E -x '[ \t]*')" ]; then
|
||||
#group=$(( $group + 1 ))
|
||||
#continue
|
||||
#fi
|
||||
|
||||
rules="$(resolve_rules "$head")"
|
||||
|
||||
# If specified rule cannot be recognised - end with error
|
||||
if [ $? -eq 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! echo "$ver" | grep -q -E -x "[v=]?[ \t]*$RE_VER"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
ver=$(echo "$ver" | grep -E -x "$RE_VER")
|
||||
|
||||
success=true
|
||||
allow_prerel=false
|
||||
if $FORCE_ALLOW_PREREL; then
|
||||
allow_prerel=true
|
||||
fi
|
||||
|
||||
while read -r rule; do
|
||||
comparator="${rule%% *}"
|
||||
operand="${rule#* }"
|
||||
|
||||
if [ -n "$(get_prerelease "$operand")" ] && semver_eq "$(get_number "$operand")" "$(get_number "$ver")" || [ "$rule" = "all" ]; then
|
||||
allow_prerel=true
|
||||
fi
|
||||
|
||||
"rule_$comparator" "$operand" "$ver"
|
||||
if [ $? -eq 1 ]; then
|
||||
success=false
|
||||
break
|
||||
fi
|
||||
done <<< "$rules"
|
||||
|
||||
if $success; then
|
||||
if [ -z "$(get_prerelease "$ver")" ] || $allow_prerel; then
|
||||
echo "$ver"
|
||||
break;
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
group=$(( group + 1 ))
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
|
||||
FORCE_ALLOW_PREREL=false
|
||||
USAGE="Usage: $0 [-r <rule>] [<version>... ]
|
||||
|
||||
Omitting <version>s reads them from STDIN.
|
||||
Omitting -r <rule> simply sorts the versions according to semver ordering."
|
||||
|
||||
while getopts ar:h o; do
|
||||
case "$o" in
|
||||
a) FORCE_ALLOW_PREREL=true ;;
|
||||
r) RULES_STRING="$OPTARG||";;
|
||||
h) echo "$USAGE" && exit ;;
|
||||
?) echo "$USAGE" && exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
shift $(( OPTIND-1 ))
|
||||
|
||||
VERSIONS=( ${@:-$(cat -)} )
|
||||
|
||||
# Sort versions
|
||||
VERSIONS=( $(semver_sort "${VERSIONS[@]}") )
|
||||
|
||||
if [ -z "$RULES_STRING" ]; then
|
||||
printf '%s\n' "${VERSIONS[@]}"
|
||||
else
|
||||
apply_rules "$RULES_STRING" "${VERSIONS[@]}"
|
||||
fi
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)"
|
||||
STATUS_FILE_PATH="/umbrel-status"
|
||||
|
||||
set_status="/status-server/set-status"
|
||||
|
||||
check_root () {
|
||||
if [[ $UID != 0 ]]; then
|
||||
echo "This script must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_umbrel_os () {
|
||||
[[ -f "/etc/default/umbrel" ]] && source "/etc/default/umbrel"
|
||||
if [[ -z "${UMBREL_OS:-}" ]]; then
|
||||
echo "This script must only be run on Umbrel OS"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
has_service_failed () {
|
||||
systemctl is-failed --quiet '*'
|
||||
}
|
||||
|
||||
has_umbrel_started () {
|
||||
cat "${STATUS_FILE_PATH}" | grep --silent '^umbrel:completed$'
|
||||
}
|
||||
|
||||
has_error_been_caught () {
|
||||
cat "${STATUS_FILE_PATH}" | grep --silent ':errored:'
|
||||
}
|
||||
|
||||
check_root
|
||||
check_umbrel_os
|
||||
|
||||
$set_status service-monitor started
|
||||
echo "Starting service monitor..."
|
||||
|
||||
while true
|
||||
do
|
||||
if has_umbrel_started
|
||||
then
|
||||
echo "Umbrel successfully started, exiting service monitor!"
|
||||
exit 0
|
||||
fi
|
||||
if has_service_failed
|
||||
then
|
||||
echo "Service failed!"
|
||||
if has_error_been_caught
|
||||
then
|
||||
echo "Error already caught, doing nothing."
|
||||
else
|
||||
echo "Error not caught, writing an error."
|
||||
$set_status service-monitor errored service-failed
|
||||
fi
|
||||
echo "Exiting service monitor"
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
@@ -1,20 +0,0 @@
|
||||
# Umbrel Connection Details Logger (TTY)
|
||||
# Installed at /etc/systemd/system/umbrel-connection-details.service
|
||||
|
||||
[Unit]
|
||||
Description=Connection Details
|
||||
Requires=umbrel-startup.service
|
||||
After=umbrel-startup.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Restart=no
|
||||
ExecStart=/home/umbrel/umbrel/scripts/umbrel-os/umbrel-details
|
||||
User=root
|
||||
Group=root
|
||||
StandardOutput=tty
|
||||
TTYPath=/dev/tty1
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,19 +0,0 @@
|
||||
# Umbrel Docker Cleanup
|
||||
# Installed at /etc/systemd/system/umbrel-docker-cleanup.service
|
||||
|
||||
[Unit]
|
||||
Description=Umbrel Docker Cleanup
|
||||
After=umbrel-external-storage.service
|
||||
Before=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/home/umbrel/umbrel/scripts/umbrel-os/fdocker clean containers
|
||||
User=root
|
||||
Group=root
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=umbrel docker cleanup
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,26 +0,0 @@
|
||||
# Umbrel External Storage SDcard Updater
|
||||
# Installed at /etc/systemd/system/umbrel-external-storage-sdcard-update.service
|
||||
|
||||
[Unit]
|
||||
Description=External Storage SDcard Updater
|
||||
Requires=umbrel-external-storage.service
|
||||
After=umbrel-external-storage.service
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
Wants=docker.service
|
||||
After=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Restart=no
|
||||
ExecStart=/home/umbrel/umbrel/scripts/umbrel-os/external-storage/update-from-sdcard
|
||||
TimeoutStartSec=45min
|
||||
User=root
|
||||
Group=root
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=external storage updater
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,21 +0,0 @@
|
||||
# Umbrel Service Monitor
|
||||
# Installed at /etc/systemd/system/umbrel-service-monitor.service
|
||||
|
||||
[Unit]
|
||||
Description=Service Monitor
|
||||
After=umbrel-status-server.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Restart=no
|
||||
ExecStart=/home/umbrel/umbrel/scripts/umbrel-os/service-monitor
|
||||
TimeoutStartSec=45min
|
||||
User=root
|
||||
Group=root
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=service monitor
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,35 +0,0 @@
|
||||
# Umbrel Startup Service
|
||||
# Installed at /etc/systemd/system/umbrel-startup.service
|
||||
|
||||
[Unit]
|
||||
Description=Umbrel Startup Service
|
||||
Requires=umbrel-external-storage.service
|
||||
After=umbrel-external-storage.service
|
||||
Requires=umbrel-external-storage-sdcard-update.service
|
||||
After=umbrel-external-storage-sdcard-update.service
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
Wants=docker.service
|
||||
After=docker.service
|
||||
|
||||
# This prevents us hitting restart rate limits and ensures we keep restarting
|
||||
# indefinitely.
|
||||
StartLimitInterval=0
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
TimeoutStartSec=infinity
|
||||
TimeoutStopSec=16min
|
||||
ExecStart=/home/umbrel/umbrel/scripts/start
|
||||
ExecStop=/home/umbrel/umbrel/scripts/stop
|
||||
User=root
|
||||
Group=root
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=umbrel startup
|
||||
RemainAfterExit=yes
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,25 +0,0 @@
|
||||
# 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
|
||||
@@ -1,21 +0,0 @@
|
||||
# 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
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/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}"
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/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"
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../../..)"
|
||||
|
||||
source /etc/default/umbrel 2> /dev/null || true
|
||||
if [[ -z "${UMBREL_OS:-}" ]]; then
|
||||
echo "Exiting iptables setup when not on Umbrel OS"
|
||||
exit
|
||||
fi
|
||||
|
||||
delete="false"
|
||||
if [[ "${@}" == *"--delete"* ]]; then
|
||||
delete="true"
|
||||
fi
|
||||
|
||||
force="false"
|
||||
if [[ "${@}" == *"--force"* ]]; then
|
||||
force="true"
|
||||
fi
|
||||
|
||||
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
|
||||
if [[ "${delete}" == "false" ]]; then
|
||||
position="--append"
|
||||
if [[ "${force}" == "true" ]]; then
|
||||
position="--insert"
|
||||
fi
|
||||
iptables "${position}" ${rule[@]}
|
||||
echo "Appended new iptables entry."
|
||||
fi
|
||||
}
|
||||
|
||||
main
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,130 +0,0 @@
|
||||
<!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 service-failed">
|
||||
<p><b class="text-dark">Error: System service failed</b></p>
|
||||
<p>Something went wrong when starting Umbrel. Need help? Feel free to jump on our <a href="https://community.getumbrel.com" target="_blank">community forum</a> or <a href="https://t.me/getumbrel" target="_blank">Telegram chat</a>.</p>
|
||||
</div>
|
||||
|
||||
<div class="error-text docker-failed">
|
||||
<p><b class="text-dark">Error: Failed to start containers</b></p>
|
||||
<p>Something went wrong when starting Umbrel. Need help? Feel free to jump on our <a href="https://community.getumbrel.com" target="_blank">community forum</a> or <a href="https://t.me/getumbrel" target="_blank">Telegram chat</a>.</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>
|
||||
<p>Note: If it's not an external drive that you have used with Umbrel previously, all of the data on it (if any) will be erased.</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>
|
||||
|
||||
<div class="error-text mount-failed">
|
||||
<p><b class="text-dark">Error: Failed to mount external drive</b></p>
|
||||
<p>This normally happens if you've experienced filesystem corruption. Need help? Feel free to jump on our <a href="https://community.getumbrel.com" target="_blank">community forum</a> or <a href="https://t.me/getumbrel" target="_blank">Telegram chat</a>.</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>
|
||||
@@ -1,117 +0,0 @@
|
||||
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();
|
||||
@@ -1,116 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
.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="service-failed"] .error-text.service-failed,
|
||||
[data-error="docker-failed"] .error-text.docker-failed,
|
||||
[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,
|
||||
[data-error="mount-failed"] .error-text.mount-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)}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/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()
|
||||
@@ -1,9 +0,0 @@
|
||||
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()
|
||||
@@ -1,88 +0,0 @@
|
||||
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()
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,109 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
import io
|
||||
import qrcode
|
||||
|
||||
umbrel_ascii = '''
|
||||
,;###GGGGGGGGGGl#Sp
|
||||
,##GGGlW""^' '`""%GGGG#S,
|
||||
,#GGG" "lGG#o
|
||||
#GGl^ '$GG#
|
||||
,#GGb \GGG,
|
||||
lGG" "GGG
|
||||
#GGGlGGGl##p,,p##lGGl##p,,p###ll##GGGG
|
||||
!GGGlW"""*GGGGGGG#""""WlGGGGG#W""*WGGGGS
|
||||
"" "^ '" ""
|
||||
|
||||
|
||||
@GGS lG#
|
||||
!GGG !GGG
|
||||
!GGG !GGG
|
||||
!GGG !GGG
|
||||
!GGG !GGG
|
||||
!GGG !GGG
|
||||
'GGG $GGl
|
||||
"GGG#psqp##GG#
|
||||
"%GGGGGG#"
|
||||
'''.strip('\n')
|
||||
|
||||
def create_qr(data):
|
||||
output_buffer = io.TextIOWrapper(io.BytesIO(), sys.stdout.encoding)
|
||||
|
||||
qr = qrcode.QRCode(border=0, error_correction=qrcode.constants.ERROR_CORRECT_Q)
|
||||
qr.add_data(data)
|
||||
qr.print_ascii(out=output_buffer)
|
||||
|
||||
output_buffer.seek(0)
|
||||
qr_ascii = output_buffer.read().strip()
|
||||
|
||||
return qr_ascii
|
||||
|
||||
def combine_ascii(left_ascii, right_ascii, spacing=0):
|
||||
left_lines = left_ascii.split('\n')
|
||||
right_lines = right_ascii.split('\n')
|
||||
|
||||
no_left_lines = len(left_lines)
|
||||
no_right_lines = len(right_lines)
|
||||
|
||||
left_width = max(list(map(lambda line: len(line), left_lines)))
|
||||
max_height = max(no_left_lines, no_right_lines)
|
||||
|
||||
lines = []
|
||||
for i in range(max_height):
|
||||
left_line = left_lines[i] if i < no_left_lines else ""
|
||||
right_line = right_lines[i] if i < no_right_lines else ""
|
||||
|
||||
padding = " " * ((left_width - len(left_line)) + spacing)
|
||||
|
||||
lines.append(left_line + padding + right_line)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def read_file_when_available(file_path, timeout):
|
||||
start = time.time()
|
||||
while not os.path.exists(file_path):
|
||||
if (time.time() - start) > timeout:
|
||||
return False
|
||||
time.sleep(1)
|
||||
|
||||
with open(file_path, "r") as file:
|
||||
file_contents = file.read()
|
||||
|
||||
return file_contents
|
||||
|
||||
def run(command):
|
||||
result = subprocess.run(command, stdout=subprocess.PIPE)
|
||||
return result.stdout.decode('utf-8').rstrip("\n")
|
||||
|
||||
def main():
|
||||
timeout = 30
|
||||
tor_hostname_file = "/home/umbrel/umbrel/tor/data/web/hostname"
|
||||
tor_hostname = read_file_when_available(tor_hostname_file, timeout)
|
||||
|
||||
ip = run(['hostname', '-I']).split(" ")[0]
|
||||
|
||||
if not tor_hostname or not ip:
|
||||
print("Couldn't get connection details")
|
||||
return
|
||||
|
||||
tor_hostname_qr_ascii = create_qr(tor_hostname.strip())
|
||||
ascii_banner = combine_ascii(umbrel_ascii, tor_hostname_qr_ascii, spacing=10)
|
||||
|
||||
connection_details = f"""
|
||||
|
||||
|
||||
{ascii_banner}
|
||||
|
||||
Your Umbrel is up and running at:
|
||||
|
||||
http://umbrel.local
|
||||
http://{ip}
|
||||
http://{tor_hostname}
|
||||
"""
|
||||
print(connection_details)
|
||||
|
||||
main()
|
||||
@@ -1,6 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# This script is used to bootstrap the mender update process
|
||||
# The update server references it in the form:
|
||||
# https://raw.githubusercontent.com/getumbrel/umbrel/<tag>/scripts/update-script
|
||||
|
||||
update_url=""
|
||||
|
||||
if ! command -v mender &> /dev/null
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# This script needs to be in this exact location for legacy reasons.
|
||||
#
|
||||
# It bootstraps the umbrelOS 1.0 update for old 0.5.x Umbrel installs.
|
||||
# During OTA they download the latest release tarball from GitHub, extract
|
||||
# it and then execute ./scripts/*-run.sh to process the update. So now we just
|
||||
# have this ./scripts/bootstrap-run.sh in this exact location in the codebase
|
||||
# so old Umbrel installs can bootstrap into the new mender based update system
|
||||
# and reboot into the new OS.
|
||||
|
||||
RELEASE=$1
|
||||
UMBREL_ROOT=$2
|
||||
UPDATE_ROOT=$(readlink -f "${PWD}/../..")
|
||||
|
||||
# Make sure we correctly display an error if the bootstrap fails
|
||||
function fail_update() {
|
||||
reason="${1}"
|
||||
echo "${reason}" | tee "${UMBREL_ROOT}/statuses/update-failure"
|
||||
echo "Update failed!"
|
||||
exit 1
|
||||
}
|
||||
|
||||
function is_pi() {
|
||||
cat /proc/cpuinfo | grep --quiet 'Raspberry Pi'
|
||||
}
|
||||
|
||||
function is_home() {
|
||||
dmidecode -t system | grep --silent 'Umbrel Home'
|
||||
}
|
||||
|
||||
echo "Hardware support detection"
|
||||
|
||||
# Raspberry Pi
|
||||
if is_pi
|
||||
then
|
||||
echo "Raspberry Pi detected"
|
||||
fail_update "Please install umbrelOS 1.0 on your microSD card to update: https://link.umbrel.com/pi-update"
|
||||
fi
|
||||
|
||||
# Not an Umbrel Home (custom Linux install)
|
||||
if ! is_home
|
||||
then
|
||||
echo "Custon Linux install detected"
|
||||
fail_update "umbrelOS 1.0 is not yet supported for custom Linux installs: https://link.umbrel.com/linux-update"
|
||||
fi
|
||||
|
||||
# If we get here we're running on an Umbrel Home
|
||||
echo "Umbrel Home detected"
|
||||
|
||||
# Flash new update
|
||||
echo '{"state": "installing", "progress": 25, "description": "Installing umbrelOS 1.0, this may take a while", "updateTo": ""}' > "${UMBREL_ROOT}/statuses/update-status.json"
|
||||
mender install https://download.umbrel.com/release/1.0.4/umbrelos-amd64.update
|
||||
|
||||
# Reboot
|
||||
cat <<EOF > "$UMBREL_ROOT"/statuses/update-status.json
|
||||
{"state": "installing", "progress": 97, "description": "Restarting Umbrel Home"}
|
||||
EOF
|
||||
|
||||
# Sleep 5 seconds to give the frontend a chance to poll
|
||||
# and pull in latest update status
|
||||
echo "Sleeping 5 seconds"
|
||||
sleep 5
|
||||
|
||||
# The following two cleanups shouldn't be needed since we'll reboot into a 1.0 install
|
||||
# but we do it just incase something goes wrong and we end up back in 0.5.x
|
||||
|
||||
# Remove update lockfile so the next OTA update doesn't fail
|
||||
rm -f "${UMBREL_ROOT}/statuses/update-in-progress"
|
||||
|
||||
# Write out a status file which will be used within scripts/start
|
||||
# To complete the update when the system boots up again
|
||||
touch "${UMBREL_ROOT}/statuses/umbrel-update-reboot-performed"
|
||||
shutdown --reboot now
|
||||
exit 0
|
||||
@@ -1,178 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [[ $UID != 0 ]]; then
|
||||
echo "Update script must be run as root"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_dependencies () {
|
||||
for cmd in "$@"; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "This script requires \"${cmd}\" to be installed"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
check_semver_range () {
|
||||
local range="${1}"
|
||||
local version="${2}"
|
||||
"${UMBREL_ROOT}/scripts/umbrel-os/semver" -r "${range}" "${version}" | grep --quiet "^${version}$"
|
||||
}
|
||||
|
||||
check_dependencies jq curl rsync
|
||||
|
||||
UMBREL_ROOT="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../..)"
|
||||
|
||||
update_type=""
|
||||
if [[ "${1}" == "--ota" ]]; then
|
||||
update_type="ota"
|
||||
elif [[ "${1}" == "--path" ]]; then
|
||||
update_type="path"
|
||||
update_path="${2}"
|
||||
elif [[ "${1}" == "--repo" ]]; then
|
||||
update_type="repo"
|
||||
descriptor="${2}"
|
||||
if [[ "${descriptor}" != *"#"* ]]; then
|
||||
descriptor="${descriptor}#master"
|
||||
fi
|
||||
repo="${descriptor%%#*}"
|
||||
branch="${descriptor#*#}"
|
||||
else
|
||||
echo "Update requires \"--ota\", \"--path <path>\", or \"--repo user/repo[#branch]\" option."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${update_type}" == "repo" ]]; then
|
||||
repo_path="/tmp/umbrel-update/"
|
||||
rm -rf "${repo_path}" 2> /dev/null || true
|
||||
mkdir -p "${repo_path}"
|
||||
git clone --single-branch --branch "${branch}" "https://github.com/${repo}.git" "${repo_path}"
|
||||
|
||||
# Patch version to show development build
|
||||
commit=$(git --git-dir "${repo_path}/.git" rev-parse --short HEAD)
|
||||
mv "${repo_path}/info.json" "${repo_path}/info.json.orig"
|
||||
jq \
|
||||
--arg commit "${commit}" \
|
||||
'.build = $commit' \
|
||||
"${repo_path}/info.json.orig" \
|
||||
> "${repo_path}/info.json"
|
||||
|
||||
# Now hand over to the path updater
|
||||
update_type="path"
|
||||
update_path="${repo_path}"
|
||||
fi
|
||||
|
||||
if [[ "${update_type}" == "path" ]] && [[ ! -f "${update_path}/.umbrel" ]]; then
|
||||
echo "Update path doesn't seem to be an Umbrel install."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# This will cover only --repo and --path usage
|
||||
if [[ "${update_type}" == "path" ]] && [[ "$*" != *"--skip-version-check"* ]]; then
|
||||
current_version=$(cat "${UMBREL_ROOT}/info.json" | jq --raw-output '.version')
|
||||
update_requires=$(cat "${update_path}/info.json" | jq --raw-output '.requires')
|
||||
|
||||
if ! check_semver_range "${update_requires}" "${current_version}"; then
|
||||
echo "Error: Your Umbrel must first satisify this version: ${update_requires}"
|
||||
echo " Your Umbrel version is: ${current_version}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${update_type}" == "ota" ]]; then
|
||||
RELEASE=$(cat "$UMBREL_ROOT"/statuses/update-status.json | jq .updateTo -r)
|
||||
elif [[ "${update_type}" == "path" ]]; then
|
||||
RELEASE=$(cat "$update_path"/info.json | jq .version -r)
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "======================================="
|
||||
echo "=============== UPDATE ================"
|
||||
echo "======================================="
|
||||
echo "========== Stage: Download ============"
|
||||
echo "======================================="
|
||||
echo
|
||||
|
||||
# Make sure an update is not in progres
|
||||
if [[ -f "$UMBREL_ROOT/statuses/update-in-progress" ]]; then
|
||||
echo "An update is already in progress. Exiting now."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "Creating lock"
|
||||
touch "$UMBREL_ROOT"/statuses/update-in-progress
|
||||
|
||||
# Cleanup just in case there's temp stuff lying around from previous update
|
||||
echo "Cleaning up any previous mess"
|
||||
[[ -d "$UMBREL_ROOT"/.umbrel-"$RELEASE" ]] && rm -rf "$UMBREL_ROOT"/.umbrel-"$RELEASE"
|
||||
|
||||
# Update status file
|
||||
cat <<EOF > "$UMBREL_ROOT"/statuses/update-status.json
|
||||
{"state": "installing", "progress": 10, "description": "Downloading Umbrel $RELEASE", "updateTo": "$RELEASE"}
|
||||
EOF
|
||||
|
||||
# Download new release
|
||||
if [[ "${update_type}" == "ota" ]]; then
|
||||
echo "Downloading Umbrel ${RELEASE}"
|
||||
mkdir -p "${UMBREL_ROOT}/.umbrel-${RELEASE}"
|
||||
cd "${UMBREL_ROOT}/.umbrel-${RELEASE}"
|
||||
curl -L "https://github.com/getumbrel/umbrel/archive/$RELEASE.tar.gz" | tar -xz --strip-components=1
|
||||
fi
|
||||
|
||||
# Copy over new release from path
|
||||
if [[ "${update_type}" == "path" ]]; then
|
||||
echo "Copying Umbrel ${RELEASE} from ${update_path}"
|
||||
mkdir -p "${UMBREL_ROOT}/.umbrel-${RELEASE}"
|
||||
cp --recursive \
|
||||
--archive \
|
||||
--no-target-directory \
|
||||
"${update_path}" "${UMBREL_ROOT}/.umbrel-${RELEASE}"
|
||||
cd "${UMBREL_ROOT}/.umbrel-${RELEASE}"
|
||||
fi
|
||||
|
||||
# Run update scripts
|
||||
echo "Running update install scripts of the new release"
|
||||
cd scripts/update
|
||||
UPDATE_INSTALL_SCRIPTS=$(ls *-run.sh)
|
||||
for script in $UPDATE_INSTALL_SCRIPTS; do
|
||||
if [[ -x $script ]]; then
|
||||
echo
|
||||
echo "== Begin Update Script $script =="
|
||||
./"$script" "$RELEASE" "$UMBREL_ROOT"
|
||||
echo "== End Update Script $script =="
|
||||
echo
|
||||
fi
|
||||
done
|
||||
|
||||
# Delete cloned repo
|
||||
echo "Deleting cloned repository"
|
||||
[[ -d "$UMBREL_ROOT"/.umbrel-"$RELEASE" ]] && rm -rf "$UMBREL_ROOT"/.umbrel-"$RELEASE"
|
||||
|
||||
echo "Removing lock"
|
||||
rm -f "$UMBREL_ROOT"/statuses/update-in-progress
|
||||
|
||||
# Make sure the update-status file isn't stuck in an
|
||||
# unresolved state (i.e anything except "success" or "failed")
|
||||
|
||||
update_state=$(cat "${UMBREL_ROOT}/statuses/update-status.json" | jq .state -r)
|
||||
|
||||
# Show error
|
||||
update_failure_status_file="${UMBREL_ROOT}/statuses/update-failure"
|
||||
if [[ -f "${update_failure_status_file}" ]]; then
|
||||
reason=$(cat "${update_failure_status_file}")
|
||||
rm "${update_failure_status_file}"
|
||||
cat <<EOF > "${UMBREL_ROOT}/statuses/update-status.json"
|
||||
{"state": "failed", "progress": 100, "description": "${reason}", "updateTo": ""}
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [[ "${update_state}" != "success" ]] && [[ "${update_state}" != "failed" ]]; then
|
||||
# Sleep for a few seconds to make sure the user is on the update view
|
||||
sleep 8
|
||||
cat <<EOF > "${UMBREL_ROOT}/statuses/update-status.json"
|
||||
{"state": "failed", "progress": 100, "description": "An unexpected error occured", "updateTo": ""}
|
||||
EOF
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user