Feat: Flash Backup requires connection to mothership (#868)

* fix: branding

* feat: flash backup requires connection to mothership

* feat: flash backup requires connection to mothership
This commit is contained in:
ljm42
2024-04-26 09:01:42 -07:00
committed by GitHub
parent 460e557dd8
commit cc69213beb
4 changed files with 408 additions and 8 deletions

View File

@@ -0,0 +1,205 @@
#!/bin/bash
# This file is /etc/rc.d/rc.flash_backup
# use at queue "f" for flash backup
QUEUE=" -q f "
TASKNAME="/etc/rc.d/rc.flash_backup watch"
TASKACTION="/usr/local/emhttp/plugins/dynamix.my.servers/scripts/UpdateFlashBackup update"
last=$(date +%s)
# set GIT_OPTIONAL_LOCKS=0 globally to reduce/eliminate writes to /boot
export GIT_OPTIONAL_LOCKS=0
FAST=1 # 1 second delay when waiting for git
SLOW=10 # 10 second delay when waiting for git
# wait for existing git commands to complete
# $1 is the time in seconds to sleep when waiting. SLOW or FAST
_waitforgit() {
while [[ $(pgrep -f '^git -C /boot' -c) -ne 0 ]]; do
sleep "$1"
done
}
# log to syslog, then wait for existing git commands to complete
# $1 is the time in seconds to sleep when waiting. SLOW or FAST
_waitforgitlog() {
if [[ $(pgrep -f '^git -C /boot' -c) -ne 0 ]]; then
logger "waiting for current backup to complete" --tag flash_backup
_waitforgit "$1"
fi
}
status() {
_connected && CONNECTED="system is connected to Unraid Connect Cloud." || CONNECTED="system is not connected to Unraid Connect Cloud."
if _watching; then
echo "flash backup monitor is running. ${CONNECTED}"
_hasqueue && echo "changes detected, backup queued."
exit 0
else
if _enabled; then
echo "flash backup is enabled but the monitor is not running. ${CONNECTED}"
else
echo "flash backup is disabled so the monitor is disabled. ${CONNECTED}"
fi
exit 1
fi
}
start() {
_start
exit 0
}
stop() {
_stop
exit 0
}
reload() {
_start
sleep 1
status
}
_start() {
# Note: can start if not signed in, but watcher loop will not process until signed in
# only run if flash_backup is enabled
if ! _enabled; then
logger "flash backup disabled, exiting" --tag flash_backup
exit 1
fi
_stop
# start watcher loop as background process
exec ${TASKNAME} &>/dev/null &
}
_stop() {
if _watching; then
logger "stop watching for file changes" --tag flash_backup
# terminate watcher loop/process
pkill --full "${TASKNAME}" &>/dev/null
fi
# do not flush. better to have unsaved changes than to corrupt the backup during shutdown
# note that an existing git process could still be running
}
flush() {
# remove any queued jobs
_removequeue
# wait for existing git commands to finish before flushing
_waitforgitlog "${FAST}"
logger "flush: ${TASKACTION}" --tag flash_backup
# push any changes ad-hoc
# shellcheck disable=SC2086
echo "${TASKACTION}_nolimit &>/dev/null" | at ${QUEUE} -M now &>/dev/null
}
_watching() {
local flash_backup_pid
flash_backup_pid=$(pgrep --full "${TASKNAME}")
if [[ ${flash_backup_pid} ]]; then
return 0
fi
return 1
}
_watch() {
# safely clean up git *.lock files
_clearlocks
# flush: this will ensure we start with a clean repo
flush
logger "start watching for file changes" --tag flash_backup
# start watcher loop
while true; do
# wait for system to be connected to Unraid Connect Cloud, then process flash backups
_connected && _f1
sleep 60
done
}
_f1() {
# wait for existing git commands to finish before checking for updates
_waitforgit "${SLOW}"
if [ "$(git -C /boot status -s)" ]; then
_hasqueue || _f2
elif _haserror && _beenawhile; then
# we are in an error state and it has been 3 hours since we last tried submitting. run the task now.
_runtaskaction
fi
}
_f2() {
if ! _haserror || [[ $(($(date +"%M") % 10)) -eq 0 ]]; then
logger "adding task: ${TASKACTION}" --tag flash_backup
fi
sed -i "s@uptodate=yes@uptodate=no@" /var/local/emhttp/flashbackup.ini &>/dev/null
_runtaskaction
}
_hasqueue() {
# returns false if the queue is empty, true otherwise
# shellcheck disable=SC2086
if [ -z "$(atq ${QUEUE})" ]; then
return 1
fi
return 0
}
_removequeue() {
# delete any at jobs in queue f
# @TODO shellcheck SC2162
# shellcheck disable=SC2086
atq ${QUEUE} | while read line; do
id=$(echo ${line} | cut -d " " -f 1)
atrm ${id}
done
}
_runtaskaction() {
# shellcheck disable=SC2086
echo "${TASKACTION} &>/dev/null" | at ${QUEUE} -M now +1 minute &>/dev/null
last=$(date +%s)
}
_enabled() {
local output
output=$(git -C /boot config --get remote.origin.url 2>&1)
if [[ "${output}" == *"backup.unraid.net"* ]]; then
return 0
fi
return 1
}
_connected() {
CFG=/var/local/emhttp/myservers.cfg
[[ ! -f "${CFG}" ]] && return 1
# shellcheck disable=SC1090
source <(sed -nr '/\[connectionStatus\]/,/\[/{/minigraph/p}' "${CFG}" 2>/dev/null)
if [[ -z "${minigraph}" || "${minigraph}" != "CONNECTED" ]]; then
return 1
fi
return 0
}
_haserror() {
errorstring=$(awk -F "=" '/error/ {print $2}' /var/local/emhttp/flashbackup.ini 2>&1 || echo '')
if [ ${#errorstring} -le 2 ]; then
return 1
fi
return 0
}
_beenawhile() {
now=$(date +%s)
age=$((now - last))
maxage=$((3 * 60 * 60)) # three hours
[[ $age -gt $maxage ]] && return 0
return 1
}
# wait for git commands to end, then delete any stale lock files
_clearlocks() {
_waitforgitlog "${FAST}"
find /boot/.git -type f -name '*.lock' -delete
}
case "$1" in
'status')
status
;;
'start')
start
;;
'stop')
stop
;;
'reload')
reload
;;
'flush')
flush
;;
'watch')
_watch
;;
*)
echo "usage $0 status|start|stop|reload|flush"
;;
esac

