mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 02:42:08 +00:00
fix(backup): repair host backup schedules systemd rejects
This commit is contained in:
@@ -40,5 +40,63 @@ class ScheduleValidation(unittest.TestCase):
|
|||||||
self.assertGreater(check, text.index('on_calendar=$(dialog'))
|
self.assertGreater(check, text.index('on_calendar=$(dialog'))
|
||||||
|
|
||||||
|
|
||||||
|
STUB_ANALYZE = '#!/bin/sh\ncase "$2" in daily\\ *|weekly\\ *|monthly\\ *) exit 1;; esac\nexit 0\n'
|
||||||
|
JOBS = {
|
||||||
|
'nas': 'ON_CALENDAR=daily\\ 02:00\nENABLED=1\n',
|
||||||
|
'week': 'ON_CALENDAR=weekly\\ 03:15\nENABLED=1\n',
|
||||||
|
'month': 'ON_CALENDAR=monthly\\ 04:00\nENABLED=0\n',
|
||||||
|
'good': 'ON_CALENDAR=Mon..Fri\\ 03:00\nENABLED=1\n',
|
||||||
|
'manual': 'ON_CALENDAR=daily\\ 05:00\nMANUAL_RUN=1\n',
|
||||||
|
'attached': 'ON_CALENDAR=daily\\ 06:00\nPVE_STORAGE=local\n',
|
||||||
|
'custom': 'ON_CALENDAR=every\\ night\nENABLED=1\n',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduleRepair(unittest.TestCase):
|
||||||
|
def run_repair(self):
|
||||||
|
names = ('_calendar_is_valid', '_scheduler_systemctl', '_job_env_get', '_job_is_attached',
|
||||||
|
'_write_job_units', '_repaired_calendar', '_repair_job_schedules')
|
||||||
|
self.folder = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(self.folder.cleanup)
|
||||||
|
root = Path(self.folder.name)
|
||||||
|
for name, body in (('systemd-analyze', STUB_ANALYZE),
|
||||||
|
('systemctl', f'#!/bin/sh\necho "$@" >> {root}/systemctl.log\n')):
|
||||||
|
(root / name).write_text(body)
|
||||||
|
(root / name).chmod(0o755)
|
||||||
|
jobs, units = root / 'jobs', root / 'units'
|
||||||
|
jobs.mkdir()
|
||||||
|
for job, body in JOBS.items():
|
||||||
|
(jobs / f'{job}.env').write_text(f'JOB_ID={job}\n{body}')
|
||||||
|
script = (f'JOBS_DIR={jobs}\nSYSTEMD_DIR={units}\nLOCAL_SCRIPTS={root}\nSCRIPT_DIR={root}\n'
|
||||||
|
'_job_file() { echo "${JOBS_DIR}/$1.env"; }\n'
|
||||||
|
'_service_file() { echo "${SYSTEMD_DIR}/proxmenux-backup-$1.service"; }\n'
|
||||||
|
'_timer_file() { echo "${SYSTEMD_DIR}/proxmenux-backup-$1.timer"; }\n'
|
||||||
|
+ ''.join(function(name) for name in names) + '_repair_job_schedules\n')
|
||||||
|
subprocess.run(['/bin/bash', '-c', script], check=True,
|
||||||
|
env={'PATH': f'{root}:/usr/bin:/bin'})
|
||||||
|
return jobs, units, root
|
||||||
|
|
||||||
|
def test_a_time_after_daily_weekly_or_monthly_is_repaired(self):
|
||||||
|
jobs, units, root = self.run_repair()
|
||||||
|
self.assertIn('ON_CALENDAR=02:00\n', (jobs / 'nas.env').read_text())
|
||||||
|
self.assertIn('OnCalendar=02:00\n', (units / 'proxmenux-backup-nas.timer').read_text())
|
||||||
|
self.assertIn('OnCalendar=Mon 03:15\n', (units / 'proxmenux-backup-week.timer').read_text())
|
||||||
|
self.assertIn('OnCalendar=*-*-01 04:00\n', (units / 'proxmenux-backup-month.timer').read_text())
|
||||||
|
log = (root / 'systemctl.log').read_text()
|
||||||
|
self.assertIn('enable --now proxmenux-backup-nas.timer', log)
|
||||||
|
self.assertNotIn('proxmenux-backup-month.timer', log)
|
||||||
|
|
||||||
|
def test_the_rest_of_the_jobs_are_left_as_they_are(self):
|
||||||
|
jobs, units, _ = self.run_repair()
|
||||||
|
for job in ('good', 'manual', 'attached', 'custom'):
|
||||||
|
self.assertEqual((jobs / f'{job}.env').read_text(), f'JOB_ID={job}\n{JOBS[job]}', job)
|
||||||
|
self.assertFalse((units / f'proxmenux-backup-{job}.timer').exists(), job)
|
||||||
|
|
||||||
|
def test_both_installers_repair_on_update(self):
|
||||||
|
root = SCHEDULER.parents[2]
|
||||||
|
for installer in ('install_proxmenux.sh', 'install_proxmenux_beta.sh'):
|
||||||
|
self.assertIn('backup_scheduler.sh" --repair-schedules', (root / installer).read_text())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -903,6 +903,8 @@ install_normal_version() {
|
|||||||
bash "$BASE_DIR/oci/engine/remote/stack_dependency_hook.sh" --refresh >/dev/null 2>&1 || true
|
bash "$BASE_DIR/oci/engine/remote/stack_dependency_hook.sh" --refresh >/dev/null 2>&1 || true
|
||||||
systemctl try-restart proxmenux-oci-watchdog.service >/dev/null 2>&1 || true
|
systemctl try-restart proxmenux-oci-watchdog.service >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
|
# A host backup job saved with a schedule systemd rejects never runs.
|
||||||
|
bash "$BASE_DIR/scripts/backup_restore/backup_scheduler.sh" --repair-schedules >/dev/null 2>&1 || true
|
||||||
chmod +x "$BASE_DIR/install_proxmenux.sh"
|
chmod +x "$BASE_DIR/install_proxmenux.sh"
|
||||||
msg_ok "Necessary files created."
|
msg_ok "Necessary files created."
|
||||||
|
|
||||||
|
|||||||
@@ -793,6 +793,8 @@ install_beta() {
|
|||||||
bash "$BASE_DIR/oci/engine/remote/stack_dependency_hook.sh" --refresh >/dev/null 2>&1 || true
|
bash "$BASE_DIR/oci/engine/remote/stack_dependency_hook.sh" --refresh >/dev/null 2>&1 || true
|
||||||
systemctl try-restart proxmenux-oci-watchdog.service >/dev/null 2>&1 || true
|
systemctl try-restart proxmenux-oci-watchdog.service >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
|
# A host backup job saved with a schedule systemd rejects never runs.
|
||||||
|
bash "$BASE_DIR/scripts/backup_restore/backup_scheduler.sh" --repair-schedules >/dev/null 2>&1 || true
|
||||||
chmod +x "$INSTALL_DIR/$MENU_SCRIPT"
|
chmod +x "$INSTALL_DIR/$MENU_SCRIPT"
|
||||||
[ -f "$BASE_DIR/install_proxmenux.sh" ] && chmod +x "$BASE_DIR/install_proxmenux.sh"
|
[ -f "$BASE_DIR/install_proxmenux.sh" ] && chmod +x "$BASE_DIR/install_proxmenux.sh"
|
||||||
[ -f "$BASE_DIR/install_proxmenux_beta.sh" ] && chmod +x "$BASE_DIR/install_proxmenux_beta.sh"
|
[ -f "$BASE_DIR/install_proxmenux_beta.sh" ] && chmod +x "$BASE_DIR/install_proxmenux_beta.sh"
|
||||||
|
|||||||
+1
-2
@@ -2385,7 +2385,6 @@
|
|||||||
"Example: cp /etc/pve/lxc/110.conf /etc/pve/lxc/110.conf.bak": "Ejemplo: cp /etc/pve/lxc/110.conf /etc/pve/lxc/110.conf.bak",
|
"Example: cp /etc/pve/lxc/110.conf /etc/pve/lxc/110.conf.bak": "Ejemplo: cp /etc/pve/lxc/110.conf /etc/pve/lxc/110.conf.bak",
|
||||||
"Example: cp /etc/pve/lxc/114.conf /etc/pve/lxc/114.conf.bak": "Ejemplo: cp /etc/pve/lxc/114.conf /etc/pve/lxc/114.conf.bak",
|
"Example: cp /etc/pve/lxc/114.conf /etc/pve/lxc/114.conf.bak": "Ejemplo: cp /etc/pve/lxc/114.conf /etc/pve/lxc/114.conf.bak",
|
||||||
"Example: daily or Mon..Fri 03:00": "Ejemplo: diario o lunes a viernes 03:00",
|
"Example: daily or Mon..Fri 03:00": "Ejemplo: diario o lunes a viernes 03:00",
|
||||||
"Example: daily, 02:30 or Mon..Fri 03:00": "Ejemplo: daily, 02:30 o Mon..Fri 03:00",
|
|
||||||
"Example: mkdir -p /tmp/lxc_convert_114": "Ejemplo: mkdir -p /tmp/lxc_convert_114",
|
"Example: mkdir -p /tmp/lxc_convert_114": "Ejemplo: mkdir -p /tmp/lxc_convert_114",
|
||||||
"Example: pct config 110 | grep unprivileged": "Ejemplo: pct config 110 | grep unprivileged",
|
"Example: pct config 110 | grep unprivileged": "Ejemplo: pct config 110 | grep unprivileged",
|
||||||
"Example: pct stop 110": "Ejemplo: pct stop 110",
|
"Example: pct stop 110": "Ejemplo: pct stop 110",
|
||||||
@@ -7488,7 +7487,7 @@
|
|||||||
"This removes the storage from Proxmox. The iSCSI target is not affected.": "Esto elimina el almacenamiento de Proxmox. El destino iSCSI no se ve afectado.",
|
"This removes the storage from Proxmox. The iSCSI target is not affected.": "Esto elimina el almacenamiento de Proxmox. El destino iSCSI no se ve afectado.",
|
||||||
"This replaces the currently installed keyfile.": "Esto reemplaza el archivo de claves instalado actualmente.",
|
"This replaces the currently installed keyfile.": "Esto reemplaza el archivo de claves instalado actualmente.",
|
||||||
"This returns available IQNs.": "Esto devuelve los IQN disponibles.",
|
"This returns available IQNs.": "Esto devuelve los IQN disponibles.",
|
||||||
"This schedule is not valid. Examples: daily, 02:30 or Mon..Fri 03:00": "Esta programación no es válida. Ejemplos: daily, 02:30 o Mon..Fri 03:00",
|
"This schedule is not valid.": "Esta programación no es válida.",
|
||||||
"This script cannot be executed from the Proxmox web terminal.": "Este script no se puede ejecutar desde la terminal web de Proxmox.",
|
"This script cannot be executed from the Proxmox web terminal.": "Este script no se puede ejecutar desde la terminal web de Proxmox.",
|
||||||
"This script must be run as root.": "Este script debe ejecutarse como root.",
|
"This script must be run as root.": "Este script debe ejecutarse como root.",
|
||||||
"This script must be run on a Proxmox host.": "Este script debe ejecutarse en un host Proxmox.",
|
"This script must be run on a Proxmox host.": "Este script debe ejecutarse en un host Proxmox.",
|
||||||
|
|||||||
+1
-2
@@ -2388,7 +2388,6 @@
|
|||||||
"Example: cp /etc/pve/lxc/110.conf /etc/pve/lxc/110.conf.bak": "Príklad: cp /etc/pve/lxc/110.conf /etc/pve/lxc/110.conf.bak",
|
"Example: cp /etc/pve/lxc/110.conf /etc/pve/lxc/110.conf.bak": "Príklad: cp /etc/pve/lxc/110.conf /etc/pve/lxc/110.conf.bak",
|
||||||
"Example: cp /etc/pve/lxc/114.conf /etc/pve/lxc/114.conf.bak": "Príklad: cp /etc/pve/lxc/114.conf /etc/pve/lxc/114.conf.bak",
|
"Example: cp /etc/pve/lxc/114.conf /etc/pve/lxc/114.conf.bak": "Príklad: cp /etc/pve/lxc/114.conf /etc/pve/lxc/114.conf.bak",
|
||||||
"Example: daily or Mon..Fri 03:00": "Príklad: daily alebo Mon..Fri 03:00",
|
"Example: daily or Mon..Fri 03:00": "Príklad: daily alebo Mon..Fri 03:00",
|
||||||
"Example: daily, 02:30 or Mon..Fri 03:00": "Príklad: daily, 02:30 alebo Mon..Fri 03:00",
|
|
||||||
"Example: mkdir -p /tmp/lxc_convert_114": "Príklad: mkdir -p /tmp/lxc_convert_114",
|
"Example: mkdir -p /tmp/lxc_convert_114": "Príklad: mkdir -p /tmp/lxc_convert_114",
|
||||||
"Example: pct config 110 | grep unprivileged": "Príklad: pct config 110 | grep unprivileged",
|
"Example: pct config 110 | grep unprivileged": "Príklad: pct config 110 | grep unprivileged",
|
||||||
"Example: pct stop 110": "Príklad: pct stop 110",
|
"Example: pct stop 110": "Príklad: pct stop 110",
|
||||||
@@ -7515,7 +7514,7 @@
|
|||||||
"This removes the storage from Proxmox. The iSCSI target is not affected.": "Týmto sa úložisko odstráni z Proxmoxu. iSCSI target sa nezmení.",
|
"This removes the storage from Proxmox. The iSCSI target is not affected.": "Týmto sa úložisko odstráni z Proxmoxu. iSCSI target sa nezmení.",
|
||||||
"This replaces the currently installed keyfile.": "Týmto sa nahradí aktuálne nainštalovaný keyfile.",
|
"This replaces the currently installed keyfile.": "Týmto sa nahradí aktuálne nainštalovaný keyfile.",
|
||||||
"This returns available IQNs.": "Týmto sa zobrazia dostupné IQN.",
|
"This returns available IQNs.": "Týmto sa zobrazia dostupné IQN.",
|
||||||
"This schedule is not valid. Examples: daily, 02:30 or Mon..Fri 03:00": "Tento plán nie je platný. Príklady: daily, 02:30 alebo Mon..Fri 03:00",
|
"This schedule is not valid.": "Tento plán nie je platný.",
|
||||||
"This script cannot be executed from the Proxmox web terminal.": "Tento skript sa nedá spustiť z webového terminálu Proxmoxu.",
|
"This script cannot be executed from the Proxmox web terminal.": "Tento skript sa nedá spustiť z webového terminálu Proxmoxu.",
|
||||||
"This script must be run as root.": "Tento skript musí byť spustený ako root.",
|
"This script must be run as root.": "Tento skript musí byť spustený ako root.",
|
||||||
"This script must be run on a Proxmox host.": "Tento skript musí byť spustený na Proxmox serveri.",
|
"This script must be run on a Proxmox host.": "Tento skript musí byť spustený na Proxmox serveri.",
|
||||||
|
|||||||
@@ -105,14 +105,17 @@ _job_is_attached() {
|
|||||||
[[ -n "$storage" ]]
|
[[ -n "$storage" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Reads a key=val pair from the job .env file (handles `printf %q`
|
# Literal systemd expressions, never translated.
|
||||||
# quoting that _write_job_env produces).
|
CALENDAR_EXAMPLES="daily, 02:30, Mon..Fri 03:00"
|
||||||
|
|
||||||
_calendar_is_valid() {
|
_calendar_is_valid() {
|
||||||
[[ "${PMX_BACKUP_NO_SYSTEMCTL:-0}" == "1" ]] && return 0
|
[[ "${PMX_BACKUP_NO_SYSTEMCTL:-0}" == "1" ]] && return 0
|
||||||
command -v systemd-analyze >/dev/null 2>&1 || return 0
|
command -v systemd-analyze >/dev/null 2>&1 || return 0
|
||||||
systemd-analyze calendar "$1" >/dev/null 2>&1
|
systemd-analyze calendar "$1" >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Reads a key=val pair from the job .env file (handles `printf %q`
|
||||||
|
# quoting that _write_job_env produces).
|
||||||
_job_env_get() {
|
_job_env_get() {
|
||||||
local id="$1" key="$2" f raw
|
local id="$1" key="$2" f raw
|
||||||
f=$(_job_file "$id")
|
f=$(_job_file "$id")
|
||||||
@@ -354,6 +357,40 @@ _reconcile_restored_jobs() {
|
|||||||
(( invalid == 0 ))
|
(( invalid == 0 ))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# systemd rejects a time after daily, weekly or monthly, and a job saved that
|
||||||
|
# way has a timer that never fires. Returns the expression systemd accepts.
|
||||||
|
_repaired_calendar() {
|
||||||
|
[[ "$1" =~ ^(daily|weekly|monthly)[[:space:]]+([0-9]{1,2}:[0-9]{2}(:[0-9]{2})?)$ ]] || return 1
|
||||||
|
case "${BASH_REMATCH[1]}" in
|
||||||
|
daily) echo "${BASH_REMATCH[2]}" ;;
|
||||||
|
weekly) echo "Mon ${BASH_REMATCH[2]}" ;;
|
||||||
|
monthly) echo "*-*-01 ${BASH_REMATCH[2]}" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
_repair_job_schedules() {
|
||||||
|
local env_file id on_calendar fixed line
|
||||||
|
for env_file in "$JOBS_DIR"/*.env; do
|
||||||
|
[[ -f "$env_file" ]] || continue
|
||||||
|
id="$(basename "$env_file" .env)"
|
||||||
|
[[ "$(_job_env_get "$id" MANUAL_RUN || echo 0)" == "1" ]] && continue
|
||||||
|
_job_is_attached "$id" && continue
|
||||||
|
on_calendar=$(_job_env_get "$id" ON_CALENDAR || echo "")
|
||||||
|
[[ -n "$on_calendar" ]] || continue
|
||||||
|
_calendar_is_valid "$on_calendar" && continue
|
||||||
|
fixed=$(_repaired_calendar "$on_calendar") || continue
|
||||||
|
_calendar_is_valid "$fixed" || continue
|
||||||
|
|
||||||
|
printf -v line 'ON_CALENDAR=%q' "$fixed"
|
||||||
|
LINE="$line" awk '/^ON_CALENDAR=/ { print ENVIRON["LINE"]; next } { print }' \
|
||||||
|
"$env_file" > "${env_file}.tmp" && mv "${env_file}.tmp" "$env_file" || continue
|
||||||
|
_write_job_units "$id" "$fixed" || continue
|
||||||
|
if [[ "$(_job_env_get "$id" ENABLED || echo 0)" == "1" ]]; then
|
||||||
|
_scheduler_systemctl enable --now "proxmenux-backup-${id}.timer" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
_prompt_retention() {
|
_prompt_retention() {
|
||||||
local __out_var="$1"
|
local __out_var="$1"
|
||||||
local last hourly daily weekly monthly yearly
|
local last hourly daily weekly monthly yearly
|
||||||
@@ -586,15 +623,15 @@ _create_job() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
on_calendar=$(dialog --backtitle "ProxMenux" --title "$(translate "Schedule")" \
|
on_calendar=$(dialog --backtitle "ProxMenux" --title "$(translate "Schedule")" \
|
||||||
--inputbox "$(translate "systemd OnCalendar expression")"$'\n'"$(translate "Example: daily, 02:30 or Mon..Fri 03:00")" \
|
--inputbox "$(translate "systemd OnCalendar expression")"$'\n'"$(translate "Examples:") ${CALENDAR_EXAMPLES}" \
|
||||||
11 72 "daily" 3>&1 1>&2 2>&3) || return 1
|
11 72 "daily" 3>&1 1>&2 2>&3) || return 1
|
||||||
[[ -z "$on_calendar" ]] && return 1
|
[[ -z "$on_calendar" ]] && return 1
|
||||||
# A timer with an invalid expression never fires and reports no error.
|
# A timer with an invalid expression never fires and reports no error.
|
||||||
while ! _calendar_is_valid "$on_calendar"; do
|
while ! _calendar_is_valid "$on_calendar"; do
|
||||||
dialog --backtitle "ProxMenux" --title "$(translate "Schedule")" \
|
dialog --backtitle "ProxMenux" --title "$(translate "Schedule")" \
|
||||||
--msgbox "$(translate "This schedule is not valid. Examples: daily, 02:30 or Mon..Fri 03:00")" 8 72
|
--msgbox "$(translate "This schedule is not valid.")"$'\n'"$(translate "Examples:") ${CALENDAR_EXAMPLES}" 9 72
|
||||||
on_calendar=$(dialog --backtitle "ProxMenux" --title "$(translate "Schedule")" \
|
on_calendar=$(dialog --backtitle "ProxMenux" --title "$(translate "Schedule")" \
|
||||||
--inputbox "$(translate "systemd OnCalendar expression")"$'\n'"$(translate "Example: daily, 02:30 or Mon..Fri 03:00")" \
|
--inputbox "$(translate "systemd OnCalendar expression")"$'\n'"$(translate "Examples:") ${CALENDAR_EXAMPLES}" \
|
||||||
11 72 "$on_calendar" 3>&1 1>&2 2>&3) || return 1
|
11 72 "$on_calendar" 3>&1 1>&2 2>&3) || return 1
|
||||||
[[ -z "$on_calendar" ]] && return 1
|
[[ -z "$on_calendar" ]] && return 1
|
||||||
done
|
done
|
||||||
@@ -997,6 +1034,11 @@ if [[ "${1:-}" == "--reconcile-restored" ]]; then
|
|||||||
exit $?
|
exit $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "--repair-schedules" ]]; then
|
||||||
|
_repair_job_schedules
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
load_language
|
load_language
|
||||||
initialize_cache
|
initialize_cache
|
||||||
main_menu
|
main_menu
|
||||||
|
|||||||
Reference in New Issue
Block a user