mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-15 11:16:50 +00:00
ProxMenux modifies the host: it rewrites configuration files, installs packages, enables services. Until now nobody could say afterwards what had changed, and showing the script does not answer that question — a four-hundred-line function may alter two values, and the reader has no way to know which two. This adds the two halves of an answer. The change journal records what ProxMenux does as it does it. Eleven bash primitives capture the previous state, apply the change and record it in the same step, writing to a spool that the Monitor reads back. One hundred and thirteen functions across twenty-five scripts are instrumented, covering post-install, shared storage, security tooling, container conversions, disk operations and the PVE 8 to 9 upgrade path. The page shows the difference — rotate 7 becoming rotate 14 — and never the script. Restore and backup scripts are deliberately left out: a restore puts the host back to a state some other script already recorded. The Audit and reports page answers the other half: what state is this host in, regardless of who put it there. Forty-three checks across seven areas read the host and classify each result as critical, warning, observation, conformant, unverified or not applicable, with the evidence they read attached to each one. A declared policy lets the reader say what this particular host is expected to do — which guests must have a backup, which storages are essential — so the report judges the host against its own intent rather than a generic template. An inventory records the hardware, network and guest topology behind those readings, a comparison shows what moved between two runs, and six report profiles produce a printable document scoped to what the reader needs. Everything is available in the eight supported languages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
429 lines
15 KiB
Bash
429 lines
15 KiB
Bash
#!/bin/bash
|
|
# ==========================================================
|
|
# Common Functions for Proxmox VE Scripts
|
|
# ==========================================================
|
|
|
|
# Configuration
|
|
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
|
BASE_DIR="/usr/local/share/proxmenux"
|
|
UTILS_FILE="$BASE_DIR/utils.sh"
|
|
TOOLS_JSON="/usr/local/share/proxmenux/installed_tools.json"
|
|
|
|
if [[ -f "$UTILS_FILE" ]]; then
|
|
source "$UTILS_FILE"
|
|
fi
|
|
if [[ -f "$LOCAL_SCRIPTS/global/pmx_journal.sh" ]]; then
|
|
source "$LOCAL_SCRIPTS/global/pmx_journal.sh"
|
|
fi
|
|
|
|
load_language
|
|
initialize_cache
|
|
|
|
|
|
get_pve_info() {
|
|
local pve_full_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
|
local pve_major=$(echo "$pve_full_version" | cut -d. -f1)
|
|
local os_codename="$(grep "VERSION_CODENAME=" /etc/os-release | cut -d"=" -f 2 | xargs)"
|
|
|
|
if [ -z "$os_codename" ]; then
|
|
os_codename=$(lsb_release -cs 2>/dev/null)
|
|
fi
|
|
|
|
|
|
local target_codename
|
|
if [ "$pve_major" -ge 9 ] 2>/dev/null; then
|
|
target_codename="trixie"
|
|
else
|
|
target_codename="$os_codename"
|
|
if [ -z "$target_codename" ]; then
|
|
target_codename="bookworm"
|
|
fi
|
|
fi
|
|
|
|
echo "$pve_full_version|$pve_major|$os_codename|$target_codename"
|
|
}
|
|
|
|
|
|
lvm_repair_check() {
|
|
msg_info "$(translate "Checking and repairing old LVM PV headers (if needed)...")"
|
|
|
|
if ! command -v pvs >/dev/null 2>&1; then
|
|
msg_info "$(translate "LVM tools not available, skipping LVM check")"
|
|
return
|
|
fi
|
|
|
|
pvs_output=$(LC_ALL=C pvs -v 2>&1 | grep "old PV header" || true)
|
|
if [ -z "$pvs_output" ]; then
|
|
msg_ok "$(translate "No PVs with old headers found.")"
|
|
return
|
|
fi
|
|
|
|
declare -A vg_map
|
|
while read -r line; do
|
|
pv=$(echo "$line" | grep -o '/dev/[^ ]*' || true)
|
|
if [ -n "$pv" ]; then
|
|
vg=$(pvs -o vg_name --noheadings "$pv" 2>/dev/null | awk '{print $1}' || true)
|
|
if [ -n "$vg" ]; then
|
|
vg_map["$vg"]=1
|
|
fi
|
|
fi
|
|
done <<< "$pvs_output"
|
|
|
|
for vg in "${!vg_map[@]}"; do
|
|
msg_warn "$(translate "Old PV header(s) found in VG $vg. Updating metadata...")"
|
|
vgck --updatemetadata "$vg" 2>/dev/null
|
|
vgchange -ay "$vg" 2>/dev/null
|
|
if [ $? -ne 0 ]; then
|
|
msg_warn "$(translate "Metadata update failed for VG $vg. Review manually.")"
|
|
else
|
|
msg_ok "$(translate "Metadata updated successfully for VG $vg")"
|
|
fi
|
|
done
|
|
|
|
msg_ok "$(translate "LVM PV headers check completed")"
|
|
}
|
|
|
|
|
|
|
|
|
|
cleanup_duplicate_repos_pve9() {
|
|
local FUNC_VERSION="1.0"
|
|
pmx_journal_context "cleanup_duplicate_repos_pve9" "$FUNC_VERSION"
|
|
msg_info "$(translate "Cleaning up duplicate repositories...")"
|
|
|
|
local sources_file="/etc/apt/sources.list"
|
|
local cleaned_count=0
|
|
|
|
# Helper: extract a DEB822 field's value from a .sources file. Handles
|
|
# both `Field: value` and folded continuations. Returns the FIRST value
|
|
# only (URIs / Suites / Components with multiple entries are read as a
|
|
# single whitespace-separated string that callers split themselves).
|
|
_deb822_get() {
|
|
local file="$1" field="$2"
|
|
[[ -f "$file" ]] || return 1
|
|
awk -v F="$field" '
|
|
BEGIN{ IGNORECASE=1 }
|
|
/^[[:space:]]*$/{ next }
|
|
/^[^[:space:]]/{
|
|
if (match($0, "^"F"[[:space:]]*:[[:space:]]*")) {
|
|
print substr($0, RSTART+RLENGTH)
|
|
exit
|
|
}
|
|
}
|
|
' "$file"
|
|
}
|
|
|
|
# Helper: back up a file once before modifying, so an accidental
|
|
# comment-out is always recoverable next to the original.
|
|
_backup_once() {
|
|
local file="$1"
|
|
[[ -f "$file" ]] || return 0
|
|
local ts backup
|
|
ts=$(date +%Y%m%d_%H%M%S)
|
|
backup="${file}.proxmenux-backup.${ts}"
|
|
[[ -f "$backup" ]] || cp -a "$file" "$backup"
|
|
}
|
|
|
|
# ── Phase 1 — comment intra-file duplicates in sources.list by URL+Suite ──
|
|
if [ -s "$sources_file" ]; then
|
|
local temp_file
|
|
temp_file=$(mktemp)
|
|
declare -A seen_repos
|
|
local file_changed=0
|
|
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
if [[ "$line" =~ ^[[:space:]]*# ]] || [[ -z "$line" ]]; then
|
|
echo "$line" >> "$temp_file"
|
|
continue
|
|
fi
|
|
if [[ "$line" =~ ^deb ]]; then
|
|
read -r _ url dist components <<< "$line"
|
|
local key="${url}_${dist}"
|
|
# Portable "is this associative-array key set?" — `[[ -v arr[key] ]]`
|
|
# is only reliable in bash 4.4+; `${var+set}` works everywhere.
|
|
if [[ -n "${seen_repos[$key]+set}" ]]; then
|
|
echo "# $line" >> "$temp_file"
|
|
cleaned_count=$((cleaned_count + 1))
|
|
file_changed=1
|
|
msg_info "$(translate "Commented duplicate: $url $dist")"
|
|
else
|
|
echo "$line" >> "$temp_file"
|
|
seen_repos[$key]="$components"
|
|
fi
|
|
else
|
|
echo "$line" >> "$temp_file"
|
|
fi
|
|
done < "$sources_file"
|
|
|
|
if [[ "$file_changed" -eq 1 ]]; then
|
|
_backup_once "$sources_file"
|
|
pmx_write_file "$sources_file" < "$temp_file"
|
|
rm -f "$temp_file"
|
|
chmod 644 "$sources_file"
|
|
else
|
|
rm -f "$temp_file"
|
|
fi
|
|
fi
|
|
|
|
# ── Phase 2 — comment lines duplicating what proxmox.sources already declares ──
|
|
# Comparison is EXACT on URL + Suite + at least one Component match, so a
|
|
# legitimate custom repo the user added under `download.proxmox.com` (e.g.
|
|
# /debian/pbs, /debian/ceph-squid, or the same URL pinned to a different
|
|
# suite) is preserved untouched.
|
|
if [ -f "/etc/apt/sources.list.d/proxmox.sources" ]; then
|
|
local pmx_uri pmx_suite pmx_comps
|
|
pmx_uri=$(_deb822_get /etc/apt/sources.list.d/proxmox.sources URIs)
|
|
pmx_suite=$(_deb822_get /etc/apt/sources.list.d/proxmox.sources Suites)
|
|
pmx_comps=$(_deb822_get /etc/apt/sources.list.d/proxmox.sources Components)
|
|
|
|
_match_and_comment() {
|
|
local target_file="$1" uri="$2" suite="$3" comps="$4"
|
|
[[ -f "$target_file" ]] || return 0
|
|
[[ -n "$uri" && -n "$suite" && -n "$comps" ]] || return 0
|
|
local base_uri="${uri#http://}"
|
|
base_uri="${base_uri#https://}"
|
|
base_uri="${base_uri%/}"
|
|
local first_comp
|
|
first_comp=$(awk '{print $1}' <<< "$comps")
|
|
local matched=0
|
|
while IFS= read -r ln; do
|
|
[[ "$ln" =~ ^[[:space:]]*# ]] && continue
|
|
[[ "$ln" =~ ^deb ]] || continue
|
|
read -r _ line_url line_suite line_comps <<< "$ln"
|
|
local ln_base="${line_url#http://}"
|
|
ln_base="${ln_base#https://}"
|
|
ln_base="${ln_base%/}"
|
|
[[ "$ln_base" == "$base_uri" ]] || continue
|
|
[[ "$line_suite" == "$suite" ]] || continue
|
|
[[ " $line_comps " == *" $first_comp "* ]] || continue
|
|
matched=1
|
|
break
|
|
done < "$target_file"
|
|
if [[ "$matched" -eq 1 ]]; then
|
|
_backup_once "$target_file"
|
|
# Anchored sed: reconstruct the exact deb prefix to avoid
|
|
# eating unrelated lines. Escape URL for regex safety.
|
|
local esc_uri esc_suite esc_comp
|
|
esc_uri=$(printf '%s' "$uri" | sed 's/[][\.^$*/]/\\&/g')
|
|
esc_suite=$(printf '%s' "$suite" | sed 's/[][\.^$*/]/\\&/g')
|
|
esc_comp=$(printf '%s' "$first_comp" | sed 's/[][\.^$*/]/\\&/g')
|
|
pmx_edit_file "$target_file" -E "/^deb[[:space:]]+${esc_uri}[[:space:]]+${esc_suite}[[:space:]]+.*(^| )${esc_comp}( |$)/s/^/# /"
|
|
cleaned_count=$((cleaned_count + 1))
|
|
fi
|
|
}
|
|
|
|
_match_and_comment "$sources_file" "$pmx_uri" "$pmx_suite" "$pmx_comps"
|
|
|
|
# Only walk a fixed allowlist of known-legacy PVE list files. Any
|
|
# other pve-*.list on disk is assumed to be user-authored (custom
|
|
# mirror, backports, staging) and left alone.
|
|
local legacy_pve_lists=(
|
|
/etc/apt/sources.list.d/pve-public-repo.list
|
|
/etc/apt/sources.list.d/pve-install-repo.list
|
|
/etc/apt/sources.list.d/pve-no-subscription.list
|
|
)
|
|
for legacy in "${legacy_pve_lists[@]}"; do
|
|
_match_and_comment "$legacy" "$pmx_uri" "$pmx_suite" "$pmx_comps"
|
|
done
|
|
|
|
# Same exact-match approach for debian.sources vs sources.list.
|
|
if [ -f "/etc/apt/sources.list.d/debian.sources" ]; then
|
|
local dbn_uri dbn_suite dbn_comps
|
|
dbn_uri=$(_deb822_get /etc/apt/sources.list.d/debian.sources URIs)
|
|
dbn_suite=$(_deb822_get /etc/apt/sources.list.d/debian.sources Suites)
|
|
dbn_comps=$(_deb822_get /etc/apt/sources.list.d/debian.sources Components)
|
|
# `Suites` in debian.sources holds multiple ("trixie trixie-updates"),
|
|
# walk each so both duplicates get commented if present.
|
|
for suite_iter in $dbn_suite; do
|
|
_match_and_comment "$sources_file" "$dbn_uri" "$suite_iter" "$dbn_comps"
|
|
done
|
|
fi
|
|
fi
|
|
|
|
# ── Phase 3 — remove ONLY the well-known legacy files, and only when the
|
|
# modern replacement already exists ──
|
|
if [ -f "/etc/apt/sources.list.d/proxmox.sources" ]; then
|
|
for old_file in /etc/apt/sources.list.d/pve-public-repo.list /etc/apt/sources.list.d/pve-install-repo.list; do
|
|
if [ -f "$old_file" ]; then
|
|
_backup_once "$old_file"
|
|
pmx_remove_file "$old_file"
|
|
cleaned_count=$((cleaned_count + 1))
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [ $cleaned_count -gt 0 ]; then
|
|
msg_ok "$(translate "Cleaned up $cleaned_count duplicate/old repositories")"
|
|
pmx_record_execution "Update package lists after repository cleanup" "apt-get update"
|
|
apt-get update > /dev/null 2>&1 || true
|
|
else
|
|
msg_ok "$(translate "No duplicate repositories found")"
|
|
fi
|
|
}
|
|
|
|
|
|
|
|
cleanup_duplicate_repos_pve9_() {
|
|
local FUNC_VERSION="1.0"
|
|
pmx_journal_context "cleanup_duplicate_repos_pve9_" "$FUNC_VERSION"
|
|
msg_info "$(translate "Cleaning up duplicate repositories...")"
|
|
|
|
local sources_file="/etc/apt/sources.list"
|
|
local temp_file=$(mktemp)
|
|
local cleaned_count=0
|
|
declare -A seen_repos
|
|
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
if [[ "$line" =~ ^[[:space:]]*# ]] || [[ -z "$line" ]]; then
|
|
echo "$line" >> "$temp_file"
|
|
continue
|
|
fi
|
|
|
|
if [[ "$line" =~ ^deb ]]; then
|
|
read -r _ url dist components <<< "$line"
|
|
local key="${url}_${dist}"
|
|
if [[ -n "${seen_repos[$key]+set}" ]]; then
|
|
echo "# $line" >> "$temp_file"
|
|
cleaned_count=$((cleaned_count + 1))
|
|
else
|
|
echo "$line" >> "$temp_file"
|
|
seen_repos[$key]="$components"
|
|
fi
|
|
else
|
|
echo "$line" >> "$temp_file"
|
|
fi
|
|
done < "$sources_file"
|
|
|
|
pmx_write_file "$sources_file" < "$temp_file"
|
|
rm -f "$temp_file"
|
|
chmod 644 "$sources_file"
|
|
|
|
for src in proxmox debian ceph; do
|
|
local sources_path="/etc/apt/sources.list.d/${src}.sources"
|
|
if [ -f "$sources_path" ]; then
|
|
case "$src" in
|
|
proxmox)
|
|
url_match="download.proxmox.com"
|
|
;;
|
|
debian)
|
|
url_match="deb.debian.org"
|
|
;;
|
|
ceph)
|
|
url_match="download.proxmox.com/ceph"
|
|
;;
|
|
*)
|
|
url_match=""
|
|
;;
|
|
esac
|
|
|
|
if [[ -n "$url_match" ]]; then
|
|
if grep -q "^deb.*$url_match" "$sources_file"; then
|
|
pmx_edit_file "$sources_file" "/^deb.*$url_match/s/^/# /"
|
|
cleaned_count=$((cleaned_count + 1))
|
|
fi
|
|
fi
|
|
|
|
for list_file in /etc/apt/sources.list.d/*.list; do
|
|
[[ -f "$list_file" ]] || continue
|
|
if grep -q "^deb.*$url_match" "$list_file"; then
|
|
pmx_edit_file "$list_file" "/^deb.*$url_match/s/^/# /"
|
|
cleaned_count=$((cleaned_count + 1))
|
|
fi
|
|
done
|
|
fi
|
|
done
|
|
|
|
if [ $cleaned_count -gt 0 ]; then
|
|
msg_ok "$(translate "Cleaned up $cleaned_count duplicate/old repositories")"
|
|
pmx_record_execution "Update package lists after repository cleanup" "apt-get update"
|
|
apt-get update > /dev/null 2>&1 || true
|
|
else
|
|
msg_ok "$(translate "No duplicate repositories found")"
|
|
fi
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cleanup_duplicate_repos_pve8() {
|
|
msg_info "$(translate "Cleaning up duplicate repositories...")"
|
|
|
|
local cleaned_count=0
|
|
local sources_file="/etc/apt/sources.list"
|
|
|
|
|
|
if [[ -f "$sources_file" ]]; then
|
|
local temp_file
|
|
temp_file=$(mktemp)
|
|
declare -A seen_repos
|
|
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
if [[ "$line" =~ ^[[:space:]]*# ]] || [[ -z "$line" ]]; then
|
|
echo "$line" >> "$temp_file"
|
|
continue
|
|
fi
|
|
|
|
if [[ "$line" =~ ^[[:space:]]*deb ]]; then
|
|
read -r _ url dist components <<< "$line"
|
|
local key="${url}_${dist}"
|
|
if [[ -n "${seen_repos[$key]+set}" ]]; then
|
|
echo "# $line" >> "$temp_file"
|
|
cleaned_count=$((cleaned_count + 1))
|
|
else
|
|
echo "$line" >> "$temp_file"
|
|
seen_repos[$key]="$components"
|
|
fi
|
|
else
|
|
echo "$line" >> "$temp_file"
|
|
fi
|
|
done < "$sources_file"
|
|
|
|
mv "$temp_file" "$sources_file"
|
|
chmod 644 "$sources_file"
|
|
fi
|
|
|
|
|
|
local old_pve_files=(/etc/apt/sources.list.d/pve-*.list /etc/apt/sources.list.d/proxmox.list)
|
|
|
|
for file in "${old_pve_files[@]}"; do
|
|
if [[ -f "$file" ]]; then
|
|
local base_name
|
|
base_name=$(basename "$file" .list)
|
|
local sources_equiv="/etc/apt/sources.list.d/${base_name}.sources"
|
|
|
|
if [[ -f "$sources_equiv" ]] && grep -q "^Enabled: *true" "$sources_equiv"; then
|
|
msg_info "$(translate "Removing old repository file: $(basename "$file")")"
|
|
rm -f "$file"
|
|
cleaned_count=$((cleaned_count + 1))
|
|
fi
|
|
fi
|
|
done
|
|
|
|
|
|
if [ "$cleaned_count" -gt 0 ]; then
|
|
msg_ok "$(translate "Cleaned up $cleaned_count duplicate/old repositories")"
|
|
apt-get update > /dev/null 2>&1 || true
|
|
else
|
|
msg_ok "$(translate "No duplicate repositories found")"
|
|
fi
|
|
}
|
|
|
|
|
|
|
|
cleanup_duplicate_repos() {
|
|
local pve_version
|
|
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
|
|
|
|
if [[ -z "$pve_version" ]]; then
|
|
msg_error "Unable to detect Proxmox version."
|
|
return 1
|
|
fi
|
|
|
|
if [[ "$pve_version" -ge 9 ]]; then
|
|
cleanup_duplicate_repos_pve9
|
|
else
|
|
cleanup_duplicate_repos_pve8
|
|
fi
|
|
}
|