mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-10-02 11:36:44 +00:00
ProxMenux 1.2.6.2-beta: OCI containers in the Monitor, docs and fixes
OCI manager Apps - App tab: containers installed from an OCI image are identified from their installation record; the application and image versions are shown and an update is detected by image digest; repository link; Refresh data. - Updates tab for OCI containers: Update and Recreate run the same flow as the OCI menu in the Monitor terminal; the pre-update backup can be kept in a backup storage; scheduled image updates with an optional minimum age. - Logs tab: console output of the application, kept on the host (lxc.console.logfile + logrotate) and followed live. - The Proxmox console opens a shell (cmode: shell) when the image has one. - A damaged image download is fetched again before failing. - Multi-container applications open at their LAN address; volume mount points on block storage report their usage. Monitor - Proxmox notifications are delivered to a loopback-only HTTP listener when HTTPS is enabled, so they no longer fail certificate verification. - Log persistence counts recurring patterns only; an ended burst is not reported as persistent and its warning clears on its own (#386). - Proxmox notification config backups are deduplicated and capped at three. - The update icon on the Apps page opens the container on its Updates tab. - Version 1.2.6.2-beta and its release notes in every Monitor language. Docs - OCI manager Apps and Audit & Report rebuilt as per-page message files, with a new page for OCI containers in the Monitor. - Seven pages fixed where rich-text tags were missing from t.rich. Translations - Spanish fixes across the OCI engine, the Monitor and the TUI menus. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
386d33df6e
commit
4437a671d2
+609
@@ -0,0 +1,609 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenux - Backup/Restore Test Matrix (non-destructive)
|
||||
# ==========================================================
|
||||
|
||||
set -u
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
RUNNER="${SCRIPT_DIR}/run_scheduled_backup.sh"
|
||||
APPLY_ONBOOT="${SCRIPT_DIR}/apply_pending_restore.sh"
|
||||
CLUSTER_APPLY="${SCRIPT_DIR}/apply_cluster_postboot.sh"
|
||||
HOST_SCRIPT="${SCRIPT_DIR}/backup_host.sh"
|
||||
LIB_SCRIPT="${SCRIPT_DIR}/lib_host_backup_common.sh"
|
||||
SCHED_SCRIPT="${SCRIPT_DIR}/backup_scheduler.sh"
|
||||
|
||||
KEEP_TMP=0
|
||||
if [[ "${1:-}" == "--keep-tmp" ]]; then
|
||||
KEEP_TMP=1
|
||||
fi
|
||||
|
||||
TMP_ROOT="$(mktemp -d /tmp/proxmenux-brtest.XXXXXX)"
|
||||
REPORT_FILE="/tmp/proxmenux-backup-restore-test-$(date +%Y%m%d_%H%M%S).log"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
SKIP=0
|
||||
|
||||
log() {
|
||||
echo "$*" | tee -a "$REPORT_FILE"
|
||||
}
|
||||
|
||||
pass() {
|
||||
PASS=$((PASS + 1))
|
||||
log "[PASS] $*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
FAIL=$((FAIL + 1))
|
||||
log "[FAIL] $*"
|
||||
}
|
||||
|
||||
skip() {
|
||||
SKIP=$((SKIP + 1))
|
||||
log "[SKIP] $*"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [[ "$KEEP_TMP" -eq 0 ]]; then
|
||||
rm -rf "$TMP_ROOT"
|
||||
else
|
||||
log "[INFO] Temp root preserved: $TMP_ROOT"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
assert_file_contains() {
|
||||
local file="$1"
|
||||
local needle="$2"
|
||||
if [[ -f "$file" ]] && grep -q "$needle" "$file"; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
run_cmd_expect_ok() {
|
||||
local desc="$1"
|
||||
shift
|
||||
if "$@" >>"$REPORT_FILE" 2>&1; then
|
||||
pass "$desc"
|
||||
return 0
|
||||
fi
|
||||
fail "$desc"
|
||||
return 1
|
||||
}
|
||||
|
||||
run_cmd_expect_fail() {
|
||||
local desc="$1"
|
||||
shift
|
||||
if "$@" >>"$REPORT_FILE" 2>&1; then
|
||||
fail "$desc"
|
||||
return 1
|
||||
fi
|
||||
pass "$desc"
|
||||
return 0
|
||||
}
|
||||
|
||||
syntax_tests() {
|
||||
log "\n=== Syntax checks ==="
|
||||
run_cmd_expect_ok "bash -n backup_host.sh" bash -n "$HOST_SCRIPT"
|
||||
run_cmd_expect_ok "bash -n lib_host_backup_common.sh" bash -n "$LIB_SCRIPT"
|
||||
run_cmd_expect_ok "bash -n backup_scheduler.sh" bash -n "$SCHED_SCRIPT"
|
||||
run_cmd_expect_ok "bash -n run_scheduled_backup.sh" bash -n "$RUNNER"
|
||||
run_cmd_expect_ok "bash -n apply_pending_restore.sh" bash -n "$APPLY_ONBOOT"
|
||||
run_cmd_expect_ok "bash -n apply_cluster_postboot.sh" bash -n "$CLUSTER_APPLY"
|
||||
}
|
||||
|
||||
certificate_restore_tests() {
|
||||
log "\n=== Custom pveproxy certificate restore (sandbox) ==="
|
||||
if ! command -v openssl >/dev/null 2>&1; then
|
||||
skip "Certificate restore tests require openssl."
|
||||
return
|
||||
fi
|
||||
|
||||
local fixture="$TMP_ROOT/certificate-restore"
|
||||
local source_node="$fixture/source-node"
|
||||
local empty_node="$fixture/empty-node"
|
||||
local active_dir="$fixture/active"
|
||||
local bin_dir="$fixture/bin"
|
||||
local call_log="$fixture/pvenode.calls"
|
||||
mkdir -p "$source_node" "$empty_node" "$active_dir" "$bin_dir"
|
||||
|
||||
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
|
||||
-subj '/CN=proxmenux-restore-test' \
|
||||
-keyout "$source_node/pveproxy-ssl.key" \
|
||||
-out "$source_node/pveproxy-ssl.pem" >>"$REPORT_FILE" 2>&1
|
||||
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
|
||||
-out "$fixture/mismatched.key" >>"$REPORT_FILE" 2>&1
|
||||
|
||||
cat > "$bin_dir/pvenode" <<'EOS'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >> "$PMX_TEST_CALL_LOG"
|
||||
[[ "${PMX_TEST_PVENODE_FAIL:-0}" == "1" ]] && exit 1
|
||||
if [[ "$1 $2" == "cert set" ]]; then
|
||||
cp "$3" "$PMX_PVEPROXY_CERT_PATH"
|
||||
cp "$4" "$PMX_PVEPROXY_KEY_PATH"
|
||||
elif [[ "$1 $2" == "cert delete" ]]; then
|
||||
rm -f "$PMX_PVEPROXY_CERT_PATH" "$PMX_PVEPROXY_KEY_PATH"
|
||||
fi
|
||||
EOS
|
||||
cat > "$bin_dir/systemctl" <<'EOS'
|
||||
#!/bin/bash
|
||||
[[ "${PMX_TEST_SERVICE_INACTIVE:-0}" == "1" ]] && exit 1
|
||||
[[ "$1 $2 $3" == "is-active --quiet pveproxy.service" ]]
|
||||
EOS
|
||||
chmod +x "$bin_dir/pvenode" "$bin_dir/systemctl"
|
||||
|
||||
export PMX_PVENODE_BIN="$bin_dir/pvenode"
|
||||
export PMX_SYSTEMCTL_BIN="$bin_dir/systemctl"
|
||||
export PMX_PVEPROXY_CERT_PATH="$active_dir/pveproxy-ssl.pem"
|
||||
export PMX_PVEPROXY_KEY_PATH="$active_dir/pveproxy-ssl.key"
|
||||
export PMX_TEST_CALL_LOG="$call_log"
|
||||
export PMX_PVEPROXY_VERIFY_ATTEMPTS=1
|
||||
|
||||
if (
|
||||
PMX_CERT_HELPER_ONLY=1
|
||||
source "$CLUSTER_APPLY"
|
||||
_restore_pveproxy_certificate "$source_node"
|
||||
) >>"$REPORT_FILE" 2>&1; then
|
||||
local source_fp active_fp
|
||||
source_fp=$(openssl x509 -in "$source_node/pveproxy-ssl.pem" -noout -sha256 -fingerprint)
|
||||
active_fp=$(openssl x509 -in "$PMX_PVEPROXY_CERT_PATH" -noout -sha256 -fingerprint)
|
||||
if [[ "$source_fp" == "$active_fp" ]] \
|
||||
&& assert_file_contains "$call_log" "cert set .* --force 1 --restart 1"; then
|
||||
pass "Valid custom certificate is installed through pvenode and verified"
|
||||
else
|
||||
fail "Valid custom certificate was not installed as expected"
|
||||
fi
|
||||
else
|
||||
fail "Valid custom certificate restore failed"
|
||||
fi
|
||||
|
||||
rm -f "$call_log"
|
||||
if (
|
||||
PMX_CERT_HELPER_ONLY=1
|
||||
source "$CLUSTER_APPLY"
|
||||
_restore_pveproxy_certificate "$empty_node"
|
||||
) >>"$REPORT_FILE" 2>&1 && [[ ! -e "$call_log" ]]; then
|
||||
pass "Backup without a custom certificate leaves the current certificate unchanged"
|
||||
else
|
||||
fail "Empty certificate backup unexpectedly changed the current certificate"
|
||||
fi
|
||||
|
||||
local active_before
|
||||
active_before=$(openssl x509 -in "$PMX_PVEPROXY_CERT_PATH" -noout -sha256 -fingerprint)
|
||||
mv "$source_node/pveproxy-ssl.key" "$fixture/source.key"
|
||||
rm -f "$call_log"
|
||||
if ! (
|
||||
PMX_CERT_HELPER_ONLY=1
|
||||
source "$CLUSTER_APPLY"
|
||||
_restore_pveproxy_certificate "$source_node"
|
||||
) >>"$REPORT_FILE" 2>&1 \
|
||||
&& [[ ! -e "$call_log" ]] \
|
||||
&& [[ "$active_before" == "$(openssl x509 -in "$PMX_PVEPROXY_CERT_PATH" -noout -sha256 -fingerprint)" ]]; then
|
||||
pass "Incomplete certificate pair is rejected without changing the active certificate"
|
||||
else
|
||||
fail "Incomplete certificate pair was not rejected safely"
|
||||
fi
|
||||
|
||||
cp "$fixture/mismatched.key" "$source_node/pveproxy-ssl.key"
|
||||
rm -f "$call_log"
|
||||
if ! (
|
||||
PMX_CERT_HELPER_ONLY=1
|
||||
source "$CLUSTER_APPLY"
|
||||
_restore_pveproxy_certificate "$source_node"
|
||||
) >>"$REPORT_FILE" 2>&1 \
|
||||
&& [[ ! -e "$call_log" ]] \
|
||||
&& [[ "$active_before" == "$(openssl x509 -in "$PMX_PVEPROXY_CERT_PATH" -noout -sha256 -fingerprint)" ]]; then
|
||||
pass "Mismatched certificate pair is rejected without changing the active certificate"
|
||||
else
|
||||
fail "Mismatched certificate pair was not rejected safely"
|
||||
fi
|
||||
|
||||
local replacement_node="$fixture/replacement-node"
|
||||
mkdir -p "$replacement_node"
|
||||
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
|
||||
-subj '/CN=proxmenux-rollback-test' \
|
||||
-keyout "$replacement_node/pveproxy-ssl.key" \
|
||||
-out "$replacement_node/pveproxy-ssl.pem" >>"$REPORT_FILE" 2>&1
|
||||
rm -f "$call_log"
|
||||
export PMX_TEST_SERVICE_INACTIVE=1
|
||||
if ! (
|
||||
PMX_CERT_HELPER_ONLY=1
|
||||
source "$CLUSTER_APPLY"
|
||||
_restore_pveproxy_certificate "$replacement_node"
|
||||
) >>"$REPORT_FILE" 2>&1 \
|
||||
&& [[ "$active_before" == "$(openssl x509 -in "$PMX_PVEPROXY_CERT_PATH" -noout -sha256 -fingerprint)" ]] \
|
||||
&& [[ "$(grep -c '^cert set ' "$call_log" 2>/dev/null)" == "2" ]]; then
|
||||
pass "Failed post-install verification restores the previous certificate"
|
||||
else
|
||||
fail "Certificate rollback did not restore the previous active pair"
|
||||
fi
|
||||
unset PMX_TEST_SERVICE_INACTIVE
|
||||
|
||||
unset PMX_PVENODE_BIN PMX_SYSTEMCTL_BIN PMX_PVEPROXY_CERT_PATH \
|
||||
PMX_PVEPROXY_KEY_PATH PMX_TEST_CALL_LOG PMX_PVEPROXY_VERIFY_ATTEMPTS
|
||||
}
|
||||
|
||||
staging_state_tests() {
|
||||
log "\n=== Staging state and custom paths ==="
|
||||
# shellcheck source=/dev/null
|
||||
source "$LIB_SCRIPT"
|
||||
|
||||
if hb_default_profile_paths | grep -Fxq '/var/lib/proxmenux/backup-jobs'; then
|
||||
pass "Default profile includes scheduled backup job definitions"
|
||||
else
|
||||
fail "Default profile does not include scheduled backup job definitions"
|
||||
fi
|
||||
|
||||
if ! help mapfile >/dev/null 2>&1; then
|
||||
skip "Staging copy tests require bash >= 4 and GNU rsync."
|
||||
return
|
||||
fi
|
||||
|
||||
local jobs_dir="$TMP_ROOT/staging-jobs"
|
||||
local custom_dir="$TMP_ROOT/custom-state"
|
||||
local stage_root="$TMP_ROOT/staging-output"
|
||||
local previous_jobs_dir="$HB_BACKUP_JOBS_DIR"
|
||||
local previous_state_dir="$HB_STATE_DIR"
|
||||
mkdir -p "$jobs_dir" "$custom_dir/images" "$custom_dir/tmp"
|
||||
echo "custom-state" > "$custom_dir/images/application.conf"
|
||||
echo "audit" > "$custom_dir/application.log"
|
||||
echo "temporary-but-selected" > "$custom_dir/tmp/state"
|
||||
cat > "$jobs_dir/staging-test.env" <<EOJ
|
||||
JOB_ID=staging-test
|
||||
BACKEND=local
|
||||
ON_CALENDAR=daily
|
||||
PROFILE_MODE=custom
|
||||
ENABLED=1
|
||||
LOCAL_DEST_DIR=/var/lib/vz/dump
|
||||
LOCAL_ARCHIVE_EXT=tar.gz
|
||||
EOJ
|
||||
echo "$custom_dir" > "$jobs_dir/staging-test.paths"
|
||||
|
||||
HB_BACKUP_JOBS_DIR="$jobs_dir"
|
||||
HB_STATE_DIR="$TMP_ROOT/operator-state"
|
||||
mkdir -p "$HB_STATE_DIR"
|
||||
printf '/root\n' > "$(hb_extra_paths_file)"
|
||||
if hb_path_is_operator_added /root && hb_path_is_operator_added "$custom_dir"; then
|
||||
pass "Persisted and non-default paths are classified as operator-added"
|
||||
else
|
||||
fail "Operator-added path classification failed"
|
||||
fi
|
||||
if PMX_BACKUP_NO_SYSTEMCTL=1 hb_prepare_staging "$stage_root" "$custom_dir" >>"$REPORT_FILE" 2>&1; then
|
||||
pass "Custom payload stages successfully"
|
||||
else
|
||||
fail "Custom payload staging failed"
|
||||
fi
|
||||
|
||||
if [[ -f "$stage_root/rootfs/${custom_dir#/}/images/application.conf" && \
|
||||
-f "$stage_root/rootfs/${custom_dir#/}/application.log" && \
|
||||
-f "$stage_root/rootfs/${custom_dir#/}/tmp/state" ]]; then
|
||||
pass "Custom path content is copied without generic exclusions"
|
||||
else
|
||||
fail "Custom path content was silently excluded from staging"
|
||||
fi
|
||||
if [[ -f "$stage_root/rootfs/${jobs_dir#/}/staging-test.env" ]]; then
|
||||
pass "Job definitions are included with a custom profile"
|
||||
else
|
||||
fail "Job definitions were not added to the custom profile"
|
||||
fi
|
||||
if [[ ! -s "$stage_root/metadata/failed_paths.txt" ]]; then
|
||||
pass "Successful staging has no failed paths"
|
||||
else
|
||||
fail "Successful staging unexpectedly reports failed paths"
|
||||
fi
|
||||
HB_BACKUP_JOBS_DIR="$previous_jobs_dir"
|
||||
HB_STATE_DIR="$previous_state_dir"
|
||||
}
|
||||
|
||||
restored_job_reconcile_tests() {
|
||||
log "\n=== Restored job reconciliation (sandbox) ==="
|
||||
local jobs_dir="$TMP_ROOT/reconcile-jobs"
|
||||
local logs_dir="$TMP_ROOT/reconcile-logs"
|
||||
local systemd_dir="$TMP_ROOT/reconcile-systemd"
|
||||
local marker="$TMP_ROOT/unsafe-evaluated"
|
||||
mkdir -p "$jobs_dir" "$logs_dir" "$systemd_dir"
|
||||
|
||||
cat > "$jobs_dir/enabled.env" <<'EOJ'
|
||||
JOB_ID=enabled
|
||||
BACKEND=local
|
||||
ON_CALENDAR=Mon..Fri\ 03:00
|
||||
PROFILE_MODE=custom
|
||||
ENABLED=1
|
||||
LOCAL_DEST_DIR=/var/lib/vz/dump
|
||||
LOCAL_ARCHIVE_EXT=tar.gz
|
||||
EOJ
|
||||
echo "/etc/hosts" > "$jobs_dir/enabled.paths"
|
||||
|
||||
cat > "$jobs_dir/attached.env" <<'EOJ'
|
||||
JOB_ID=attached
|
||||
BACKEND=pbs
|
||||
PVE_PARENT_JOB=backup-1
|
||||
PVE_STORAGE=pbs-main
|
||||
PROFILE_MODE=default
|
||||
ENABLED=1
|
||||
PBS_REPOSITORY=root@pam@pbs.example:datastore
|
||||
PBS_PASSWORD=''
|
||||
PBS_BACKUP_ID=hostcfg-test
|
||||
EOJ
|
||||
echo "/etc/hosts" > "$jobs_dir/attached.paths"
|
||||
|
||||
cat > "$jobs_dir/unsafe.env" <<EOJ
|
||||
JOB_ID=unsafe
|
||||
BACKEND=local
|
||||
ON_CALENDAR=daily
|
||||
PROFILE_MODE=custom
|
||||
ENABLED=1
|
||||
LOCAL_DEST_DIR=/var/lib/vz/dump
|
||||
LOCAL_ARCHIVE_EXT=tar.gz
|
||||
EVIL=\$(touch "$marker")
|
||||
EOJ
|
||||
echo "/etc/hosts" > "$jobs_dir/unsafe.paths"
|
||||
|
||||
if PMX_BACKUP_JOBS_DIR="$jobs_dir" PMX_BACKUP_LOG_DIR="$logs_dir" \
|
||||
PMX_BACKUP_SYSTEMD_DIR="$systemd_dir" PMX_BACKUP_NO_SYSTEMCTL=1 \
|
||||
bash "$SCHED_SCRIPT" --reconcile-restored >>"$REPORT_FILE" 2>&1; then
|
||||
fail "Reconciliation should report the invalid restored job"
|
||||
else
|
||||
pass "Reconciliation rejects an invalid restored job"
|
||||
fi
|
||||
|
||||
if assert_file_contains "$systemd_dir/proxmenux-backup-enabled.timer" "OnCalendar=Mon..Fri 03:00"; then
|
||||
pass "Valid standalone job timer is reconstructed"
|
||||
else
|
||||
fail "Valid standalone job timer was not reconstructed"
|
||||
fi
|
||||
if [[ ! -e "$systemd_dir/proxmenux-backup-attached.timer" ]]; then
|
||||
pass "PVE-attached job is not converted into a duplicate timer"
|
||||
else
|
||||
fail "PVE-attached job unexpectedly created a timer"
|
||||
fi
|
||||
if [[ ! -e "$systemd_dir/proxmenux-backup-unsafe.timer" && ! -e "$marker" ]]; then
|
||||
pass "Rejected job cannot create a unit or execute shell content"
|
||||
else
|
||||
fail "Rejected job produced side effects"
|
||||
fi
|
||||
}
|
||||
|
||||
scheduler_e2e_tests() {
|
||||
log "\n=== Scheduler E2E (sandbox) ==="
|
||||
if ! help mapfile >/dev/null 2>&1; then
|
||||
skip "Scheduler E2E skipped: current bash does not provide mapfile (requires bash >= 4)."
|
||||
return
|
||||
fi
|
||||
|
||||
local jobs_dir="$TMP_ROOT/backup-jobs"
|
||||
local logs_dir="$TMP_ROOT/backup-jobs-logs"
|
||||
local lock_dir="$TMP_ROOT/locks"
|
||||
local archives_dir="$TMP_ROOT/archives"
|
||||
|
||||
mkdir -p "$jobs_dir" "$logs_dir" "$lock_dir" "$archives_dir"
|
||||
|
||||
cat > "$jobs_dir/t1.env" <<EOJ
|
||||
JOB_ID=t1
|
||||
BACKEND=local
|
||||
PROFILE_MODE=custom
|
||||
LOCAL_DEST_DIR=${archives_dir}
|
||||
LOCAL_ARCHIVE_EXT=tar.gz
|
||||
KEEP_LAST=2
|
||||
KEEP_HOURLY=0
|
||||
KEEP_DAILY=0
|
||||
KEEP_WEEKLY=0
|
||||
KEEP_MONTHLY=0
|
||||
KEEP_YEARLY=0
|
||||
EOJ
|
||||
|
||||
cat > "$jobs_dir/t1.paths" <<EOP
|
||||
/etc/hosts
|
||||
/etc/resolv.conf
|
||||
EOP
|
||||
|
||||
local i
|
||||
for i in 1 2 3; do
|
||||
if PMX_BACKUP_JOBS_DIR="$jobs_dir" PMX_BACKUP_LOG_DIR="$logs_dir" PMX_BACKUP_LOCK_DIR="$lock_dir" \
|
||||
bash "$RUNNER" t1 >>"$REPORT_FILE" 2>&1; then
|
||||
:
|
||||
else
|
||||
fail "Runner execution #$i for t1"
|
||||
return
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
local archive_count
|
||||
archive_count="$(find "$archives_dir" -maxdepth 1 -type f -name 't1-*.tar.gz' | wc -l | tr -d ' ')"
|
||||
if [[ "$archive_count" == "2" ]]; then
|
||||
pass "Retention KEEP_LAST=2 keeps exactly 2 archives"
|
||||
else
|
||||
fail "Retention expected 2 archives, got $archive_count"
|
||||
fi
|
||||
|
||||
if assert_file_contains "$logs_dir/t1-last.status" "RESULT=ok"; then
|
||||
pass "t1-last.status reports RESULT=ok"
|
||||
else
|
||||
fail "t1-last.status does not report RESULT=ok"
|
||||
fi
|
||||
|
||||
cat > "$jobs_dir/tbad.env" <<EOJ
|
||||
JOB_ID=tbad
|
||||
BACKEND=invalid
|
||||
PROFILE_MODE=custom
|
||||
KEEP_LAST=1
|
||||
EOJ
|
||||
echo "/etc/hosts" > "$jobs_dir/tbad.paths"
|
||||
|
||||
run_cmd_expect_fail "Invalid backend fails" \
|
||||
env PMX_BACKUP_JOBS_DIR="$jobs_dir" PMX_BACKUP_LOG_DIR="$logs_dir" PMX_BACKUP_LOCK_DIR="$lock_dir" \
|
||||
bash "$RUNNER" tbad
|
||||
|
||||
if assert_file_contains "$logs_dir/tbad-last.status" "RESULT=failed"; then
|
||||
pass "tbad-last.status reports RESULT=failed"
|
||||
else
|
||||
fail "tbad-last.status does not report RESULT=failed"
|
||||
fi
|
||||
|
||||
cat > "$jobs_dir/tempty.env" <<EOJ
|
||||
JOB_ID=tempty
|
||||
BACKEND=local
|
||||
PROFILE_MODE=custom
|
||||
LOCAL_DEST_DIR=${archives_dir}
|
||||
LOCAL_ARCHIVE_EXT=tar.gz
|
||||
KEEP_LAST=1
|
||||
EOJ
|
||||
: > "$jobs_dir/tempty.paths"
|
||||
|
||||
run_cmd_expect_fail "Empty paths fails" \
|
||||
env PMX_BACKUP_JOBS_DIR="$jobs_dir" PMX_BACKUP_LOG_DIR="$logs_dir" PMX_BACKUP_LOCK_DIR="$lock_dir" \
|
||||
bash "$RUNNER" tempty
|
||||
|
||||
if assert_file_contains "$logs_dir/tempty-last.status" "RESULT=failed"; then
|
||||
pass "tempty-last.status reports RESULT=failed"
|
||||
else
|
||||
fail "tempty-last.status does not report RESULT=failed"
|
||||
fi
|
||||
}
|
||||
|
||||
pending_restore_tests() {
|
||||
log "\n=== Pending restore E2E (sandbox) ==="
|
||||
local pending_base="$TMP_ROOT/restore-pending"
|
||||
local logs_dir="$TMP_ROOT/restore-logs"
|
||||
local target_root="$TMP_ROOT/target"
|
||||
local pre_backup_base="$TMP_ROOT/pre-restore"
|
||||
local recovery_base="$TMP_ROOT/recovery"
|
||||
|
||||
mkdir -p "$pending_base/r1/rootfs/etc/pve" "$pending_base/r1/rootfs/etc/zfs" "$pending_base/r1/rootfs/etc" "$target_root/etc"
|
||||
|
||||
echo "new-value" > "$pending_base/r1/rootfs/etc/test.conf"
|
||||
echo "cluster-data" > "$pending_base/r1/rootfs/etc/pve/cluster.cfg"
|
||||
echo "zfs-data" > "$pending_base/r1/rootfs/etc/zfs/zpool.cache"
|
||||
echo "old-value" > "$target_root/etc/test.conf"
|
||||
|
||||
cat > "$pending_base/r1/apply-on-boot.list" <<EOL
|
||||
etc/test.conf
|
||||
etc/pve/cluster.cfg
|
||||
etc/zfs/zpool.cache
|
||||
EOL
|
||||
|
||||
cat > "$pending_base/r1/plan.env" <<EOP
|
||||
HB_RESTORE_INCLUDE_ZFS=0
|
||||
EOP
|
||||
|
||||
ln -sfn "$pending_base/r1" "$pending_base/current"
|
||||
|
||||
if PMX_RESTORE_PENDING_BASE="$pending_base" PMX_RESTORE_LOG_DIR="$logs_dir" \
|
||||
PMX_RESTORE_DEST_PREFIX="$target_root" PMX_RESTORE_PRE_BACKUP_BASE="$pre_backup_base" \
|
||||
PMX_RESTORE_RECOVERY_BASE="$recovery_base" \
|
||||
bash "$APPLY_ONBOOT" >>"$REPORT_FILE" 2>&1; then
|
||||
pass "apply_pending_restore completes"
|
||||
else
|
||||
fail "apply_pending_restore completes"
|
||||
return
|
||||
fi
|
||||
|
||||
if assert_file_contains "$target_root/etc/test.conf" "new-value"; then
|
||||
pass "Regular file restored into target prefix"
|
||||
else
|
||||
fail "Regular file was not restored"
|
||||
fi
|
||||
|
||||
if [[ -e "$target_root/etc/pve/cluster.cfg" ]]; then
|
||||
fail "Cluster file should not be restored live"
|
||||
else
|
||||
pass "Cluster file skipped from live restore"
|
||||
fi
|
||||
|
||||
if find "$recovery_base" -type f -name cluster.cfg 2>/dev/null | grep -q .; then
|
||||
pass "Cluster file extracted to recovery directory"
|
||||
else
|
||||
fail "Cluster file not found in recovery directory"
|
||||
fi
|
||||
|
||||
if assert_file_contains "$pending_base/completed/r1/state" "completed"; then
|
||||
pass "Pending restore state marked completed"
|
||||
else
|
||||
fail "Pending restore state not marked completed"
|
||||
fi
|
||||
|
||||
if [[ -e "$pending_base/current" ]]; then
|
||||
fail "current symlink should be removed"
|
||||
else
|
||||
pass "current symlink removed"
|
||||
fi
|
||||
}
|
||||
|
||||
pending_jobs_restore_tests() {
|
||||
log "\n=== Pending restore of scheduled jobs (sandbox) ==="
|
||||
if ! help mapfile >/dev/null 2>&1; then
|
||||
skip "Pending scheduled-job restore test requires the Linux runtime."
|
||||
return
|
||||
fi
|
||||
local pending_base="$TMP_ROOT/jobs-restore-pending"
|
||||
local logs_dir="$TMP_ROOT/jobs-restore-logs"
|
||||
local target_root="$TMP_ROOT/jobs-restore-target"
|
||||
local pre_backup_base="$TMP_ROOT/jobs-pre-restore"
|
||||
local recovery_base="$TMP_ROOT/jobs-recovery"
|
||||
local restored_jobs="$target_root/var/lib/proxmenux/backup-jobs"
|
||||
local source_jobs="$pending_base/r2/rootfs/var/lib/proxmenux/backup-jobs"
|
||||
|
||||
mkdir -p "$source_jobs" "$target_root"
|
||||
cat > "$source_jobs/restored.env" <<'EOJ'
|
||||
JOB_ID=restored
|
||||
BACKEND=local
|
||||
ON_CALENDAR=daily
|
||||
PROFILE_MODE=custom
|
||||
ENABLED=1
|
||||
LOCAL_DEST_DIR=/var/lib/vz/dump
|
||||
LOCAL_ARCHIVE_EXT=tar.gz
|
||||
EOJ
|
||||
echo "/etc/hosts" > "$source_jobs/restored.paths"
|
||||
echo "var/lib/proxmenux/backup-jobs" > "$pending_base/r2/apply-on-boot.list"
|
||||
echo "HB_RESTORE_INCLUDE_ZFS=0" > "$pending_base/r2/plan.env"
|
||||
ln -sfn "$pending_base/r2" "$pending_base/current"
|
||||
|
||||
if PMX_RESTORE_PENDING_BASE="$pending_base" PMX_RESTORE_LOG_DIR="$logs_dir" \
|
||||
PMX_RESTORE_DEST_PREFIX="$target_root" PMX_RESTORE_PRE_BACKUP_BASE="$pre_backup_base" \
|
||||
PMX_RESTORE_RECOVERY_BASE="$recovery_base" \
|
||||
bash "$APPLY_ONBOOT" >>"$REPORT_FILE" 2>&1; then
|
||||
pass "Pending restore applies scheduled job definitions"
|
||||
else
|
||||
fail "Pending restore failed while applying scheduled job definitions"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -f "$restored_jobs/restored.env" && \
|
||||
-f "$target_root/etc/systemd/system/proxmenux-backup-restored.timer" ]]; then
|
||||
pass "Pending restore reconstructs the scheduled timer"
|
||||
else
|
||||
fail "Pending restore did not reconstruct the scheduled timer"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
log "ProxMenux backup/restore test matrix"
|
||||
log "Report: $REPORT_FILE"
|
||||
log "Temp root: $TMP_ROOT"
|
||||
|
||||
syntax_tests
|
||||
certificate_restore_tests
|
||||
staging_state_tests
|
||||
restored_job_reconcile_tests
|
||||
scheduler_e2e_tests
|
||||
pending_restore_tests
|
||||
pending_jobs_restore_tests
|
||||
|
||||
log "\n=== Summary ==="
|
||||
log "PASS=$PASS"
|
||||
log "FAIL=$FAIL"
|
||||
log "SKIP=$SKIP"
|
||||
|
||||
if [[ "$FAIL" -eq 0 ]]; then
|
||||
log "RESULT=OK"
|
||||
exit 0
|
||||
else
|
||||
log "RESULT=FAILED"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
TEST_ROOT=$(mktemp -d)
|
||||
trap 'rm -rf "$TEST_ROOT"' EXIT
|
||||
|
||||
export PROXMENUX_SYSFS_ROOT="$TEST_ROOT/sys"
|
||||
export PROXMENUX_ETC_ROOT="$TEST_ROOT/etc"
|
||||
export PROXMENUX_STATE_ROOT="$TEST_ROOT/usr/local/share/proxmenux"
|
||||
export PROXMENUX_VFIO_BIND_STATE="$PROXMENUX_ETC_ROOT/proxmenux/vfio-bind.bdfs"
|
||||
export PROXMENUX_VFIO_BIND_UDEV_RULE="$PROXMENUX_ETC_ROOT/udev/rules.d/10-proxmenux-vfio-bind.rules"
|
||||
export PROXMENUX_VFIO_CONF="$PROXMENUX_ETC_ROOT/modprobe.d/vfio.conf"
|
||||
export PROXMENUX_NVIDIA_VFIO_BLACKLIST="$PROXMENUX_ETC_ROOT/modprobe.d/proxmenux-nvidia-vfio-blacklist.conf"
|
||||
export PROXMENUX_NVIDIA_SERVICE_STATE="$PROXMENUX_STATE_ROOT/nvidia-host-services.state"
|
||||
export PROXMENUX_NVIDIA_SERVICE_LEGACY_STATE="$TEST_ROOT/var/lib/proxmenux/nvidia-host-services.state"
|
||||
export PROXMENUX_VFIO_BIND_LEGACY_HOOK="$PROXMENUX_ETC_ROOT/initramfs-tools/scripts/init-top/proxmenux-vfio-bind"
|
||||
export SYSTEMCTL_LOG="$TEST_ROOT/systemctl.log"
|
||||
|
||||
mkdir -p "$PROXMENUX_SYSFS_ROOT/bus/pci/devices" "$PROXMENUX_ETC_ROOT/modprobe.d" "$TEST_ROOT/bin"
|
||||
|
||||
# The policy helper manages systemd services on a real host. Unit tests must
|
||||
# never touch them, so provide an inert command before sourcing the helper.
|
||||
cat > "$TEST_ROOT/bin/systemctl" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
case "${1:-}" in
|
||||
is-enabled|is-active) exit 1 ;;
|
||||
*) printf '%s\n' "$*" >> "$SYSTEMCTL_LOG"; exit 0 ;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "$TEST_ROOT/bin/systemctl"
|
||||
export PATH="$TEST_ROOT/bin:$PATH"
|
||||
|
||||
make_pci_device() {
|
||||
local bdf="$1" vendor="$2" device="$3" class="$4"
|
||||
local path="$PROXMENUX_SYSFS_ROOT/bus/pci/devices/$bdf"
|
||||
mkdir -p "$path"
|
||||
printf '0x%s\n' "$vendor" > "$path/vendor"
|
||||
printf '0x%s\n' "$device" > "$path/device"
|
||||
printf '0x%s\n' "$class" > "$path/class"
|
||||
}
|
||||
|
||||
# Two identical NVIDIA GPUs reproduce the collision that vendor:device
|
||||
# binding caused: both share 10de:2484 but must be independently reversible.
|
||||
make_pci_device 0000:01:00.0 10de 2484 030000
|
||||
make_pci_device 0000:01:00.1 10de 228b 040300
|
||||
make_pci_device 0000:02:00.0 10de 2484 030000
|
||||
make_pci_device 0000:02:00.1 10de 228b 040300
|
||||
make_pci_device 0000:00:1f.3 8086 7ad0 040300
|
||||
|
||||
cat > "$PROXMENUX_VFIO_CONF" <<'EOF'
|
||||
options vfio-pci ids=10DE:2484,10DE:228B,8086:7ad0 disable_vga=1
|
||||
EOF
|
||||
|
||||
HOST_CONFIG_CHANGED=false
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
# shellcheck source=../../global/pci_passthrough_helpers.sh
|
||||
source "$SCRIPT_DIR/../../global/pci_passthrough_helpers.sh"
|
||||
|
||||
_proxmenux_vfio_bind_migrate_legacy_nvidia_ids
|
||||
|
||||
grep -qxF '0000:01:00.0' "$PROXMENUX_VFIO_BIND_STATE"
|
||||
grep -qxF '0000:01:00.1' "$PROXMENUX_VFIO_BIND_STATE"
|
||||
grep -qxF '0000:02:00.0' "$PROXMENUX_VFIO_BIND_STATE"
|
||||
grep -qxF '0000:02:00.1' "$PROXMENUX_VFIO_BIND_STATE"
|
||||
grep -q 'ids=8086:7ad0' "$PROXMENUX_VFIO_CONF"
|
||||
! grep -qi '10de:2484' "$PROXMENUX_VFIO_CONF"
|
||||
! grep -qi '10de:228b' "$PROXMENUX_VFIO_CONF"
|
||||
_proxmenux_all_nvidia_in_vfio
|
||||
[[ -f "$PROXMENUX_NVIDIA_VFIO_BLACKLIST" ]]
|
||||
|
||||
# Returning only one GPU to the host must keep the other exact BDF in VFIO,
|
||||
# remove the global NVIDIA blacklist, retain NVIDIA softdeps, and restore any
|
||||
# service state captured by the short-lived /var/lib implementation.
|
||||
mkdir -p "$(dirname "$PROXMENUX_NVIDIA_SERVICE_LEGACY_STATE")"
|
||||
printf '%s\n' 'nvidia-persistenced.service enabled=1 active=1' \
|
||||
> "$PROXMENUX_NVIDIA_SERVICE_LEGACY_STATE"
|
||||
_proxmenux_vfio_bind_remove_bdfs 0000:01:00.0 0000:01:00.1
|
||||
! _proxmenux_vfio_bind_has_bdf 0000:01:00.0
|
||||
! _proxmenux_vfio_bind_has_bdf 0000:01:00.1
|
||||
_proxmenux_vfio_bind_has_bdf 0000:02:00.0
|
||||
_proxmenux_vfio_bind_has_bdf 0000:02:00.1
|
||||
! _proxmenux_all_nvidia_in_vfio
|
||||
[[ ! -e "$PROXMENUX_NVIDIA_VFIO_BLACKLIST" ]]
|
||||
grep -qFx 'softdep nvidia pre: vfio-pci' "$PROXMENUX_VFIO_CONF"
|
||||
[[ ! -e "$PROXMENUX_NVIDIA_SERVICE_LEGACY_STATE" ]]
|
||||
[[ ! -e "$PROXMENUX_NVIDIA_SERVICE_STATE" ]]
|
||||
grep -qFx 'enable nvidia-persistenced.service' "$SYSTEMCTL_LOG"
|
||||
grep -qFx 'start nvidia-persistenced.service' "$SYSTEMCTL_LOG"
|
||||
|
||||
# Returning the final NVIDIA GPU removes the remaining per-BDF policy while
|
||||
# leaving unrelated Intel VFIO configuration untouched.
|
||||
_proxmenux_vfio_bind_remove_bdfs 0000:02:00.0 0000:02:00.1
|
||||
! _proxmenux_vfio_bind_has_entries
|
||||
! grep -qFx 'softdep nvidia pre: vfio-pci' "$PROXMENUX_VFIO_CONF"
|
||||
grep -q 'ids=8086:7ad0' "$PROXMENUX_VFIO_CONF"
|
||||
|
||||
echo "PASS: NVIDIA per-BDF migration and selective restore"
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenux - Update or recreate one OCI instance
|
||||
# ==========================================================
|
||||
# Author : MacRimi
|
||||
# Copyright : (c) 2024 MacRimi
|
||||
# License : GPL-3.0
|
||||
# https://github.com/MacRimi/ProxMenux/blob/main/LICENSE
|
||||
# Version : 1.0
|
||||
# ==========================================================
|
||||
# Description:
|
||||
# Runs in the ProxMenux Monitor terminal. Opens the same flow as
|
||||
# OCI manager Apps -> Manage installed OCI applications for one
|
||||
# container, without the list:
|
||||
#
|
||||
# VMID - the container (required)
|
||||
# ACTION - "update" or "recreate" (required)
|
||||
# KEEP_BACKUP - storage where the backup taken before the
|
||||
# update is kept (optional)
|
||||
# ==========================================================
|
||||
|
||||
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
|
||||
if [[ -f "$UTILS_FILE" ]]; then
|
||||
source "$UTILS_FILE"
|
||||
fi
|
||||
load_language
|
||||
initialize_cache
|
||||
|
||||
if [[ ! ${VMID:-} =~ ^[0-9]{1,9}$ ]]; then
|
||||
msg_error "$(translate "Invalid VMID")"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ${ACTION:-} != "update" && ${ACTION:-} != "recreate" ]]; then
|
||||
msg_error "$(translate "Invalid action")"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
args=(manage "$VMID" --action "$ACTION")
|
||||
if [[ -n ${KEEP_BACKUP:-} ]]; then
|
||||
if [[ ! $KEEP_BACKUP =~ ^[A-Za-z0-9._-]{1,64}$ ]]; then
|
||||
msg_error "$(translate "Invalid storage name")"
|
||||
exit 1
|
||||
fi
|
||||
args+=(--keep-backup "$KEEP_BACKUP")
|
||||
fi
|
||||
|
||||
exec bash "$LOCAL_SCRIPTS/oci/oci_manager_apps.sh" "${args[@]}"
|
||||
@@ -291,7 +291,7 @@ RateLimitIntervalSec=30s
|
||||
RateLimitBurst=1000
|
||||
# Disable Journald forwarding to syslog
|
||||
ForwardToSyslog=no
|
||||
# Don't forward to wall (para evitar mensajes en terminales)
|
||||
# Don't forward to wall: it would print on every open terminal
|
||||
ForwardToWall=no
|
||||
# Disable signing of the logs, save cpu resources
|
||||
Seal=no
|
||||
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_ROOT="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||
CUSTOM_SCRIPT="$SCRIPT_ROOT/customizable_post_install.sh"
|
||||
UNINSTALL_SCRIPT="$SCRIPT_ROOT/uninstall-tools.sh"
|
||||
TEST_ROOT="$(mktemp -d)"
|
||||
trap 'rm -rf "$TEST_ROOT"' EXIT
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_eq() {
|
||||
[[ "$1" == "$2" ]] || fail "expected '$2', got '$1'"
|
||||
}
|
||||
|
||||
for script in "$CUSTOM_SCRIPT" "$UNINSTALL_SCRIPT"; do
|
||||
[[ -r "$script" ]] || fail "missing script: $script"
|
||||
done
|
||||
|
||||
# Load only the function libraries. Redirecting the built-in installation
|
||||
# paths keeps the test isolated from a real ProxMenux installation.
|
||||
load_language() { :; }
|
||||
initialize_cache() { :; }
|
||||
translate() { printf '%s' "$1"; }
|
||||
msg_info() { :; }
|
||||
msg_info2() { :; }
|
||||
msg_ok() { :; }
|
||||
msg_warn() { :; }
|
||||
msg_error() { :; }
|
||||
msg_success() { :; }
|
||||
show_proxmenux_logo() { :; }
|
||||
clear() { :; }
|
||||
|
||||
sed \
|
||||
-e "s|^LOCAL_SCRIPTS=.*|LOCAL_SCRIPTS=\"$TEST_ROOT/missing-scripts\"|" \
|
||||
-e "s|^BASE_DIR=.*|BASE_DIR=\"$TEST_ROOT/state\"|" \
|
||||
-e "s|^UTILS_FILE=.*|UTILS_FILE=\"$TEST_ROOT/missing-utils.sh\"|" \
|
||||
-e "s|^TOOLS_JSON=.*|TOOLS_JSON=\"$TEST_ROOT/installed_tools.json\"|" \
|
||||
"$CUSTOM_SCRIPT" > "$TEST_ROOT/customizable.sh"
|
||||
# shellcheck disable=SC1090
|
||||
source "$TEST_ROOT/customizable.sh"
|
||||
|
||||
BASE_DIR="$TEST_ROOT/state"
|
||||
TOOLS_JSON="$TEST_ROOT/installed_tools.json"
|
||||
mkdir -p "$BASE_DIR"
|
||||
printf '{}\n' > "$TOOLS_JSON"
|
||||
REGISTRY_LOG="$TEST_ROOT/registry.log"
|
||||
register_tool() { printf '%s|%s|%s\n' "$1" "$2" "${3:-}" >> "$REGISTRY_LOG"; }
|
||||
|
||||
# RPC: preserve asymmetric service/socket states and expose rollback even if
|
||||
# a later disable operation were to fail.
|
||||
declare -A UNIT_ENABLED=(
|
||||
[rpcbind.socket]="enabled"
|
||||
[rpcbind.service]="disabled"
|
||||
)
|
||||
declare -A UNIT_ACTIVE=(
|
||||
[rpcbind.socket]="active"
|
||||
[rpcbind.service]="inactive"
|
||||
)
|
||||
|
||||
systemctl() {
|
||||
local action="$1"
|
||||
shift
|
||||
case "$action" in
|
||||
show)
|
||||
local unit="${@: -1}"
|
||||
printf 'loaded\n'
|
||||
;;
|
||||
is-enabled)
|
||||
printf '%s\n' "${UNIT_ENABLED[$1]:-disabled}"
|
||||
;;
|
||||
is-active)
|
||||
printf '%s\n' "${UNIT_ACTIVE[$1]:-inactive}"
|
||||
;;
|
||||
disable)
|
||||
[[ "${1:-}" == "--now" ]] && shift
|
||||
local unit
|
||||
for unit in "$@"; do
|
||||
UNIT_ENABLED[$unit]="disabled"
|
||||
UNIT_ACTIVE[$unit]="inactive"
|
||||
done
|
||||
;;
|
||||
enable)
|
||||
local runtime=false
|
||||
if [[ "${1:-}" == "--runtime" ]]; then
|
||||
runtime=true
|
||||
shift
|
||||
fi
|
||||
UNIT_ENABLED[$1]="$([[ "$runtime" == true ]] && printf 'enabled-runtime' || printf 'enabled')"
|
||||
;;
|
||||
mask)
|
||||
local runtime=false
|
||||
if [[ "${1:-}" == "--runtime" ]]; then
|
||||
runtime=true
|
||||
shift
|
||||
fi
|
||||
UNIT_ENABLED[$1]="$([[ "$runtime" == true ]] && printf 'masked-runtime' || printf 'masked')"
|
||||
;;
|
||||
start) UNIT_ACTIVE[$1]="active" ;;
|
||||
stop) UNIT_ACTIVE[$1]="inactive" ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
disable_rpc
|
||||
grep -Fqx 'rpcbind.socket|enabled|active' "$BASE_DIR/rpcbind.state" || fail "rpcbind.socket state was not recorded"
|
||||
grep -Fqx 'rpcbind.service|disabled|inactive' "$BASE_DIR/rpcbind.state" || fail "rpcbind.service state was not recorded"
|
||||
grep -Fqx 'rpc|true|1.1' "$REGISTRY_LOG" || fail "RPC was not registered with version 1.1"
|
||||
assert_eq "${UNIT_ENABLED[rpcbind.socket]}" "disabled"
|
||||
assert_eq "${UNIT_ACTIVE[rpcbind.socket]}" "inactive"
|
||||
|
||||
# Source uninstall functions without launching their interactive menu.
|
||||
sed \
|
||||
-e "s|^LOCAL_SCRIPTS=.*|LOCAL_SCRIPTS=\"$TEST_ROOT/missing-scripts\"|" \
|
||||
-e "s|^BASE_DIR=.*|BASE_DIR=\"$TEST_ROOT/state\"|" \
|
||||
-e "s|^UTILS_FILE=.*|UTILS_FILE=\"$TEST_ROOT/missing-utils.sh\"|" \
|
||||
-e "s|^TOOLS_JSON=.*|TOOLS_JSON=\"$TEST_ROOT/installed_tools.json\"|" \
|
||||
-e '/^show_uninstall_menu$/d' \
|
||||
"$UNINSTALL_SCRIPT" > "$TEST_ROOT/uninstall.sh"
|
||||
# shellcheck disable=SC1090
|
||||
source "$TEST_ROOT/uninstall.sh"
|
||||
BASE_DIR="$TEST_ROOT/state"
|
||||
TOOLS_JSON="$TEST_ROOT/installed_tools.json"
|
||||
register_tool() { printf '%s|%s|%s\n' "$1" "$2" "${3:-}" >> "$REGISTRY_LOG"; }
|
||||
|
||||
uninstall_rpc
|
||||
assert_eq "${UNIT_ENABLED[rpcbind.socket]}" "enabled"
|
||||
assert_eq "${UNIT_ACTIVE[rpcbind.socket]}" "active"
|
||||
assert_eq "${UNIT_ENABLED[rpcbind.service]}" "disabled"
|
||||
assert_eq "${UNIT_ACTIVE[rpcbind.service]}" "inactive"
|
||||
[[ ! -e "$BASE_DIR/rpcbind.state" ]] || fail "RPC state was not removed after a successful restore"
|
||||
|
||||
# MOTD: verify both an existing file and a previously absent file are
|
||||
# restored byte-for-byte to their original state.
|
||||
PROXMENUX_MOTD_FILE="$TEST_ROOT/motd"
|
||||
export PROXMENUX_MOTD_FILE
|
||||
printf 'Original line\n\nSecond line\n' > "$PROXMENUX_MOTD_FILE"
|
||||
cp "$PROXMENUX_MOTD_FILE" "$TEST_ROOT/motd.expected"
|
||||
rm -f "$BASE_DIR/motd.state" "$BASE_DIR/motd.original"
|
||||
setup_motd
|
||||
uninstall_motd
|
||||
cmp -s "$PROXMENUX_MOTD_FILE" "$TEST_ROOT/motd.expected" || fail "existing MOTD was not restored exactly"
|
||||
|
||||
rm -f "$PROXMENUX_MOTD_FILE" "$BASE_DIR/motd.state" "$BASE_DIR/motd.original"
|
||||
setup_motd
|
||||
uninstall_motd
|
||||
[[ ! -e "$PROXMENUX_MOTD_FILE" ]] || fail "previously absent MOTD was not removed on restore"
|
||||
|
||||
# System utilities: only packages absent before this invocation may be
|
||||
# recorded for a later purge.
|
||||
INSTALLED_PACKAGES="$TEST_ROOT/installed-packages"
|
||||
printf 'curl\n' > "$INSTALLED_PACKAGES"
|
||||
PROXMENUX_UTILS=(
|
||||
'curl:curl:curl client'
|
||||
'htop:htop:process viewer'
|
||||
)
|
||||
dialog() { printf '"curl" "htop"' >&2; }
|
||||
ensure_repositories() { return 0; }
|
||||
dpkg-query() {
|
||||
local package="${@: -1}"
|
||||
if grep -Fqx "$package" "$INSTALLED_PACKAGES"; then
|
||||
printf 'install ok installed\n'
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
install_single_package() {
|
||||
local package="$1"
|
||||
grep -Fqx "$package" "$INSTALLED_PACKAGES" || printf '%s\n' "$package" >> "$INSTALLED_PACKAGES"
|
||||
return 0
|
||||
}
|
||||
rm -f "$BASE_DIR/system_utils.packages"
|
||||
install_system_utils
|
||||
grep -Fqx 'htop' "$BASE_DIR/system_utils.packages" || fail "new utility was not tracked"
|
||||
if grep -Fqx 'curl' "$BASE_DIR/system_utils.packages"; then
|
||||
fail "pre-existing utility was incorrectly tracked for removal"
|
||||
fi
|
||||
|
||||
# Static contracts from the audit. Capture function bodies first so
|
||||
# `set -o pipefail` cannot mistake grep -q's early exit for a failure in
|
||||
# `declare -f` caused by SIGPIPE.
|
||||
install_ceph_body="$(declare -f install_ceph)"
|
||||
enable_kexec_body="$(declare -f enable_kexec)"
|
||||
if grep -q 'apt-key' <<< "$install_ceph_body"; then
|
||||
fail "install_ceph still contains apt-key"
|
||||
fi
|
||||
grep -q 'Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg' <<< "$install_ceph_body" || fail "Ceph deb822 keyring is missing"
|
||||
grep -q 'is already installed' <<< "$enable_kexec_body" || fail "kexec package state message is missing"
|
||||
grep -q 'service file is already configured' <<< "$enable_kexec_body" || fail "kexec service state message is missing"
|
||||
if grep -Eq '^[[:space:]]*(lvm_repair|repo_cleanup|apt_upgrade)\)' "$UNINSTALL_SCRIPT"; then
|
||||
fail "orphaned uninstall menu entries remain"
|
||||
fi
|
||||
if declare -F uninstall_apt_upgrade >/dev/null; then
|
||||
fail "unsafe apt full-upgrade uninstaller remains"
|
||||
fi
|
||||
|
||||
printf 'PASS: post-install audit fixes\n'
|
||||
@@ -166,8 +166,7 @@ done <<< "$IMAGES"
|
||||
# `--separate-output` prints each selected tag on its own line with no
|
||||
# quoting, so we never need `eval` to split the output. The previous form
|
||||
# `eval "declare -a A=($SELECTED)"` would execute backticks / $(...) baked
|
||||
# into a filename — perfectly legal on ext4 — as shell commands. Audit
|
||||
# Tier 6 — `import-disk-image.sh` `eval` sobre salida del dialog.
|
||||
# into a filename — perfectly legal on ext4 — as shell commands.
|
||||
SELECTED_IMAGES_STR=$(dialog --backtitle "$BACKTITLE" \
|
||||
--separate-output \
|
||||
--title "$(translate 'Select Disk Images')" \
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
# ==========================================================
|
||||
# Guest Agent Configurator - ProxMenux
|
||||
# ==========================================================
|
||||
# Añade soporte al QEMU Guest Agent y dispositivos útiles.
|
||||
# Se adapta según el sistema operativo.
|
||||
# Adds QEMU Guest Agent support and the useful devices, per guest OS.
|
||||
# ==========================================================
|
||||
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
@@ -28,7 +27,7 @@ function configure_guest_agent() {
|
||||
|
||||
msg_info "$(translate "Adding QEMU Guest Agent support...")"
|
||||
|
||||
# Habilitar el agente en la VM
|
||||
# Enable the agent on the VM
|
||||
qm set "$VMID" -agent enabled=1 >/dev/null 2>&1
|
||||
|
||||
# Añadir canal de comunicación virtio
|
||||
|
||||
@@ -211,7 +211,7 @@ function configure_guest_agent() {
|
||||
|
||||
msg_info "$(translate "Adding QEMU Guest Agent support...")"
|
||||
|
||||
# Habilitar el agente en la VM
|
||||
# Enable the agent on the VM
|
||||
qm set "$VMID" -agent enabled=1 >/dev/null 2>&1
|
||||
|
||||
# Añadir canal de comunicación virtio
|
||||
|
||||
Reference in New Issue
Block a user