View File

@@ -0,0 +1,182 @@
#!/bin/bash
# unraid-api-handler
flash="/boot/config/plugins/dynamix.my.servers"
[[ ! -d "${flash}" ]] && echo "Please reinstall the Unraid Connect plugin" && exit 1
[[ ! -f "${flash}/env" ]] && echo 'env=production' >"${flash}/env"
# define env to avoid shellcheck SC2154. Will be overridden by the source command below
env=production
# shellcheck disable=SC1091
source "${flash}/env"
api_base_directory="/usr/local/bin"
# Only allow specific envs
if [ "${env}" != "staging" ] && [ "${env}" != "production" ]; then
echo "\"${env}\" is an unsupported env. Please use \"staging\" or \"production\"."
exit 1
fi
switchenv() {
stop
# Get current environment from file
local envFile="${flash}/env"
local currentEnv
currentEnv=$(
# shellcheck disable=SC1090
source "${envFile}"
echo "${env}"
)
if [[ "${currentEnv}" = "production" ]]; then
echo "Switching from production to staging"
echo 'env="staging"' >"${envFile}"
cp "${api_base_directory}/unraid-api/.env.staging" "${api_base_directory}/unraid-api/.env"
elif [[ "${currentEnv}" = "staging" ]]; then
echo "Switching from staging to production"
echo 'env="production"' >"${envFile}"
cp "${api_base_directory}/unraid-api/.env.production" "${api_base_directory}/unraid-api/.env"
fi
echo "Run \"unraid-api start\" to start the API."
}
raiseloglevel() {
kill -s SIGUSR2 "$(pidof unraid-api)"
}
lowerloglevel() {
kill -s SIGUSR1 "$(pidof unraid-api)"
}
status() {
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" status
}
start() {
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" start 2>&1 | logger &
}
report() {
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" report "$1" "$2"
}
startdebug() {
LOG_CONTEXT=true LOG_STACKTRACE=true LOG_TRACING=true LOG_LEVEL=debug "${api_base_directory}/unraid-api/unraid-api" start --debug
}
stop() {
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" stop 2>/dev/null
}
reload() {
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" restart
}
_install() {
# process file from commandline
if [[ -n "$1" ]]; then
file=$(realpath "${flash}/$1")
if [[ "${file}" == "${flash}"* ]] && [[ "${file}" == *".tgz" || "${file}" == *".zip" ]] && [[ -f "${file}" ]]; then
[[ "${file}" == *".tgz" ]] && ext=tgz || ext=zip
echo "installing $1"
cp "${file}" "${flash}/unraid-api.${ext}"
else
echo "invalid installation file: $1"
exit 1
fi
fi
# If this was downloaded from a Github action it'll be a zip with a tgz inside
# Let's extract the tgz and rename it for the next step
if [[ -f "${flash}/unraid-api.zip" ]]; then
for f in ${flash}/unraid-api.zip; do unzip -p "${f}" >"${flash}/${f%.zip}.tgz"; done
rm -f "${flash}/unraid-api.zip"
fi
# Ensure installation tgz exists
[[ ! -f "${flash}/unraid-api.tgz" ]] && echo "Please reinstall the Unraid Connect plugin" && exit 1
# Stop old process
[[ -f "${api_base_directory}/unraid-api/unraid-api" ]] && stop
# Install unraid-api
rm -rf "${api_base_directory}/unraid-api"
mkdir -p "${api_base_directory}/unraid-api"
tar -C "${api_base_directory}/unraid-api" -xzf "${flash}/unraid-api.tgz" --strip 1
# Reset permissions
rm -f "${flash}/data/permissions.json"
# Copy env file
cp "${api_base_directory}/unraid-api/.env.${env}" "${api_base_directory}/unraid-api/.env"
# Copy wc files from flash
if [ -f "${flash}/webComps/unraid.min.js" ]; then
rm -rf /usr/local/emhttp/webGui/webComps
mkdir -p /usr/local/emhttp/webGui/webComps
cp ${flash}/webComps/* /usr/local/emhttp/webGui/webComps
else
# not fatal, previous version of unraid.min.js should still exist in /usr/local/emhttp/webGui/webComps
echo "Note: ${flash}/webComps/unraid.min.js is missing"
fi
# bail if expected file does not exist
[[ ! -f "${api_base_directory}/unraid-api/unraid-api" ]] && echo "unraid-api install failed" && exit 1
}
install() {
# Install the files
_install "$1"
# if nginx is running, start the api. if not, it will be started by rc.nginx
if /etc/rc.d/rc.nginx status &>/dev/null; then
# Start new process
start
# Note: do not run another unraid-api command until you see "UNRAID API started successfully!" in syslog
sleep 3
echo "unraid-api installed and started"
else
echo "unraid-api installed"
fi
exit 0
}
uninstall() {
# Stop old process
[[ -f "${api_base_directory}/unraid-api/unraid-api" ]] && stop
# Remove all unraid-api files
rm -rf "${api_base_directory}/unraid-api"
rm -f /var/run/unraid-api.sock
}
case "$1" in
'status')
status
;;
'start')
start
;;
'report')
report "$2" "$3"
;;
'switch-env')
switchenv
;;
'start-debug')
startdebug
;;
'raise-log-level')
raiseloglevel
;;
'lower-log-level')
lowerloglevel
;;
'stop')
stop
;;
'reload')
reload
;;
'restart')
reload
;;
'install')
install "$2"
;;
'_install')
_install "$2"
;;
'uninstall')
uninstall
;;
*)
echo "usage $0 status|start|report|switch-env|start-debug|raise-log-level|lower-log-level|stop|reload|install|uninstall"
;;
esac

View File

@@ -522,7 +522,7 @@ _(Allow Remote Access)_:
<?if(!$isRegistered): // NOTE: manually added close tags so the next section would not be indented ?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until you have signed in)_</span></dd></dl>
<?elseif(!$isMiniGraphConnected && $myServersFlashCfg['remote']['wanaccess']!="yes"): // NOTE: manually added close tags so the next section would not be indented ?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until connected to Unraid Connect Cloud)_</span></dd></dl>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until connected to Unraid Connect Cloud - try reloading the page)_</span></dd></dl>
<?elseif(!$hasMyUnraidNetCert): // NOTE: manually added close tags so the next section would not be indented ?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until you Provision a myunraid.net SSL Cert)_</span><input type="hidden" id="wanport" value="0"></dd></dl>
<?elseif(!$boolWebUIAuth): // NOTE: manually added close tags so the next section would not be indented ?>
@@ -627,8 +627,8 @@ _(Enable Transparent 2FA for Local Access)_<!-- do not index -->:
_(Flash backup)_:
<?if(!$isRegistered):?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until you have signed in)_</span>
<?elseif(!$isMiniGraphConnected && empty($flashbackup_status['activated'])):?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until connected to Unraid Connect Cloud)_</span>
<?elseif(!$isMiniGraphConnected):?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until connected to Unraid Connect Cloud - try reloading the page)_</span>
<?else: // begin show flash backup form ?>
: <span id='flashbackuptext'><span class='blue p0'>_(Loading)_ <i class="fa fa-spinner fa-spin" aria-hidden="true"></i></span></span>

View File

@@ -70,8 +70,8 @@ function save_flash_backup_state($loading='') {
rename($flashbackup_tmp, $flashbackup_ini);
}
function load_flash_backup_state() {
global $arrState,$flashbackup_ini,$isRegistered;
function default_flash_backup_state() {
global $arrState;
$arrState = [
'activated' => 'no',
@@ -80,6 +80,12 @@ function load_flash_backup_state() {
'error' => '',
'remoteerror' => ''
];
}
function load_flash_backup_state() {
global $arrState,$flashbackup_ini,$isRegistered;
default_flash_backup_state();
$arrNewState = (file_exists($flashbackup_ini)) ? @parse_ini_file($flashbackup_ini) : [];
if ($arrNewState) {
@@ -277,7 +283,14 @@ if ($pgrep_output[0] != "0") {
// check if signed-in
if (!$isRegistered) {
response_complete(406, array('error' => 'Must be signed in to My Servers to use Flash Backup'));
default_flash_backup_state();
response_complete(406, array('error' => 'Must be signed in to Unraid Connect to use Flash Backup'));
}
// check if connected to Unraid Connect Cloud
if (!$isConnected) {
default_flash_backup_state();
response_complete(406, array('error' => 'Must be connected to Unraid Connect Cloud to use Flash Backup'));
}
// keyfile
@@ -540,7 +553,7 @@ if (empty($SSH_PORT)) {
} else {
$arrState['loading'] = '';
if (stripos(implode($ssh_output),'permission denied') !== false) {
$arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to My Servers Cloud';
$arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to Unraid Connect Cloud';
} else {
$arrState['error'] = 'Unable to connect to backup.unraid.net:22';
}
@@ -654,7 +667,7 @@ if ($command == 'update' || $command == 'activate') {
if ($return_var != 0) {
// check for permission denied
if (stripos(implode($push_output),'permission denied') !== false) {
$arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to My Servers Cloud';
$arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to Unraid Connect Cloud';
} elseif (stripos(implode($push_output),'fatal: loose object') !== false && stripos(implode($push_output),'is corrupt') !== false) {
// detect corruption #2
$arrState['error'] = 'Error: Backup corrupted';