mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 18:56:52 +00:00
Add audit and reports page, and a change journal
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>
This commit is contained in:
@@ -12,6 +12,9 @@ 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
|
||||
@@ -84,6 +87,8 @@ lvm_repair_check() {
|
||||
|
||||
|
||||
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"
|
||||
@@ -152,7 +157,8 @@ cleanup_duplicate_repos_pve9() {
|
||||
|
||||
if [[ "$file_changed" -eq 1 ]]; then
|
||||
_backup_once "$sources_file"
|
||||
mv "$temp_file" "$sources_file"
|
||||
pmx_write_file "$sources_file" < "$temp_file"
|
||||
rm -f "$temp_file"
|
||||
chmod 644 "$sources_file"
|
||||
else
|
||||
rm -f "$temp_file"
|
||||
@@ -201,7 +207,7 @@ cleanup_duplicate_repos_pve9() {
|
||||
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')
|
||||
sed -i -E "/^deb[[:space:]]+${esc_uri}[[:space:]]+${esc_suite}[[:space:]]+.*(^| )${esc_comp}( |$)/s/^/# /" "$target_file"
|
||||
pmx_edit_file "$target_file" -E "/^deb[[:space:]]+${esc_uri}[[:space:]]+${esc_suite}[[:space:]]+.*(^| )${esc_comp}( |$)/s/^/# /"
|
||||
cleaned_count=$((cleaned_count + 1))
|
||||
fi
|
||||
}
|
||||
@@ -240,7 +246,7 @@ cleanup_duplicate_repos_pve9() {
|
||||
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"
|
||||
rm -f "$old_file"
|
||||
pmx_remove_file "$old_file"
|
||||
cleaned_count=$((cleaned_count + 1))
|
||||
fi
|
||||
done
|
||||
@@ -248,6 +254,7 @@ cleanup_duplicate_repos_pve9() {
|
||||
|
||||
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")"
|
||||
@@ -257,6 +264,8 @@ cleanup_duplicate_repos_pve9() {
|
||||
|
||||
|
||||
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"
|
||||
@@ -285,7 +294,8 @@ cleanup_duplicate_repos_pve9_() {
|
||||
fi
|
||||
done < "$sources_file"
|
||||
|
||||
mv "$temp_file" "$sources_file"
|
||||
pmx_write_file "$sources_file" < "$temp_file"
|
||||
rm -f "$temp_file"
|
||||
chmod 644 "$sources_file"
|
||||
|
||||
for src in proxmox debian ceph; do
|
||||
@@ -308,7 +318,7 @@ cleanup_duplicate_repos_pve9_() {
|
||||
|
||||
if [[ -n "$url_match" ]]; then
|
||||
if grep -q "^deb.*$url_match" "$sources_file"; then
|
||||
sed -i "/^deb.*$url_match/s/^/# /" "$sources_file"
|
||||
pmx_edit_file "$sources_file" "/^deb.*$url_match/s/^/# /"
|
||||
cleaned_count=$((cleaned_count + 1))
|
||||
fi
|
||||
fi
|
||||
@@ -316,7 +326,7 @@ cleanup_duplicate_repos_pve9_() {
|
||||
for list_file in /etc/apt/sources.list.d/*.list; do
|
||||
[[ -f "$list_file" ]] || continue
|
||||
if grep -q "^deb.*$url_match" "$list_file"; then
|
||||
sed -i "/^deb.*$url_match/s/^/# /" "$list_file"
|
||||
pmx_edit_file "$list_file" "/^deb.*$url_match/s/^/# /"
|
||||
cleaned_count=$((cleaned_count + 1))
|
||||
fi
|
||||
done
|
||||
@@ -325,6 +335,7 @@ cleanup_duplicate_repos_pve9_() {
|
||||
|
||||
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")"
|
||||
|
||||
@@ -5,6 +5,10 @@ if [[ -n "${__PROXMENUX_PCI_PASSTHROUGH_HELPERS__:-}" ]]; then
|
||||
fi
|
||||
__PROXMENUX_PCI_PASSTHROUGH_HELPERS__=1
|
||||
|
||||
if [[ -f /usr/local/share/proxmenux/scripts/global/pmx_journal.sh ]]; then
|
||||
source /usr/local/share/proxmenux/scripts/global/pmx_journal.sh
|
||||
fi
|
||||
|
||||
function _pci_is_iommu_active() {
|
||||
grep -qE 'intel_iommu=on|amd_iommu=on' /proc/cmdline 2>/dev/null || return 1
|
||||
[[ -d /sys/kernel/iommu_groups ]] || return 1
|
||||
@@ -497,6 +501,8 @@ _proxmenux_vfio_bind_add_bdfs() {
|
||||
}
|
||||
|
||||
_proxmenux_vfio_bind_remove_bdfs() {
|
||||
local FUNC_VERSION="1.0"
|
||||
pmx_journal_context "_proxmenux_vfio_bind_remove_bdfs" "$FUNC_VERSION"
|
||||
# Args: any number of BDFs to remove from the binder list
|
||||
[[ -f "$PROXMENUX_VFIO_BIND_STATE" ]] || return 0
|
||||
_proxmenux_vfio_bind_cleanup_legacy
|
||||
@@ -511,13 +517,14 @@ _proxmenux_vfio_bind_remove_bdfs() {
|
||||
else
|
||||
normalized="0000:${bdf}"
|
||||
fi
|
||||
sed -i "\|^${normalized}\$|d" "$tmp"
|
||||
sed "\|^${normalized}\$|d" "$tmp" > "${tmp}.next" && mv "${tmp}.next" "$tmp"
|
||||
done
|
||||
if ! cmp -s "$tmp" "$PROXMENUX_VFIO_BIND_STATE"; then
|
||||
mv "$tmp" "$PROXMENUX_VFIO_BIND_STATE"
|
||||
pmx_write_file "$PROXMENUX_VFIO_BIND_STATE" < "$tmp"
|
||||
rm -f "$tmp"
|
||||
_proxmenux_vfio_bind_write_udev_rule
|
||||
# If empty, remove state file too (keeps host clean)
|
||||
[[ ! -s "$PROXMENUX_VFIO_BIND_STATE" ]] && rm -f "$PROXMENUX_VFIO_BIND_STATE"
|
||||
[[ ! -s "$PROXMENUX_VFIO_BIND_STATE" ]] && pmx_remove_file "$PROXMENUX_VFIO_BIND_STATE"
|
||||
_proxmenux_nvidia_vfio_policy_sync || true
|
||||
_proxmenux_mark_host_config_changed
|
||||
else
|
||||
@@ -598,9 +605,11 @@ EOF
|
||||
}
|
||||
|
||||
_proxmenux_nvidia_vfio_softdeps_sync() {
|
||||
local FUNC_VERSION="1.0"
|
||||
pmx_journal_context "_proxmenux_nvidia_vfio_softdeps_sync" "$FUNC_VERSION"
|
||||
local changed=1
|
||||
mkdir -p "$(dirname "$PROXMENUX_VFIO_CONF")"
|
||||
touch "$PROXMENUX_VFIO_CONF"
|
||||
[[ -f "$PROXMENUX_VFIO_CONF" ]] || pmx_write_file "$PROXMENUX_VFIO_CONF" < /dev/null
|
||||
|
||||
local -a softdeps=(
|
||||
"softdep nvidia pre: vfio-pci"
|
||||
@@ -612,14 +621,14 @@ _proxmenux_nvidia_vfio_softdeps_sync() {
|
||||
if _proxmenux_vfio_bind_state_has_vendor "10de"; then
|
||||
for line in "${softdeps[@]}"; do
|
||||
if ! grep -qFx "$line" "$PROXMENUX_VFIO_CONF" 2>/dev/null; then
|
||||
echo "$line" >> "$PROXMENUX_VFIO_CONF"
|
||||
echo "$line" | pmx_append_file "$PROXMENUX_VFIO_CONF"
|
||||
changed=0
|
||||
fi
|
||||
done
|
||||
else
|
||||
for line in "${softdeps[@]}"; do
|
||||
if grep -qFx "$line" "$PROXMENUX_VFIO_CONF" 2>/dev/null; then
|
||||
sed -i "\|^${line}$|d" "$PROXMENUX_VFIO_CONF"
|
||||
pmx_edit_file "$PROXMENUX_VFIO_CONF" "\|^${line}$|d"
|
||||
changed=0
|
||||
fi
|
||||
done
|
||||
@@ -779,6 +788,8 @@ _proxmenux_vfio_bind_migrate_legacy_nvidia_ids() {
|
||||
# passed through.
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
_proxmenux_nvidia_migrate_legacy_blacklist() {
|
||||
local FUNC_VERSION="1.0"
|
||||
pmx_journal_context "_proxmenux_nvidia_migrate_legacy_blacklist" "$FUNC_VERSION"
|
||||
local changed=false
|
||||
local blacklist_file="${PROXMENUX_ETC_ROOT}/modprobe.d/blacklist.conf"
|
||||
local nvidia_blacklist="${PROXMENUX_ETC_ROOT}/modprobe.d/nvidia-blacklist.conf"
|
||||
@@ -788,29 +799,37 @@ _proxmenux_nvidia_migrate_legacy_blacklist() {
|
||||
local modules_load_active="${PROXMENUX_ETC_ROOT}/modules-load.d/nvidia-vfio.conf"
|
||||
|
||||
if [[ -f "$blacklist_file" ]] && grep -qE '^blacklist (nvidia|nvidia_drm|nvidia_modeset|nvidia_uvm|nvidiafb)$' "$blacklist_file"; then
|
||||
sed -i \
|
||||
pmx_edit_file "$blacklist_file" \
|
||||
-e '/^blacklist nvidia$/d' \
|
||||
-e '/^blacklist nvidia_drm$/d' \
|
||||
-e '/^blacklist nvidia_modeset$/d' \
|
||||
-e '/^blacklist nvidia_uvm$/d' \
|
||||
-e '/^blacklist nvidiafb$/d' \
|
||||
"$blacklist_file"
|
||||
-e '/^blacklist nvidiafb$/d'
|
||||
changed=true
|
||||
fi
|
||||
|
||||
if [[ -f "$nvidia_blacklist" ]]; then
|
||||
rm -f "$nvidia_blacklist"
|
||||
pmx_remove_file "$nvidia_blacklist"
|
||||
changed=true
|
||||
fi
|
||||
|
||||
if [[ -f "$udev_disabled" ]]; then
|
||||
mv "$udev_disabled" "$udev_rules" >/dev/null 2>&1 || true
|
||||
if pmx_write_file "$udev_rules" < "$udev_disabled"; then
|
||||
chmod --reference="$udev_disabled" "$udev_rules" 2>/dev/null || true
|
||||
chown --reference="$udev_disabled" "$udev_rules" 2>/dev/null || true
|
||||
pmx_remove_file "$udev_disabled" || true
|
||||
fi
|
||||
pmx_record_execution "Reload udev rules" "udevadm control --reload-rules"
|
||||
udevadm control --reload-rules >/dev/null 2>&1 || true
|
||||
changed=true
|
||||
fi
|
||||
|
||||
if [[ -f "$modules_load_disabled" ]]; then
|
||||
mv "$modules_load_disabled" "$modules_load_active" >/dev/null 2>&1 || true
|
||||
if pmx_write_file "$modules_load_active" < "$modules_load_disabled"; then
|
||||
chmod --reference="$modules_load_disabled" "$modules_load_active" 2>/dev/null || true
|
||||
chown --reference="$modules_load_disabled" "$modules_load_active" 2>/dev/null || true
|
||||
pmx_remove_file "$modules_load_disabled" || true
|
||||
fi
|
||||
changed=true
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
#!/usr/bin/env bash
|
||||
# ProxMenux change journal — recording side.
|
||||
#
|
||||
# What a sysadmin holds against a tool like this one is not that it
|
||||
# changes things: it is that afterwards nobody can say what it changed.
|
||||
# Reading the script does not answer it either — a function of four
|
||||
# hundred lines may alter two values, and the reader has no way to know
|
||||
# which two.
|
||||
#
|
||||
# So the rule here is that a change is recorded because it could not be
|
||||
# made any other way. These helpers are the writing path: they capture
|
||||
# what was there, make the change, and record both. A function that uses
|
||||
# them is auditable without its author having remembered anything, and a
|
||||
# function that writes directly is a bug we can find by grepping.
|
||||
#
|
||||
# Nothing here needs sqlite, python or network access. Each entry is one
|
||||
# small JSON file written whole into a spool directory, which the Monitor
|
||||
# reads and consolidates. One file per entry means no two concurrent
|
||||
# scripts can interleave a line, and an interrupted write leaves a file
|
||||
# the reader skips rather than a corrupted log.
|
||||
#
|
||||
# Usage:
|
||||
# source /usr/local/share/proxmenux/scripts/pmx_journal.sh
|
||||
# pmx_journal_context "optimize_logrotate" "1.1"
|
||||
# pmx_write_file /etc/logrotate.conf <<EOF
|
||||
# ...
|
||||
# EOF
|
||||
# pmx_enable_service log2ram
|
||||
#
|
||||
# Everything degrades quietly: if the journal cannot be written, the
|
||||
# change still happens. Recording must never be the reason an operation
|
||||
# fails on somebody's host.
|
||||
|
||||
PMX_JOURNAL_ROOT="${PMX_JOURNAL_ROOT:-/usr/local/share/proxmenux/changes}"
|
||||
PMX_JOURNAL_SPOOL="$PMX_JOURNAL_ROOT/spool"
|
||||
PMX_JOURNAL_OBJECTS="$PMX_JOURNAL_ROOT/objects"
|
||||
|
||||
# Set by pmx_journal_context; every entry carries them.
|
||||
PMX_JOURNAL_FUNCTION="${PMX_JOURNAL_FUNCTION:-}"
|
||||
PMX_JOURNAL_VERSION="${PMX_JOURNAL_VERSION:-}"
|
||||
PMX_JOURNAL_SOURCE="${PMX_JOURNAL_SOURCE:-${SCRIPT_SOURCE:-}}"
|
||||
|
||||
# Which function is making the changes that follow. Called once at the
|
||||
# top of a function, so the entries it produces are attributable to it
|
||||
# rather than to whichever script happened to source this file.
|
||||
pmx_journal_context() {
|
||||
PMX_JOURNAL_FUNCTION="${1:-unknown}"
|
||||
PMX_JOURNAL_VERSION="${2:-}"
|
||||
PMX_JOURNAL_SOURCE="${3:-${SCRIPT_SOURCE:-$(basename "${BASH_SOURCE[-1]:-unknown}")}}"
|
||||
}
|
||||
|
||||
_pmx_journal_ready() {
|
||||
mkdir -p "$PMX_JOURNAL_SPOOL" "$PMX_JOURNAL_OBJECTS" 2>/dev/null || return 1
|
||||
chmod 700 "$PMX_JOURNAL_ROOT" 2>/dev/null || true
|
||||
return 0
|
||||
}
|
||||
|
||||
# JSON string escaping in pure bash: no jq dependency on the recording
|
||||
# side, because the recording side runs before anything is installed.
|
||||
_pmx_json_escape() {
|
||||
local text="$1"
|
||||
text="${text//\\/\\\\}"
|
||||
text="${text//\"/\\\"}"
|
||||
text="${text//$'\n'/\\n}"
|
||||
text="${text//$'\r'/\\r}"
|
||||
text="${text//$'\t'/\\t}"
|
||||
printf '%s' "$text"
|
||||
}
|
||||
|
||||
# The largest file whose contents are worth keeping. Configuration is
|
||||
# measured in kilobytes; a binary is measured in megabytes and shows no
|
||||
# useful difference, so past this the journal records that the file was
|
||||
# there and what it hashed to, and stops short of copying it. A host that
|
||||
# fills its disk with captured binaries is a worse outcome than a change
|
||||
# whose contents cannot be shown.
|
||||
PMX_JOURNAL_MAX_OBJECT="${PMX_JOURNAL_MAX_OBJECT:-1048576}"
|
||||
|
||||
# Set by _pmx_store_object. Reported through globals rather than printed
|
||||
# because a command substitution runs in a subshell: anything the helper
|
||||
# set there would be lost on the way back, and the caller would record
|
||||
# every capture as unrecoverable.
|
||||
PMX_LAST_DIGEST=""
|
||||
PMX_LAST_OBJECT_STORED=false
|
||||
|
||||
# Stores a file's contents and returns its digest, so an entry references
|
||||
# the bytes rather than embedding them. Content is kept once however many
|
||||
# times it is captured.
|
||||
_pmx_store_object() {
|
||||
local path="$1"
|
||||
PMX_LAST_DIGEST=""
|
||||
PMX_LAST_OBJECT_STORED=false
|
||||
[ -f "$path" ] || return 1
|
||||
local digest
|
||||
digest="$(sha256sum "$path" 2>/dev/null | cut -d' ' -f1)" || return 1
|
||||
[ -n "$digest" ] || return 1
|
||||
PMX_LAST_DIGEST="$digest"
|
||||
|
||||
local size
|
||||
size="$(stat -c %s "$path" 2>/dev/null || echo 0)"
|
||||
if [ "$size" -gt "$PMX_JOURNAL_MAX_OBJECT" ] 2>/dev/null; then
|
||||
# The digest still identifies what was there; the bytes are not
|
||||
# kept, and the entry will say the change cannot be undone from
|
||||
# the journal alone.
|
||||
return 0
|
||||
fi
|
||||
|
||||
local target="$PMX_JOURNAL_OBJECTS/${digest:0:2}/$digest"
|
||||
if [ ! -f "$target" ]; then
|
||||
mkdir -p "$(dirname "$target")" 2>/dev/null || return 1
|
||||
cp "$path" "$target.tmp.$$" 2>/dev/null || return 1
|
||||
chmod 600 "$target.tmp.$$" 2>/dev/null || true
|
||||
mv "$target.tmp.$$" "$target" 2>/dev/null || return 1
|
||||
fi
|
||||
PMX_LAST_OBJECT_STORED=true
|
||||
}
|
||||
|
||||
# Writes one entry. Callers pass key=value pairs; values are escaped
|
||||
# here so no caller has to think about JSON.
|
||||
_pmx_journal_record() {
|
||||
_pmx_journal_ready || return 0
|
||||
local entry="" key value first=1
|
||||
for pair in "$@"; do
|
||||
key="${pair%%=*}"
|
||||
value="${pair#*=}"
|
||||
[ "$first" = 1 ] && first=0 || entry+=","
|
||||
# A key ending in _raw carries a number or a literal such as
|
||||
# true/false/null and is written unquoted.
|
||||
if [ "${key%_raw}" != "$key" ]; then
|
||||
entry+="\"${key%_raw}\":${value}"
|
||||
else
|
||||
entry+="\"$key\":\"$(_pmx_json_escape "$value")\""
|
||||
fi
|
||||
done
|
||||
local file
|
||||
file="$PMX_JOURNAL_SPOOL/$(date +%s)-$$-${RANDOM}.json"
|
||||
printf '{%s}\n' "$entry" > "$file.tmp" 2>/dev/null || return 0
|
||||
chmod 600 "$file.tmp" 2>/dev/null || true
|
||||
mv "$file.tmp" "$file" 2>/dev/null || true
|
||||
return 0
|
||||
}
|
||||
|
||||
_pmx_journal_common() {
|
||||
printf '%s\n' \
|
||||
"recorded_at_raw=$(date +%s)" \
|
||||
"function=${PMX_JOURNAL_FUNCTION:-unknown}" \
|
||||
"function_version=${PMX_JOURNAL_VERSION:-}" \
|
||||
"source=${PMX_JOURNAL_SOURCE:-unknown}"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Configuration: files this host had, and what they became
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
# Replaces a file with what arrives on stdin, capturing what was there.
|
||||
#
|
||||
# pmx_write_file /etc/logrotate.conf <<EOF
|
||||
# ...
|
||||
# EOF
|
||||
pmx_write_file() {
|
||||
local path="$1"
|
||||
local temp before after existed="false"
|
||||
temp="$(mktemp)" || { cat > "$path"; return $?; }
|
||||
cat > "$temp"
|
||||
|
||||
local kept="true"
|
||||
if [ -f "$path" ]; then
|
||||
existed="true"
|
||||
_pmx_store_object "$path"
|
||||
before="$PMX_LAST_DIGEST"; kept="$PMX_LAST_OBJECT_STORED"
|
||||
fi
|
||||
# The change itself. Permissions of an existing file are preserved by
|
||||
# writing through it rather than replacing the inode.
|
||||
if ! cat "$temp" > "$path" 2>/dev/null; then
|
||||
rm -f "$temp"
|
||||
return 1
|
||||
fi
|
||||
_pmx_store_object "$path"; after="$PMX_LAST_DIGEST"
|
||||
rm -f "$temp"
|
||||
# Writing the same bytes back is not a change. Recording it would
|
||||
# fill the journal with entries a reader has to open to discover
|
||||
# nothing happened — which is exactly what re-running an idempotent
|
||||
# post-install does.
|
||||
[ "$before" = "$after" ] && return 0
|
||||
|
||||
local -a fields
|
||||
mapfile -t fields < <(_pmx_journal_common)
|
||||
_pmx_journal_record "${fields[@]}" \
|
||||
"class=configuration" "operation=write_file" "target=$path" \
|
||||
"before=${before:-}" "after=${after:-}" \
|
||||
"existed_raw=$existed" \
|
||||
"capture=$([ "$existed" = true ] && echo present || echo created)" \
|
||||
"revert=$([ "$existed" = true ] && echo restore || echo remove)" \
|
||||
"exactness=$([ "$existed" != true ] || [ "$kept" = true ] && echo exact || echo none)"
|
||||
}
|
||||
|
||||
# Applies a sed expression in place, capturing the file first.
|
||||
#
|
||||
# pmx_edit_file /etc/default/grub 's/^X=.*/X=1/'
|
||||
pmx_edit_file() {
|
||||
local path="$1"; shift
|
||||
[ -f "$path" ] || return 1
|
||||
local before after kept
|
||||
_pmx_store_object "$path"
|
||||
before="$PMX_LAST_DIGEST"; kept="$PMX_LAST_OBJECT_STORED"
|
||||
sed -i "$@" "$path" || return 1
|
||||
_pmx_store_object "$path"; after="$PMX_LAST_DIGEST"
|
||||
# An expression that matched nothing is not a change, and recording
|
||||
# it would fill the journal with entries a reader has to dismiss.
|
||||
[ "$before" = "$after" ] && return 0
|
||||
|
||||
local -a fields
|
||||
mapfile -t fields < <(_pmx_journal_common)
|
||||
_pmx_journal_record "${fields[@]}" \
|
||||
"class=configuration" "operation=edit_file" "target=$path" \
|
||||
"before=${before:-}" "after=${after:-}" \
|
||||
"expression=$*" "capture=present" "revert=restore" \
|
||||
"exactness=$([ "$kept" = true ] && echo exact || echo none)"
|
||||
}
|
||||
|
||||
# Removes a file, keeping its contents so the removal can be undone.
|
||||
pmx_remove_file() {
|
||||
local path="$1"
|
||||
[ -e "$path" ] || return 0
|
||||
local before kept
|
||||
_pmx_store_object "$path"
|
||||
before="$PMX_LAST_DIGEST"; kept="$PMX_LAST_OBJECT_STORED"
|
||||
rm -f "$path" || return 1
|
||||
|
||||
local -a fields
|
||||
mapfile -t fields < <(_pmx_journal_common)
|
||||
_pmx_journal_record "${fields[@]}" \
|
||||
"class=configuration" "operation=remove_file" "target=$path" \
|
||||
"before=${before:-}" "after=" "capture=present" \
|
||||
"revert=restore" \
|
||||
"exactness=$([ "$kept" = true ] && echo exact || echo none)"
|
||||
}
|
||||
|
||||
# Adds to a file, keeping what was there.
|
||||
#
|
||||
# Appending looks like it needs no capture — the previous content is
|
||||
# still in the file — but the journal shows a change as the difference
|
||||
# between two states, and a reader asking what a function did to a file
|
||||
# should not have to reconstruct the first state by subtracting.
|
||||
#
|
||||
# printf 'ulimit -n 1048576\n' | pmx_append_file /root/.profile
|
||||
pmx_append_file() {
|
||||
local path="$1"
|
||||
local temp before after existed="false"
|
||||
temp="$(mktemp)" || { cat >> "$path"; return $?; }
|
||||
cat > "$temp"
|
||||
|
||||
local kept="true"
|
||||
if [ -f "$path" ]; then
|
||||
existed="true"
|
||||
_pmx_store_object "$path"
|
||||
before="$PMX_LAST_DIGEST"; kept="$PMX_LAST_OBJECT_STORED"
|
||||
fi
|
||||
if ! cat "$temp" >> "$path" 2>/dev/null; then
|
||||
rm -f "$temp"
|
||||
return 1
|
||||
fi
|
||||
_pmx_store_object "$path"; after="$PMX_LAST_DIGEST"
|
||||
rm -f "$temp"
|
||||
[ "$before" = "$after" ] && return 0
|
||||
|
||||
local -a fields
|
||||
mapfile -t fields < <(_pmx_journal_common)
|
||||
_pmx_journal_record "${fields[@]}" \
|
||||
"class=configuration" "operation=append_file" "target=$path" \
|
||||
"before=${before:-}" "after=${after:-}" \
|
||||
"capture=$([ "$existed" = true ] && echo present || echo created)" \
|
||||
"revert=$([ "$existed" = true ] && echo restore || echo remove)" \
|
||||
"exactness=$([ "$existed" != true ] || [ "$kept" = true ] && echo exact || echo none)"
|
||||
}
|
||||
|
||||
# Applies a setting through the command that owns it, capturing the
|
||||
# state that command reports before and after.
|
||||
#
|
||||
# Some settings have no file to write: the timezone, whether the clock is
|
||||
# disciplined, a bootloader entry. The tool that owns them is the only
|
||||
# thing that can read them back, so it is asked twice — before and after
|
||||
# — and the journal records the two answers.
|
||||
#
|
||||
# pmx_apply_setting "timezone" "timedatectl show -p Timezone --value" \
|
||||
# timedatectl set-timezone "$timezone"
|
||||
pmx_apply_setting() {
|
||||
local name="$1" reader="$2"; shift 2
|
||||
local before after
|
||||
before="$(eval "$reader" 2>/dev/null | head -c 400)"
|
||||
"$@" >/dev/null 2>&1
|
||||
local status=$?
|
||||
after="$(eval "$reader" 2>/dev/null | head -c 400)"
|
||||
# A setting already at the wanted value is not a change.
|
||||
[ "$before" = "$after" ] && return $status
|
||||
|
||||
local -a fields
|
||||
mapfile -t fields < <(_pmx_journal_common)
|
||||
_pmx_journal_record "${fields[@]}" \
|
||||
"class=configuration" "operation=apply_setting" "target=$name" \
|
||||
"before_state=$before" "after_state=$after" "command=$*" \
|
||||
"capture=present" "revert=reapply" \
|
||||
"result=$([ $status -eq 0 ] && echo ok || echo failed)" \
|
||||
"exactness=exact"
|
||||
return $status
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Installation: what was not on this host and now is
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
# Installs packages, recording which ones actually arrived.
|
||||
#
|
||||
# What is recorded is the difference the operation made, not what was
|
||||
# asked for: a package already present is not a change, and the
|
||||
# dependencies apt pulled in are, even though nobody named them.
|
||||
pmx_install_pkg() {
|
||||
local -a requested=("$@")
|
||||
[ ${#requested[@]} -gt 0 ] || return 0
|
||||
|
||||
local before_list after_list added
|
||||
before_list="$(dpkg-query -W -f='${binary:Package}\n' 2>/dev/null | sort -u)"
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y "${requested[@]}" >/dev/null 2>&1
|
||||
local status=$?
|
||||
after_list="$(dpkg-query -W -f='${binary:Package}\n' 2>/dev/null | sort -u)"
|
||||
added="$(comm -13 <(printf '%s\n' "$before_list") <(printf '%s\n' "$after_list") | tr '\n' ' ')"
|
||||
|
||||
local -a fields
|
||||
mapfile -t fields < <(_pmx_journal_common)
|
||||
_pmx_journal_record "${fields[@]}" \
|
||||
"class=installation" "operation=install_package" \
|
||||
"target=${requested[*]}" "installed=${added% }" \
|
||||
"result=$([ $status -eq 0 ] && echo ok || echo failed)" \
|
||||
"capture=present" "revert=purge" \
|
||||
"exactness=$([ -n "${added// /}" ] && echo partial || echo none)"
|
||||
return $status
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Services: what was running, and what runs now
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
_pmx_service_state() {
|
||||
local unit="$1"
|
||||
printf '%s/%s' \
|
||||
"$(systemctl is-enabled "$unit" 2>/dev/null || echo unknown)" \
|
||||
"$(systemctl is-active "$unit" 2>/dev/null || echo unknown)"
|
||||
}
|
||||
|
||||
pmx_enable_service() {
|
||||
local unit="$1"
|
||||
local before after
|
||||
before="$(_pmx_service_state "$unit")"
|
||||
systemctl enable --now "$unit" >/dev/null 2>&1
|
||||
local status=$?
|
||||
after="$(_pmx_service_state "$unit")"
|
||||
[ "$before" = "$after" ] && return $status
|
||||
|
||||
local -a fields
|
||||
mapfile -t fields < <(_pmx_journal_common)
|
||||
_pmx_journal_record "${fields[@]}" \
|
||||
"class=configuration" "operation=enable_service" "target=$unit" \
|
||||
"before_state=$before" "after_state=$after" \
|
||||
"capture=present" "revert=disable" "exactness=exact"
|
||||
return $status
|
||||
}
|
||||
|
||||
pmx_disable_service() {
|
||||
local unit="$1"
|
||||
local before after
|
||||
before="$(_pmx_service_state "$unit")"
|
||||
systemctl disable --now "$unit" >/dev/null 2>&1
|
||||
local status=$?
|
||||
after="$(_pmx_service_state "$unit")"
|
||||
[ "$before" = "$after" ] && return $status
|
||||
|
||||
local -a fields
|
||||
mapfile -t fields < <(_pmx_journal_common)
|
||||
_pmx_journal_record "${fields[@]}" \
|
||||
"class=configuration" "operation=disable_service" "target=$unit" \
|
||||
"before_state=$before" "after_state=$after" \
|
||||
"capture=present" "revert=enable" "exactness=exact"
|
||||
return $status
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Execution: what ProxMenux ran on the user's behalf
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
# For work ProxMenux launches but does not decide: a system upgrade, a
|
||||
# rebuild. Recording it as a change of ours would claim authorship of
|
||||
# whatever apt decided; recording nothing would leave a host that changed
|
||||
# under the reader's feet with no trace of why.
|
||||
pmx_record_execution() {
|
||||
local description="$1"; shift
|
||||
local command="$*"
|
||||
local -a fields
|
||||
mapfile -t fields < <(_pmx_journal_common)
|
||||
_pmx_journal_record "${fields[@]}" \
|
||||
"class=execution" "operation=run_command" \
|
||||
"target=$description" "command=$command" \
|
||||
"capture=none" "revert=none" "exactness=none"
|
||||
}
|
||||
|
||||
# Records that a function was applied without being able to say what it
|
||||
# changed — the state before it ran is not knowable. Used by the
|
||||
# registration path so a host carries an honest account of what was
|
||||
# applied before the journal existed.
|
||||
pmx_record_applied() {
|
||||
local tool="$1" version="$2" state="${3:-applied}"
|
||||
local -a fields
|
||||
mapfile -t fields < <(_pmx_journal_common)
|
||||
_pmx_journal_record "${fields[@]}" \
|
||||
"class=registration" "operation=$state" "target=$tool" \
|
||||
"function_version=$version" "capture=unknown" \
|
||||
"revert=none" "exactness=none"
|
||||
}
|
||||
@@ -17,6 +17,9 @@ TOOLS_JSON="/usr/local/share/proxmenux/installed_tools.json"
|
||||
if [[ -f "$UTILS_FILE" ]]; then
|
||||
source "$UTILS_FILE"
|
||||
fi
|
||||
if [[ -f /usr/local/share/proxmenux/scripts/global/pmx_journal.sh ]]; then
|
||||
source /usr/local/share/proxmenux/scripts/global/pmx_journal.sh
|
||||
fi
|
||||
|
||||
load_language
|
||||
initialize_cache
|
||||
@@ -77,7 +80,9 @@ create_backup() {
|
||||
|
||||
# Create the patch script that will be called by APT hook
|
||||
create_patch_script() {
|
||||
cat > "$PATCH_BIN" <<'EOFPATCH'
|
||||
local FUNC_VERSION="1.0"
|
||||
pmx_journal_context "create_patch_script" "$FUNC_VERSION"
|
||||
pmx_write_file "$PATCH_BIN" <<'EOFPATCH'
|
||||
#!/usr/bin/env bash
|
||||
# ==========================================================
|
||||
# Proxmox Subscription Banner Patch (v3 - Minimal)
|
||||
|
||||
@@ -10,6 +10,9 @@ 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
|
||||
@@ -27,6 +30,8 @@ register_tool() {
|
||||
}
|
||||
|
||||
remove_subscription_banner_pve8() {
|
||||
local FUNC_VERSION="1.0"
|
||||
pmx_journal_context "remove_subscription_banner_pve8" "$FUNC_VERSION"
|
||||
local JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
|
||||
local GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
|
||||
local APT_HOOK="/etc/apt/apt.conf.d/no-nag-script"
|
||||
@@ -50,17 +55,20 @@ remove_subscription_banner_pve8() {
|
||||
cp "$JS_FILE" "$BACKUP_FILE"
|
||||
|
||||
|
||||
sed -i "s/No valid subscription/Subscription active/g" "$JS_FILE"
|
||||
sed -i "s/Ext.Msg.WARNING/Ext.Msg.INFO/g" "$JS_FILE"
|
||||
sed -i "s/res.data.status.toLowerCase() !== 'active'/false/g" "$JS_FILE"
|
||||
sed -i "s/subscriptionActive: ''/subscriptionActive: true/g" "$JS_FILE"
|
||||
pmx_edit_file "$JS_FILE" \
|
||||
-e "s/No valid subscription/Subscription active/g" \
|
||||
-e "s/Ext.Msg.WARNING/Ext.Msg.INFO/g" \
|
||||
-e "s/res.data.status.toLowerCase() !== 'active'/false/g" \
|
||||
-e "s/subscriptionActive: ''/subscriptionActive: true/g"
|
||||
|
||||
[[ -f "$GZ_FILE" ]] && rm -f "$GZ_FILE"
|
||||
[[ -f "$GZ_FILE" ]] && pmx_remove_file "$GZ_FILE"
|
||||
|
||||
pmx_record_execution "Clear cached Proxmox JavaScript files" "find /var/cache/pve-manager/ -name *.js* -delete"
|
||||
find /var/cache/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
|
||||
pmx_record_execution "Clear generated Proxmox JavaScript files" "find /var/lib/pve-manager/ -name *.js* -delete"
|
||||
find /var/lib/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
|
||||
|
||||
[[ -f "$APT_HOOK" ]] && rm -f "$APT_HOOK"
|
||||
[[ -f "$APT_HOOK" ]] && pmx_remove_file "$APT_HOOK"
|
||||
|
||||
|
||||
msg_ok "Subscription banner removed successfully."
|
||||
|
||||
@@ -12,6 +12,9 @@ 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
|
||||
@@ -34,6 +37,8 @@ download_common_functions() {
|
||||
}
|
||||
|
||||
update_pve8() {
|
||||
local FUNC_VERSION="1.0"
|
||||
pmx_journal_context "update_pve8" "$FUNC_VERSION"
|
||||
local start_time=$(date +%s)
|
||||
local log_file="/var/log/proxmox-update-$(date +%Y%m%d-%H%M%S).log"
|
||||
local changes_made=false
|
||||
@@ -67,20 +72,20 @@ update_pve8() {
|
||||
|
||||
|
||||
if [ -f /etc/apt/sources.list.d/pve-enterprise.list ] && grep -q "^deb" /etc/apt/sources.list.d/pve-enterprise.list; then
|
||||
sed -i "s/^deb/#deb/g" /etc/apt/sources.list.d/pve-enterprise.list
|
||||
pmx_edit_file /etc/apt/sources.list.d/pve-enterprise.list "s/^deb/#deb/g"
|
||||
msg_ok "$(translate "Enterprise Proxmox repository disabled")"
|
||||
changes_made=true
|
||||
fi
|
||||
|
||||
if [ -f /etc/apt/sources.list.d/ceph.list ] && grep -q "^deb" /etc/apt/sources.list.d/ceph.list; then
|
||||
sed -i "s/^deb/#deb/g" /etc/apt/sources.list.d/ceph.list
|
||||
pmx_edit_file /etc/apt/sources.list.d/ceph.list "s/^deb/#deb/g"
|
||||
msg_ok "$(translate "Enterprise Proxmox Ceph repository disabled")"
|
||||
changes_made=true
|
||||
fi
|
||||
|
||||
|
||||
if [ ! -f /etc/apt/sources.list.d/pve-public-repo.list ] || ! grep -q "pve-no-subscription" /etc/apt/sources.list.d/pve-public-repo.list; then
|
||||
echo "deb http://download.proxmox.com/debian/pve $OS_CODENAME pve-no-subscription" > /etc/apt/sources.list.d/pve-public-repo.list
|
||||
echo "deb http://download.proxmox.com/debian/pve $OS_CODENAME pve-no-subscription" | pmx_write_file /etc/apt/sources.list.d/pve-public-repo.list
|
||||
msg_ok "$(translate "Free public Proxmox repository enabled")"
|
||||
changes_made=true
|
||||
fi
|
||||
@@ -90,14 +95,15 @@ update_pve8() {
|
||||
cp "$sources_file" "${sources_file}.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
if grep -q -E "(debian-security -security|debian main$|debian -updates)" "$sources_file"; then
|
||||
sed -i '/^deb.*debian-security -security/d' "$sources_file"
|
||||
sed -i '/^deb.*debian main$/d' "$sources_file"
|
||||
sed -i '/^deb.*debian -updates/d' "$sources_file"
|
||||
pmx_edit_file "$sources_file" \
|
||||
-e '/^deb.*debian-security -security/d' \
|
||||
-e '/^deb.*debian main$/d' \
|
||||
-e '/^deb.*debian -updates/d'
|
||||
changes_made=true
|
||||
msg_ok "$(translate "Malformed repository entries cleaned")"
|
||||
fi
|
||||
|
||||
cat > "$sources_file" << EOF
|
||||
pmx_write_file "$sources_file" << EOF
|
||||
# Debian $OS_CODENAME repositories
|
||||
deb http://deb.debian.org/debian $OS_CODENAME main contrib non-free non-free-firmware
|
||||
deb http://deb.debian.org/debian $OS_CODENAME-updates main contrib non-free non-free-firmware
|
||||
@@ -108,12 +114,13 @@ EOF
|
||||
|
||||
local firmware_conf="/etc/apt/apt.conf.d/no-firmware-warnings.conf"
|
||||
if [ ! -f "$firmware_conf" ]; then
|
||||
echo 'APT::Get::Update::SourceListWarnings::NonFreeFirmware "false";' > "$firmware_conf"
|
||||
echo 'APT::Get::Update::SourceListWarnings::NonFreeFirmware "false";' | pmx_write_file "$firmware_conf"
|
||||
fi
|
||||
|
||||
cleanup_duplicate_repos
|
||||
|
||||
msg_info "$(translate "Updating package lists...")"
|
||||
pmx_record_execution "Update package lists" "apt-get update"
|
||||
if apt-get update > "$log_file" 2>&1; then
|
||||
msg_ok "$(translate "Package lists updated successfully")"
|
||||
else
|
||||
@@ -159,12 +166,16 @@ EOF
|
||||
|
||||
if [[ $MENU_RESULT -eq 1 ]]; then
|
||||
msg_info2 "$(translate "Update cancelled by user")"
|
||||
pmx_record_execution "Remove unused packages" "apt-get -y autoremove"
|
||||
apt-get -y autoremove > /dev/null 2>&1 || true
|
||||
pmx_record_execution "Clean downloaded package cache" "apt-get -y autoclean"
|
||||
apt-get -y autoclean > /dev/null 2>&1 || true
|
||||
return 0
|
||||
elif [[ $MENU_RESULT -eq 2 ]]; then
|
||||
msg_ok "$(translate "System is already up to date. No update needed.")"
|
||||
pmx_record_execution "Remove unused packages" "apt-get -y autoremove"
|
||||
apt-get -y autoremove > /dev/null 2>&1 || true
|
||||
pmx_record_execution "Clean downloaded package cache" "apt-get -y autoclean"
|
||||
apt-get -y autoclean > /dev/null 2>&1 || true
|
||||
return 0
|
||||
fi
|
||||
@@ -173,6 +184,7 @@ EOF
|
||||
local conflicting_packages=$(dpkg -l 2>/dev/null | grep -E "^ii.*(ntp|openntpd|systemd-timesyncd)" | awk '{print $2}')
|
||||
if [ -n "$conflicting_packages" ]; then
|
||||
msg_info "$(translate "Removing conflicting utilities...")"
|
||||
pmx_record_execution "Purge conflicting time services" "apt-get -y purge $conflicting_packages"
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y purge $conflicting_packages >> "$log_file" 2>&1
|
||||
msg_ok "$(translate "Conflicting utilities removed")"
|
||||
fi
|
||||
@@ -185,7 +197,7 @@ EOF
|
||||
export DPKG_OPTIONS="--force-confdef --force-confold"
|
||||
|
||||
msg_info "$(translate "Performing packages upgrade...")"
|
||||
apt-get install pv -y > /dev/null 2>&1
|
||||
pmx_install_pkg pv
|
||||
total_packages=$(apt-get -s dist-upgrade | grep "^Inst" | wc -l)
|
||||
msg_ok "$(translate "Packages upgrade successfull")"
|
||||
|
||||
@@ -196,6 +208,7 @@ EOF
|
||||
tput civis
|
||||
tput sc
|
||||
|
||||
pmx_record_execution "Upgrade Proxmox VE 8 packages" "apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold dist-upgrade"
|
||||
(
|
||||
/usr/bin/env \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
@@ -250,7 +263,7 @@ EOF
|
||||
|
||||
if [ ${#missing_packages[@]} -gt 0 ]; then
|
||||
msg_info "$(translate "Installing essential Proxmox packages...")"
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install "${missing_packages[@]}" >> "$log_file" 2>&1
|
||||
pmx_install_pkg "${missing_packages[@]}"
|
||||
msg_ok "$(translate "Essential Proxmox packages installed")"
|
||||
fi
|
||||
|
||||
@@ -258,7 +271,9 @@ EOF
|
||||
cleanup_duplicate_repos
|
||||
|
||||
msg_info "$(translate "Performing system cleanup...")"
|
||||
pmx_record_execution "Remove unused packages" "apt-get -y autoremove"
|
||||
apt-get -y autoremove > /dev/null 2>&1 || true
|
||||
pmx_record_execution "Clean downloaded package cache" "apt-get -y autoclean"
|
||||
apt-get -y autoclean > /dev/null 2>&1 || true
|
||||
msg_ok "$(translate "Cleanup finished")"
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@ APT_ENV="env DEBIAN_FRONTEND=noninteractive LC_ALL=C LANG=C"
|
||||
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
|
||||
@@ -35,6 +38,8 @@ download_common_functions() {
|
||||
}
|
||||
|
||||
update_pve9() {
|
||||
local FUNC_VERSION="1.0"
|
||||
pmx_journal_context "update_pve9" "$FUNC_VERSION"
|
||||
local pve_version
|
||||
pve_version=$(pveversion | awk -F'/' '{print $2}' | cut -d'-' -f1)
|
||||
local start_time
|
||||
@@ -79,17 +84,17 @@ update_pve9() {
|
||||
disable_sources_repo() {
|
||||
local file="$1"
|
||||
if [[ -f "$file" ]]; then
|
||||
sed -i ':a;/^\n*$/{$d;N;ba}' "$file"
|
||||
pmx_edit_file "$file" ':a;/^\n*$/{$d;N;ba}'
|
||||
|
||||
if grep -q "^Enabled:" "$file"; then
|
||||
sed -i 's/^Enabled:.*$/Enabled: false/' "$file"
|
||||
pmx_edit_file "$file" 's/^Enabled:.*$/Enabled: false/'
|
||||
else
|
||||
echo "Enabled: false" >> "$file"
|
||||
echo "Enabled: false" | pmx_append_file "$file"
|
||||
fi
|
||||
|
||||
if ! grep -q "^Types: " "$file"; then
|
||||
msg_warn "$(translate "Malformed .sources file detected, removing: $(basename "$file")")"
|
||||
rm -f "$file"
|
||||
pmx_remove_file "$file"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
@@ -110,18 +115,18 @@ update_pve9() {
|
||||
/etc/apt/sources.list.d/pve-install-repo.list \
|
||||
/etc/apt/sources.list.d/debian.list; do
|
||||
if [[ -f "$legacy_file" ]]; then
|
||||
rm -f "$legacy_file"
|
||||
pmx_remove_file "$legacy_file"
|
||||
msg_ok "$(translate "Removed legacy repository: $(basename "$legacy_file")")" | tee -a "$screen_capture"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -f /etc/apt/sources.list.d/debian.sources ]]; then
|
||||
rm -f /etc/apt/sources.list.d/debian.sources
|
||||
pmx_remove_file /etc/apt/sources.list.d/debian.sources
|
||||
msg_ok "$(translate "Old debian.sources file removed to prevent duplication")" | tee -a "$screen_capture"
|
||||
fi
|
||||
|
||||
msg_info "$(translate "Creating Proxmox VE 9.x no-subscription repository...")"
|
||||
cat > /etc/apt/sources.list.d/proxmox.sources << EOF
|
||||
pmx_write_file /etc/apt/sources.list.d/proxmox.sources << EOF
|
||||
Enabled: true
|
||||
Types: deb
|
||||
URIs: http://download.proxmox.com/debian/pve
|
||||
@@ -134,7 +139,7 @@ EOF
|
||||
changes_made=true
|
||||
|
||||
msg_info "$(translate "Creating Debian ${TARGET_CODENAME} sources file...")"
|
||||
cat > /etc/apt/sources.list.d/debian.sources << EOF
|
||||
pmx_write_file /etc/apt/sources.list.d/debian.sources << EOF
|
||||
Types: deb
|
||||
URIs: http://deb.debian.org/debian/
|
||||
Suites: ${TARGET_CODENAME} ${TARGET_CODENAME}-updates
|
||||
@@ -154,11 +159,12 @@ EOF
|
||||
local firmware_conf="/etc/apt/apt.conf.d/no-firmware-warnings.conf"
|
||||
if [ ! -f "$firmware_conf" ]; then
|
||||
msg_info "$(translate "Disabling non-free firmware warnings...")"
|
||||
echo 'APT::Get::Update::SourceListWarnings::NonFreeFirmware "false";' > "$firmware_conf"
|
||||
echo 'APT::Get::Update::SourceListWarnings::NonFreeFirmware "false";' | pmx_write_file "$firmware_conf"
|
||||
msg_ok "$(translate "Non-free firmware warnings disabled")"
|
||||
fi
|
||||
|
||||
# UPDATE: no progress bar here (dpkg is not involved); capture output to parse errors
|
||||
pmx_record_execution "Update package lists" "apt-get update"
|
||||
update_output=$(apt-get update 2>&1)
|
||||
update_exit_code=$?
|
||||
|
||||
@@ -176,21 +182,25 @@ EOF
|
||||
|
||||
if command -v gpg >/dev/null 2>&1; then
|
||||
# Modern approach: receive -> export -> dearmor into /etc/apt/keyrings/<KEY>.gpg
|
||||
pmx_record_execution "Import missing repository signing key" "gpg --batch --keyserver keyserver.ubuntu.com --recv-keys $key"
|
||||
if gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" \
|
||||
&& gpg --batch --export "$key" | gpg --dearmor -o "/etc/apt/keyrings/${key}.gpg"; then
|
||||
msg_ok "$(translate "Imported missing GPG key: $key")"
|
||||
else
|
||||
msg_warn "$(translate "Keyrings method failed; trying apt-key fallback")"
|
||||
pmx_record_execution "Import missing repository signing key with apt-key" "apt-key adv --keyserver keyserver.ubuntu.com --recv-keys $key"
|
||||
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys "$key" >/dev/null 2>&1 || true
|
||||
fi
|
||||
else
|
||||
# Fallback for minimal systems without gpg installed
|
||||
msg_warn "$(translate "gpg not found; trying apt-key fallback")"
|
||||
pmx_record_execution "Import missing repository signing key with apt-key" "apt-key adv --keyserver keyserver.ubuntu.com --recv-keys $key"
|
||||
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys "$key" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Retry update after importing the key
|
||||
pmx_record_execution "Retry package list update" "apt-get update"
|
||||
if apt-get update > "$log_file" 2>&1; then
|
||||
msg_ok "$(translate "Package lists updated after GPG fix")" | tee -a "$screen_capture"
|
||||
else
|
||||
@@ -270,19 +280,24 @@ EOF
|
||||
|
||||
if [[ $MENU_RESULT -eq 1 ]]; then
|
||||
msg_info2 "$(translate "Update cancelled by user")"
|
||||
pmx_record_execution "Remove unused packages" "apt-get -y autoremove"
|
||||
apt-get -y autoremove > /dev/null 2>&1 || true
|
||||
pmx_record_execution "Clean downloaded package cache" "apt-get -y autoclean"
|
||||
apt-get -y autoclean > /dev/null 2>&1 || true
|
||||
rm -f "$screen_capture"
|
||||
return 0
|
||||
elif [[ $MENU_RESULT -eq 2 ]]; then
|
||||
msg_ok "$(translate "System is already up to date. No update needed.")"
|
||||
pmx_record_execution "Remove unused packages" "apt-get -y autoremove"
|
||||
apt-get -y autoremove > /dev/null 2>&1 || true
|
||||
pmx_record_execution "Clean downloaded package cache" "apt-get -y autoclean"
|
||||
apt-get -y autoclean > /dev/null 2>&1 || true
|
||||
rm -f "$screen_capture"
|
||||
return 0
|
||||
fi
|
||||
|
||||
msg_info "$(translate "Cleaning up unused time synchronization services...")"
|
||||
pmx_record_execution "Purge unused time synchronization services" "apt-get -y -o Dpkg::Options::=--force-confdef purge ntp openntpd systemd-timesyncd"
|
||||
if /usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' purge ntp openntpd systemd-timesyncd > /dev/null 2>&1; then
|
||||
msg_ok "$(translate "Old time services removed successfully")"
|
||||
else
|
||||
@@ -292,6 +307,7 @@ EOF
|
||||
echo -e
|
||||
|
||||
|
||||
pmx_record_execution "Upgrade Proxmox VE 9 packages" "apt -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold full-upgrade"
|
||||
DEBIAN_FRONTEND=noninteractive apt -y \
|
||||
-o Dpkg::Options::='--force-confdef' \
|
||||
-o Dpkg::Options::='--force-confold' \
|
||||
@@ -314,7 +330,7 @@ EOF
|
||||
msg_info "$(translate "Installing essential Proxmox packages...")"
|
||||
local additional_packages="zfsutils-linux proxmox-backup-restore-image chrony"
|
||||
|
||||
if /usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' install $additional_packages >> "$log_file" 2>&1; then
|
||||
if pmx_install_pkg $additional_packages; then
|
||||
msg_ok "$(translate "Essential Proxmox packages installed")"
|
||||
else
|
||||
msg_warn "$(translate "Some essential Proxmox packages may not have been installed")"
|
||||
@@ -323,7 +339,9 @@ EOF
|
||||
lvm_repair_check
|
||||
cleanup_duplicate_repos
|
||||
|
||||
pmx_record_execution "Remove unused packages" "apt-get -y autoremove"
|
||||
apt-get -y autoremove > /dev/null 2>&1 || true
|
||||
pmx_record_execution "Clean downloaded package cache" "apt-get -y autoclean"
|
||||
apt-get -y autoclean > /dev/null 2>&1 || true
|
||||
msg_ok "$(translate "Cleanup finished")"
|
||||
|
||||
|
||||
@@ -41,7 +41,16 @@ PROXMENUX_UTILS=(
|
||||
|
||||
# Ensure APT repositories are configured for the current PVE version.
|
||||
# Creates missing no-subscription repo entries for PVE8 (bookworm) or PVE9 (trixie).
|
||||
# Shared journal helpers, so any script sourcing this file records what
|
||||
# it installs without arranging for it.
|
||||
if [[ -f "${LOCAL_SCRIPTS:-/usr/local/share/proxmenux/scripts}/global/pmx_journal.sh" ]]; then
|
||||
source "${LOCAL_SCRIPTS:-/usr/local/share/proxmenux/scripts}/global/pmx_journal.sh"
|
||||
fi
|
||||
|
||||
|
||||
ensure_repositories() {
|
||||
local FUNC_VERSION="1.0"
|
||||
pmx_journal_context "ensure_repositories" "$FUNC_VERSION"
|
||||
local pve_version need_update=false
|
||||
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
|
||||
|
||||
@@ -57,7 +66,7 @@ ensure_repositories() {
|
||||
# 0640, which the PVE 9 webgui's repository manager treats as
|
||||
# unparseable and silently hides the source — issue #230.
|
||||
if [[ ! -f /etc/apt/sources.list.d/proxmox.sources ]]; then
|
||||
cat > /etc/apt/sources.list.d/proxmox.sources <<'EOF'
|
||||
pmx_write_file /etc/apt/sources.list.d/proxmox.sources <<'EOF'
|
||||
Enabled: true
|
||||
Types: deb
|
||||
URIs: http://download.proxmox.com/debian/pve
|
||||
@@ -70,7 +79,7 @@ EOF
|
||||
fi
|
||||
|
||||
if [[ ! -f /etc/apt/sources.list.d/debian.sources ]]; then
|
||||
cat > /etc/apt/sources.list.d/debian.sources <<'EOF'
|
||||
pmx_write_file /etc/apt/sources.list.d/debian.sources <<'EOF'
|
||||
Types: deb
|
||||
URIs: http://deb.debian.org/debian/
|
||||
Suites: trixie trixie-updates
|
||||
@@ -96,19 +105,20 @@ EOF
|
||||
echo "deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware"
|
||||
echo "deb http://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware"
|
||||
echo "deb http://security.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware"
|
||||
} >> "$sources_file"
|
||||
} | pmx_append_file "$sources_file"
|
||||
need_update=true
|
||||
fi
|
||||
|
||||
if [[ ! -f /etc/apt/sources.list.d/pve-no-subscription.list ]]; then
|
||||
echo "deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription" \
|
||||
> /etc/apt/sources.list.d/pve-no-subscription.list
|
||||
| pmx_write_file /etc/apt/sources.list.d/pve-no-subscription.list
|
||||
need_update=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$need_update" == true ]] || [[ ! -d /var/lib/apt/lists || -z "$(ls -A /var/lib/apt/lists 2>/dev/null)" ]]; then
|
||||
msg_info "$(translate "Updating APT package lists...")"
|
||||
pmx_record_execution "Update APT package lists" "apt-get update"
|
||||
apt-get update >/dev/null 2>&1 || apt-get update
|
||||
# Spinner pair: msg_info must be closed before returning.
|
||||
# Without this the next `msg_info` caller spawns a second
|
||||
@@ -132,7 +142,16 @@ install_single_package() {
|
||||
msg_info "$(translate "Installing") $package${description:+ ($description)}..."
|
||||
local install_success=false
|
||||
|
||||
if DEBIAN_FRONTEND=noninteractive apt-get install -y "$package" >/dev/null 2>&1; then
|
||||
# Every script that installs anything comes through here, so this is
|
||||
# where an installation becomes visible in the audit. What gets
|
||||
# recorded is the difference the operation made — the packages that
|
||||
# were not on the host and now are, dependencies included — rather
|
||||
# than the name that was asked for.
|
||||
if declare -F pmx_install_pkg >/dev/null 2>&1; then
|
||||
PMX_JOURNAL_FUNCTION="${PMX_JOURNAL_FUNCTION:-install_single_package}" \
|
||||
PMX_JOURNAL_SOURCE="${PMX_JOURNAL_SOURCE:-${SCRIPT_SOURCE:-utils-install-functions.sh}}" \
|
||||
pmx_install_pkg "$package" && install_success=true
|
||||
elif DEBIAN_FRONTEND=noninteractive apt-get install -y "$package" >/dev/null 2>&1; then
|
||||
install_success=true
|
||||
fi
|
||||
cleanup 2>/dev/null || true
|
||||
|
||||
@@ -5,6 +5,10 @@ if [[ -n "${__PROXMENUX_VM_STORAGE_HELPERS__}" ]]; then
|
||||
fi
|
||||
__PROXMENUX_VM_STORAGE_HELPERS__=1
|
||||
|
||||
if [[ -f "/usr/local/share/proxmenux/scripts/global/pmx_journal.sh" ]]; then
|
||||
source "/usr/local/share/proxmenux/scripts/global/pmx_journal.sh"
|
||||
fi
|
||||
|
||||
function _array_contains() {
|
||||
local needle="$1"
|
||||
shift
|
||||
@@ -371,6 +375,8 @@ function _vm_storage_register_vfio_iommu_tool() {
|
||||
}
|
||||
|
||||
function _vm_storage_enable_iommu_cmdline() {
|
||||
local FUNC_VERSION="1.0"
|
||||
pmx_journal_context "_vm_storage_enable_iommu_cmdline" "$FUNC_VERSION"
|
||||
local cpu_vendor iommu_param
|
||||
cpu_vendor=$(grep -m1 "vendor_id" /proc/cpuinfo 2>/dev/null | awk '{print $3}')
|
||||
|
||||
@@ -388,13 +394,15 @@ function _vm_storage_enable_iommu_cmdline() {
|
||||
if [[ -f "$cmdline_file" ]] && grep -qE 'root=ZFS=|root=ZFS/' "$cmdline_file" 2>/dev/null; then
|
||||
if ! grep -q "$iommu_param" "$cmdline_file"; then
|
||||
cp "$cmdline_file" "${cmdline_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
||||
sed -i "s|\\s*$| ${iommu_param} iommu=pt|" "$cmdline_file"
|
||||
pmx_edit_file "$cmdline_file" "s|\\s*$| ${iommu_param} iommu=pt|"
|
||||
pmx_record_execution "refresh Proxmox boot entries" "proxmox-boot-tool refresh"
|
||||
proxmox-boot-tool refresh >/dev/null 2>&1 || true
|
||||
fi
|
||||
elif [[ -f "$grub_file" ]]; then
|
||||
if ! grep -q "$iommu_param" "$grub_file"; then
|
||||
cp "$grub_file" "${grub_file}.bak.$(date +%Y%m%d_%H%M%S)"
|
||||
sed -i "/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param} iommu=pt\"|" "$grub_file"
|
||||
pmx_edit_file "$grub_file" "/GRUB_CMDLINE_LINUX_DEFAULT=/ s|\"$| ${iommu_param} iommu=pt\"|"
|
||||
pmx_record_execution "regenerate GRUB configuration" "update-grub"
|
||||
update-grub >/dev/null 2>&1 || true
|
||||
fi
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user