mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-06-14 20:36:59 +00:00
update beta 1.2.2.2
This commit is contained in:
126
scripts/backup_restore/collectors/build_manifest.sh
Normal file
126
scripts/backup_restore/collectors/build_manifest.sh
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==========================================================
|
||||
# ProxMenux backup manifest orchestrator
|
||||
# ==========================================================
|
||||
# Composes the six collectors into one manifest.json that
|
||||
# validates against schema/manifest.schema.json. Designed to
|
||||
# be called by backup_host.sh during a backup run. Read-only
|
||||
# (no side effects on the host).
|
||||
#
|
||||
# Usage:
|
||||
# build_manifest.sh [--paths-archived <path1> <path2> ...]
|
||||
# build_manifest.sh --validate (re-runs the JSON Schema validation)
|
||||
#
|
||||
# Stdout: pretty-printed manifest JSON.
|
||||
# Stderr: progress + warnings.
|
||||
# ==========================================================
|
||||
set -euo pipefail
|
||||
|
||||
COLLECTORS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SCHEMA_FILE="$COLLECTORS_DIR/../schema/manifest.schema.json"
|
||||
|
||||
# Parse flags
|
||||
paths_archived='null'
|
||||
do_validate=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--paths-archived)
|
||||
shift
|
||||
tmp='[]'
|
||||
while [[ $# -gt 0 && "$1" != --* ]]; do
|
||||
tmp="$(jq --argjson a "$tmp" --arg p "$1" -n '$a + [$p]')"
|
||||
shift
|
||||
done
|
||||
paths_archived="$tmp"
|
||||
;;
|
||||
--validate)
|
||||
do_validate=1; shift ;;
|
||||
-h|--help)
|
||||
sed -nE '/^# Usage:/,/^# Stderr:/p' "$0" | sed -E 's/^# ?//' >&2
|
||||
exit 0
|
||||
;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Run each collector. If a collector fails we fall back to a safe default
|
||||
# (empty array / null object) and warn — the manifest is still useful even
|
||||
# if one section is incomplete.
|
||||
run_collector() {
|
||||
local name="$1" fallback="$2"
|
||||
local out
|
||||
if out="$(bash "$COLLECTORS_DIR/$name" 2>>/tmp/proxmenux-manifest-stderr.log)"; then
|
||||
printf '%s' "$out"
|
||||
else
|
||||
printf 'warning: collector %s failed; using fallback\n' "$name" >&2
|
||||
printf '%s' "$fallback"
|
||||
fi
|
||||
}
|
||||
|
||||
# Empty error log first so we can attribute failures to this run.
|
||||
: >/tmp/proxmenux-manifest-stderr.log
|
||||
|
||||
source_host="$(run_collector collect_source_host.sh '{}')"
|
||||
hardware_inventory="$(run_collector collect_hardware.sh '{"gpu":[],"tpu":[],"nic":[],"wireless":[]}')"
|
||||
storage_inventory="$(run_collector collect_storage.sh '{"zfs_pools":[],"lvm":{"vgs":[]},"physical_disks":[],"pve_storage_cfg":[],"mounts":[]}')"
|
||||
installed_components="$(run_collector collect_proxmenux_state.sh '[]')"
|
||||
kernel_params="$(run_collector collect_kernel.sh '{"cmdline_extra":[],"modules_loaded_at_boot":[],"modprobe_d_files":[]}')"
|
||||
guests="$(run_collector collect_guests.sh '{"vms":[],"lxcs":[]}')"
|
||||
|
||||
created_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
# Compose the final manifest. The wrapper key matches the schema:
|
||||
# the top level is a single "proxmenux_backup_manifest" object.
|
||||
manifest="$(jq -n \
|
||||
--arg created_at "$created_at" \
|
||||
--arg created_by "proxmenux-host-backup/1.3.0" \
|
||||
--argjson source_host "$source_host" \
|
||||
--argjson hardware "$hardware_inventory" \
|
||||
--argjson storage "$storage_inventory" \
|
||||
--argjson components "$installed_components" \
|
||||
--argjson kernel "$kernel_params" \
|
||||
--argjson guests "$guests" \
|
||||
--argjson paths_archived "$paths_archived" \
|
||||
'{
|
||||
proxmenux_backup_manifest: {
|
||||
schema_version: 1,
|
||||
created_at: $created_at,
|
||||
created_by: $created_by,
|
||||
source_host: $source_host,
|
||||
hardware_inventory: $hardware,
|
||||
storage_inventory: $storage,
|
||||
proxmenux_installed_components: $components,
|
||||
kernel_params: $kernel,
|
||||
vms_lxcs_at_backup: $guests,
|
||||
backup_metadata: {
|
||||
encrypted: false,
|
||||
encryption_format: null,
|
||||
compression: "zstd",
|
||||
paths_archived: $paths_archived,
|
||||
sha256_archive: null,
|
||||
size_bytes: null
|
||||
}
|
||||
}
|
||||
}')"
|
||||
|
||||
# Optional validation step. If python3 + jsonschema are available, run
|
||||
# them; otherwise silently skip (validation is mostly a developer aid).
|
||||
if [[ "$do_validate" == 1 ]]; then
|
||||
if command -v python3 >/dev/null 2>&1 && python3 -c 'import jsonschema' 2>/dev/null; then
|
||||
printf '%s' "$manifest" | python3 -c "
|
||||
import json, sys, jsonschema
|
||||
schema = json.load(open('$SCHEMA_FILE'))
|
||||
inst = json.load(sys.stdin)
|
||||
try:
|
||||
jsonschema.validate(instance=inst, schema=schema)
|
||||
print('manifest: validates against schema', file=sys.stderr)
|
||||
except jsonschema.exceptions.ValidationError as e:
|
||||
print(f'manifest: SCHEMA VIOLATION at {list(e.absolute_path)}: {e.message}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
"
|
||||
else
|
||||
printf 'manifest: jsonschema python module not present; skipping validation\n' >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '%s\n' "$manifest"
|
||||
97
scripts/backup_restore/collectors/collect_guests.sh
Normal file
97
scripts/backup_restore/collectors/collect_guests.sh
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==========================================================
|
||||
# ProxMenux backup manifest collector — vms_lxcs_at_backup
|
||||
# ==========================================================
|
||||
# Enumerates VMs (qm list) and LXCs (pct list) on this PVE node.
|
||||
# Read-only; emits the metadata only — actual VM/LXC data is
|
||||
# the responsibility of vzdump / PBS, not this manifest.
|
||||
# Schema: scripts/backup_restore/schema/manifest.schema.json
|
||||
# ==========================================================
|
||||
set -euo pipefail
|
||||
|
||||
vms='[]'
|
||||
lxcs='[]'
|
||||
|
||||
# ── VMs (qm list) ──
|
||||
# Output:
|
||||
# VMID NAME STATUS MEM(MB) BOOTDISK(GB) PID
|
||||
# 100 Alpine-Linux-3-21 stopped 4096 0.00 0
|
||||
# Header line starts with VMID; we skip it.
|
||||
if command -v qm >/dev/null 2>&1; then
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
# Skip the header
|
||||
[[ "$line" =~ ^[[:space:]]*VMID[[:space:]] ]] && continue
|
||||
# Parse positionally. NAME can contain spaces, but `qm list` pads/columns
|
||||
# them, so we use fixed positions: VMID at col 1, STATUS as the 3rd
|
||||
# whitespace-delimited token from the END (mem, bootdisk, pid are after).
|
||||
vmid="$(printf '%s' "$line" | awk '{print $1}')"
|
||||
[[ "$vmid" =~ ^[0-9]+$ ]] || continue
|
||||
# Strip trailing PID + BOOTDISK + MEM(MB) + STATUS to extract the NAME.
|
||||
# rev → cut → rev technique:
|
||||
trailing="$(printf '%s' "$line" | awk '{printf "%s %s %s %s", $(NF-3), $(NF-2), $(NF-1), $NF}')"
|
||||
status="$(printf '%s' "$trailing" | awk '{print $1}')"
|
||||
memory_mb="$(printf '%s' "$trailing" | awk '{print $2}')"
|
||||
bootdisk_gb="$(printf '%s' "$trailing" | awk '{print $3}')"
|
||||
# Name: drop first column (vmid) and last 4 columns
|
||||
name="$(printf '%s' "$line" | awk '{$1=""; for(i=NF-3;i<=NF;i++) $i=""; sub(/^[[:space:]]+/,""); sub(/[[:space:]]+$/,""); print}')"
|
||||
case "$status" in
|
||||
running|stopped|paused) ;;
|
||||
*) status="stopped" ;;
|
||||
esac
|
||||
|
||||
vms="$(jq --argjson acc "$vms" \
|
||||
--argjson vmid "$vmid" \
|
||||
--arg name "$name" \
|
||||
--argjson memory_mb "${memory_mb:-0}" \
|
||||
--argjson bootdisk_gb "${bootdisk_gb:-0}" \
|
||||
--arg status "$status" \
|
||||
-n '
|
||||
$acc + [{
|
||||
vmid: $vmid,
|
||||
name: $name,
|
||||
memory_mb: $memory_mb,
|
||||
bootdisk_gb: $bootdisk_gb,
|
||||
status: $status,
|
||||
config_file: ("configs/qemu-server/" + ($vmid|tostring) + ".conf")
|
||||
}]
|
||||
')"
|
||||
done < <(qm list 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
# ── LXCs (pct list) ──
|
||||
# Output:
|
||||
# VMID Status Lock Name
|
||||
# 101 running alpine
|
||||
# Header line starts with VMID; we skip it.
|
||||
if command -v pct >/dev/null 2>&1; then
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
[[ "$line" =~ ^[[:space:]]*VMID[[:space:]] ]] && continue
|
||||
vmid="$(printf '%s' "$line" | awk '{print $1}')"
|
||||
[[ "$vmid" =~ ^[0-9]+$ ]] || continue
|
||||
status="$(printf '%s' "$line" | awk '{print $2}')"
|
||||
# Lock column is sparse; name is always last positional non-empty token
|
||||
name="$(printf '%s' "$line" | awk '{print $NF}')"
|
||||
case "$status" in
|
||||
running|stopped) ;;
|
||||
*) status="stopped" ;;
|
||||
esac
|
||||
|
||||
lxcs="$(jq --argjson acc "$lxcs" \
|
||||
--argjson vmid "$vmid" \
|
||||
--arg name "$name" \
|
||||
--arg status "$status" \
|
||||
-n '
|
||||
$acc + [{
|
||||
vmid: $vmid,
|
||||
name: $name,
|
||||
status: $status,
|
||||
config_file: ("configs/lxc/" + ($vmid|tostring) + ".conf")
|
||||
}]
|
||||
')"
|
||||
done < <(pct list 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
jq -n --argjson vms "$vms" --argjson lxcs "$lxcs" \
|
||||
'{ vms: $vms, lxcs: $lxcs }'
|
||||
222
scripts/backup_restore/collectors/collect_hardware.sh
Normal file
222
scripts/backup_restore/collectors/collect_hardware.sh
Normal file
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==========================================================
|
||||
# ProxMenux backup manifest collector — hardware_inventory
|
||||
# ==========================================================
|
||||
# Detects GPUs (with vendor → ProxMenux installer mapping),
|
||||
# TPUs (Coral PCIe/USB), NICs (with bridge membership), and
|
||||
# Wireless interfaces. Read-only. Schema:
|
||||
# scripts/backup_restore/schema/manifest.schema.json
|
||||
# ==========================================================
|
||||
set -euo pipefail
|
||||
|
||||
# Vendor → installer path mapping. Update when ProxMenux adds new
|
||||
# installers for hardware that depends on out-of-tree drivers.
|
||||
# Vendors WITHOUT a mapping get null (e.g. Intel/AMD iGPUs work with
|
||||
# in-tree drivers, no special installer needed).
|
||||
gpu_installer_for() {
|
||||
case "$1" in
|
||||
NVIDIA) echo "scripts/gpu_tpu/nvidia_installer.sh" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── GPUs ──
|
||||
# lspci -nnD outputs:
|
||||
# 0000:01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GP107GL [Quadro P620] [10de:1cb6] (rev a1)
|
||||
# We pick anything classified as VGA/3D/Display (display controllers).
|
||||
gpu_array='[]'
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
pci_address="$(printf '%s' "$line" | awk '{print $1}')"
|
||||
pci_id="$(printf '%s' "$line" | grep -oE '\[[0-9a-f]{4}:[0-9a-f]{4}\]' | tail -1 | tr -d '[]')"
|
||||
# Description: everything between the "controller]:" header and the
|
||||
# final "[pci_id]" tag. For AMD this includes the [AMD/ATI] tag; for
|
||||
# NVIDIA/Intel it's just vendor + model.
|
||||
desc="$(printf '%s' "$line" | sed -nE "s@.*\]:[[:space:]]*(.*)[[:space:]]+\[[0-9a-f]{4}:[0-9a-f]{4}\].*@\1@p")"
|
||||
|
||||
# Vendor classification
|
||||
case "$desc" in
|
||||
*NVIDIA*) vendor="NVIDIA" ;;
|
||||
*"Advanced Micro Devices"*|*AMD*) vendor="AMD" ;;
|
||||
*"Intel Corporation"*|*Intel*) vendor="Intel" ;;
|
||||
*) vendor="Other" ;;
|
||||
esac
|
||||
|
||||
# Model: strip every known vendor prefix from desc. Order matters —
|
||||
# the longest specific prefix (AMD's "Inc. [AMD/ATI]") must come before
|
||||
# the generic short one.
|
||||
model="$(printf '%s' "$desc" | sed -E '
|
||||
s/^Advanced Micro Devices, Inc\. \[AMD\/ATI\][[:space:]]+//
|
||||
s/^Advanced Micro Devices(, Inc\.)?[[:space:]]+//
|
||||
s/^NVIDIA Corporation[[:space:]]+//
|
||||
s/^Intel Corporation[[:space:]]+//
|
||||
s/[[:space:]]+$//
|
||||
')"
|
||||
# Kernel driver in use (may be empty if module not loaded yet)
|
||||
kernel_driver="$(lspci -nnks "$pci_address" 2>/dev/null | awk -F: '/Kernel driver in use/{sub(/^[ \t]+/,"",$2); print $2; exit}')"
|
||||
# Passthrough eligible if the GPU is bound to vfio-pci OR it's a discrete
|
||||
# secondary GPU (not the primary console). Pragmatic heuristic: discrete
|
||||
# GPUs are usually eligible; iGPUs (Intel HD/UHD, AMD APU iGPUs) usually not
|
||||
# because they drive the host console.
|
||||
passthrough_eligible=false
|
||||
case "$kernel_driver" in
|
||||
vfio-pci) passthrough_eligible=true ;;
|
||||
nvidia|nouveau) passthrough_eligible=true ;; # discrete by definition
|
||||
esac
|
||||
|
||||
# ProxMenux installer for this GPU vendor
|
||||
proxmenux_installer="$(gpu_installer_for "$vendor")"
|
||||
|
||||
# Installed driver version from the managed_installs registry
|
||||
installed_driver_version=""
|
||||
if [[ "$vendor" == "NVIDIA" ]] && [[ -f /usr/local/share/proxmenux/managed_installs.json ]]; then
|
||||
installed_driver_version="$(jq -r '
|
||||
.items[]
|
||||
| select(.removed_at == null and .type == "nvidia_xfree86")
|
||||
| .current_version // ""
|
||||
' /usr/local/share/proxmenux/managed_installs.json 2>/dev/null | head -1)"
|
||||
fi
|
||||
|
||||
gpu_array="$(jq --argjson acc "$gpu_array" \
|
||||
--arg vendor "$vendor" \
|
||||
--arg model "$model" \
|
||||
--arg pci_address "$pci_address" \
|
||||
--arg pci_id "$pci_id" \
|
||||
--arg kernel_driver "$kernel_driver" \
|
||||
--argjson passthrough_eligible "$passthrough_eligible" \
|
||||
--arg proxmenux_installer "$proxmenux_installer" \
|
||||
--arg installed_driver_version "$installed_driver_version" \
|
||||
-n '
|
||||
$acc + [{
|
||||
vendor: $vendor,
|
||||
model: $model,
|
||||
pci_address: $pci_address,
|
||||
pci_id: $pci_id,
|
||||
kernel_driver: (if $kernel_driver == "" then null else $kernel_driver end),
|
||||
passthrough_eligible: $passthrough_eligible,
|
||||
proxmenux_installer: (if $proxmenux_installer == "" then null else $proxmenux_installer end),
|
||||
installed_driver_version: (if $installed_driver_version == "" then null else $installed_driver_version end)
|
||||
}]
|
||||
')"
|
||||
done < <(lspci -nnD 2>/dev/null | grep -E 'VGA compatible|3D controller|Display controller' || true)
|
||||
|
||||
# ── TPUs (Google Coral) ──
|
||||
# PCIe variant: vendor 1ac1 (Global Unichip Corp) is the Coral M.2 / mPCIe.
|
||||
# USB variant: vendor 18d1 product 9302 (Google).
|
||||
tpu_array='[]'
|
||||
|
||||
# PCIe Coral
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
pci_address="$(printf '%s' "$line" | awk '{print $1}')"
|
||||
pci_id="$(printf '%s' "$line" | grep -oE '\[[0-9a-f]{4}:[0-9a-f]{4}\]' | tail -1 | tr -d '[]')"
|
||||
tpu_array="$(jq --argjson acc "$tpu_array" \
|
||||
--arg model "Coral PCIe" \
|
||||
--arg pci_address "$pci_address" \
|
||||
-n '
|
||||
$acc + [{
|
||||
vendor: "Google",
|
||||
model: $model,
|
||||
bus: "PCIe",
|
||||
pci_address: $pci_address,
|
||||
proxmenux_installer: "scripts/gpu_tpu/install_coral.sh",
|
||||
installed_version: null
|
||||
}]
|
||||
')"
|
||||
done < <(lspci -nnD 2>/dev/null | grep -iE '1ac1:|global unichip' || true)
|
||||
|
||||
# USB Coral
|
||||
if command -v lsusb >/dev/null 2>&1; then
|
||||
if lsusb 2>/dev/null | grep -qE '18d1:9302|Google.*Coral'; then
|
||||
tpu_array="$(jq --argjson acc "$tpu_array" \
|
||||
-n '
|
||||
$acc + [{
|
||||
vendor: "Google",
|
||||
model: "Coral USB",
|
||||
bus: "USB",
|
||||
pci_address: null,
|
||||
proxmenux_installer: "scripts/gpu_tpu/install_coral.sh",
|
||||
installed_version: null
|
||||
}]
|
||||
')"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── NICs ──
|
||||
# We want PHYSICAL interfaces (skip lo, veth*, tap*, fwln*, fwbr*, fwpr*).
|
||||
# Also distinguish wired from wireless.
|
||||
nic_array='[]'
|
||||
wireless_array='[]'
|
||||
|
||||
# Map each interface → its bridge by walking /sys/class/net/<bridge>/brif/.
|
||||
# We use bash glob expansion instead of `find -path` because find doesn't
|
||||
# follow the symlinks under /sys cleanly.
|
||||
declare -A bridge_for
|
||||
for brif_dir in /sys/class/net/*/brif; do
|
||||
[[ -d "$brif_dir" ]] || continue
|
||||
bridge="$(basename "$(dirname "$brif_dir")")"
|
||||
for member_link in "$brif_dir"/*; do
|
||||
[[ -e "$member_link" ]] || continue
|
||||
member="$(basename "$member_link")"
|
||||
bridge_for["$member"]="$bridge"
|
||||
done
|
||||
done
|
||||
|
||||
# Iterate over each physical net device
|
||||
for dev_path in /sys/class/net/*; do
|
||||
ifname="$(basename "$dev_path")"
|
||||
case "$ifname" in
|
||||
lo|veth*|tap*|fwln*|fwbr*|fwpr*|vmbr*|bond*) continue ;;
|
||||
esac
|
||||
# Bridges and bonds we record as their own thing; PHY interfaces only here.
|
||||
# Detect virtual interfaces (no device symlink → virtual)
|
||||
[[ ! -e "$dev_path/device" ]] && continue
|
||||
|
||||
mac="$(cat "$dev_path/address" 2>/dev/null || echo "")"
|
||||
[[ -z "$mac" ]] && continue
|
||||
operstate="$(cat "$dev_path/operstate" 2>/dev/null | tr '[:lower:]' '[:upper:]' || echo "UNKNOWN")"
|
||||
case "$operstate" in
|
||||
UP|DOWN) ;;
|
||||
*) operstate="UNKNOWN" ;;
|
||||
esac
|
||||
kernel_driver="$(basename "$(readlink "$dev_path/device/driver" 2>/dev/null || echo "")")"
|
||||
|
||||
# Wireless detection
|
||||
if [[ -d "$dev_path/wireless" ]] || [[ -d "$dev_path/phy80211" ]]; then
|
||||
wireless_array="$(jq --argjson acc "$wireless_array" \
|
||||
--arg ifname "$ifname" \
|
||||
--arg mac "$mac" \
|
||||
-n '$acc + [{ifname: $ifname, mac: $mac}]')"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Bridge membership: which vmbr* contains this NIC?
|
||||
in_bridges_json='[]'
|
||||
if [[ -n "${bridge_for[$ifname]:-}" ]]; then
|
||||
in_bridges_json="$(jq -n --arg b "${bridge_for[$ifname]}" '[$b]')"
|
||||
fi
|
||||
|
||||
nic_array="$(jq --argjson acc "$nic_array" \
|
||||
--arg ifname "$ifname" \
|
||||
--arg mac "$mac" \
|
||||
--arg kernel_driver "$kernel_driver" \
|
||||
--argjson in_bridges "$in_bridges_json" \
|
||||
--arg operstate "$operstate" \
|
||||
-n '
|
||||
$acc + [{
|
||||
ifname: $ifname,
|
||||
mac: $mac,
|
||||
kernel_driver: (if $kernel_driver == "" then null else $kernel_driver end),
|
||||
in_bridges: $in_bridges,
|
||||
operstate: $operstate
|
||||
}]
|
||||
')"
|
||||
done
|
||||
|
||||
# Compose the final object
|
||||
jq -n \
|
||||
--argjson gpu "$gpu_array" \
|
||||
--argjson tpu "$tpu_array" \
|
||||
--argjson nic "$nic_array" \
|
||||
--argjson wireless "$wireless_array" \
|
||||
'{ gpu: $gpu, tpu: $tpu, nic: $nic, wireless: $wireless }'
|
||||
67
scripts/backup_restore/collectors/collect_kernel.sh
Normal file
67
scripts/backup_restore/collectors/collect_kernel.sh
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==========================================================
|
||||
# ProxMenux backup manifest collector — kernel_params
|
||||
# ==========================================================
|
||||
# /proc/cmdline (filtered to user-meaningful extras), /etc/modules,
|
||||
# and /etc/modprobe.d/ files with custom directives. Read-only.
|
||||
# Schema: scripts/backup_restore/schema/manifest.schema.json
|
||||
# ==========================================================
|
||||
set -euo pipefail
|
||||
|
||||
# ── cmdline_extra ──
|
||||
# /proc/cmdline contains the kernel command line the bootloader passed.
|
||||
# We strip the boring boilerplate (BOOT_IMAGE, initrd, root, ro, rw, quiet,
|
||||
# splash, boot=zfs, rootflags) so the manifest captures only the user-
|
||||
# meaningful tweaks (intel_iommu, iommu=pt, hugepages, pcie_acs_override,
|
||||
# acpi=off, etc.). These are the bits a restore wizard cares about.
|
||||
cmdline_extra='[]'
|
||||
if [[ -r /proc/cmdline ]]; then
|
||||
raw_cmdline="$(cat /proc/cmdline)"
|
||||
for token in $raw_cmdline; do
|
||||
case "$token" in
|
||||
BOOT_IMAGE=*|initrd=*|root=*|ro|rw|quiet|splash|boot=*|rootflags=*)
|
||||
;; # boilerplate, drop
|
||||
*)
|
||||
cmdline_extra="$(jq --argjson acc "$cmdline_extra" --arg t "$token" -n '$acc + [$t]')"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
# ── modules_loaded_at_boot ──
|
||||
# /etc/modules lists modules systemd-modules-load.service inserts on boot.
|
||||
modules_at_boot='[]'
|
||||
if [[ -r /etc/modules ]]; then
|
||||
while IFS= read -r mod; do
|
||||
# Strip comments and inline comments
|
||||
mod="${mod%%#*}"
|
||||
mod="$(printf '%s' "$mod" | xargs)"
|
||||
[[ -z "$mod" ]] && continue
|
||||
modules_at_boot="$(jq --argjson acc "$modules_at_boot" --arg m "$mod" -n '$acc + [$m]')"
|
||||
done < /etc/modules
|
||||
fi
|
||||
|
||||
# ── modprobe_d_files ──
|
||||
# /etc/modprobe.d/*.conf files. We emit the path of every file that
|
||||
# contains at least one `options`, `blacklist`, `install`, `alias`, or
|
||||
# `softdep` directive — i.e. anything that has actual effect. Files that
|
||||
# are empty or pure comments aren't worth tracking.
|
||||
modprobe_files='[]'
|
||||
if [[ -d /etc/modprobe.d ]]; then
|
||||
for f in /etc/modprobe.d/*.conf; do
|
||||
[[ -r "$f" ]] || continue
|
||||
if grep -qE '^[[:space:]]*(options|blacklist|install|alias|softdep)[[:space:]]' "$f" 2>/dev/null; then
|
||||
modprobe_files="$(jq --argjson acc "$modprobe_files" --arg p "$f" -n '$acc + [$p]')"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
jq -n \
|
||||
--argjson cmdline_extra "$cmdline_extra" \
|
||||
--argjson modules_loaded "$modules_at_boot" \
|
||||
--argjson modprobe_files "$modprobe_files" \
|
||||
'{
|
||||
cmdline_extra: $cmdline_extra,
|
||||
modules_loaded_at_boot: $modules_loaded,
|
||||
modprobe_d_files: $modprobe_files
|
||||
}'
|
||||
81
scripts/backup_restore/collectors/collect_proxmenux_state.sh
Normal file
81
scripts/backup_restore/collectors/collect_proxmenux_state.sh
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==========================================================
|
||||
# ProxMenux backup manifest collector — proxmenux_installed_components
|
||||
# ==========================================================
|
||||
# Reads ProxMenux's managed_installs registry + post-install
|
||||
# tools marker file and emits the installed components array.
|
||||
# Read-only. Schema:
|
||||
# scripts/backup_restore/schema/manifest.schema.json
|
||||
# ==========================================================
|
||||
set -euo pipefail
|
||||
|
||||
REGISTRY="/usr/local/share/proxmenux/managed_installs.json"
|
||||
INSTALLED_TOOLS="/usr/local/share/proxmenux/installed_tools.json"
|
||||
|
||||
components='[]'
|
||||
|
||||
# ── managed_installs registry ──
|
||||
# Each entry already carries the installer path under `menu_script`,
|
||||
# so we trust the registry as the single source of truth. We skip LXC
|
||||
# entries because containers are restored via vzdump, not via the
|
||||
# host-config restore path.
|
||||
if [[ -r "$REGISTRY" ]]; then
|
||||
while IFS= read -r item; do
|
||||
[[ -z "$item" ]] && continue
|
||||
id="$(printf '%s' "$item" | jq -r '.id')"
|
||||
type="$(printf '%s' "$item" | jq -r '.type // ""')"
|
||||
version="$(printf '%s' "$item" | jq -r '.current_version // ""')"
|
||||
# menu_script in the registry is null for components that handle their
|
||||
# own update lifecycle (e.g. OCI apps via the secure-gateway runtime).
|
||||
# We keep that null forward: restore won't try to reinstall those —
|
||||
# the user reconfigures them after restore.
|
||||
installer="$(printf '%s' "$item" | jq -r '.menu_script // ""')"
|
||||
|
||||
components="$(jq --argjson acc "$components" \
|
||||
--arg id "$id" --arg type "$type" --arg version "$version" --arg installer "$installer" \
|
||||
-n '
|
||||
$acc + [{
|
||||
id: $id,
|
||||
type: $type,
|
||||
version_at_backup: (if $version == "" then null else $version end),
|
||||
proxmenux_installer: (if $installer == "" then null else $installer end),
|
||||
applied_settings: []
|
||||
}]
|
||||
')"
|
||||
done < <(jq -c '.items[]? | select(.removed_at == null) | select(.type != "lxc")' "$REGISTRY" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
# ── installed_tools.json (post-install optimizations) ──
|
||||
# Format: array of {name: ..., installed_at: ...} or similar. The exact
|
||||
# shape varies across ProxMenux versions; we emit one synthetic component
|
||||
# named "post_install_optimizations" with the applied_settings list.
|
||||
if [[ -r "$INSTALLED_TOOLS" ]]; then
|
||||
applied_settings="$(jq -c '
|
||||
if type == "object" then
|
||||
(.tools // .installed // [] | map(.name // .id // tostring))
|
||||
elif type == "array" then
|
||||
map(.name // .id // tostring)
|
||||
else []
|
||||
end
|
||||
' "$INSTALLED_TOOLS" 2>/dev/null || echo '[]')"
|
||||
|
||||
# Only emit if we have at least one applied setting — otherwise the
|
||||
# component would be noise.
|
||||
count="$(printf '%s' "$applied_settings" | jq 'length' 2>/dev/null || echo 0)"
|
||||
if [[ "${count:-0}" -gt 0 ]]; then
|
||||
components="$(jq --argjson acc "$components" --argjson s "$applied_settings" \
|
||||
-n '
|
||||
$acc + [{
|
||||
id: "post_install_optimizations",
|
||||
type: "proxmenux_post_install",
|
||||
version_at_backup: null,
|
||||
proxmenux_installer: "scripts/post_install/customizable_post_install.sh",
|
||||
applied_settings: $s
|
||||
}]
|
||||
')"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Output: bare array (not wrapped in an object — the orchestrator places
|
||||
# this under .proxmenux_installed_components).
|
||||
printf '%s\n' "$components"
|
||||
98
scripts/backup_restore/collectors/collect_source_host.sh
Normal file
98
scripts/backup_restore/collectors/collect_source_host.sh
Normal file
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==========================================================
|
||||
# ProxMenux backup manifest collector — source_host
|
||||
# ==========================================================
|
||||
# Emits the `source_host` section of the manifest as JSON to
|
||||
# stdout. Read-only; no side effects. Schema:
|
||||
# scripts/backup_restore/schema/manifest.schema.json
|
||||
# ==========================================================
|
||||
set -euo pipefail
|
||||
|
||||
# ── pve_version_full / pve_version ──
|
||||
# pveversion's first line is like:
|
||||
# pve-manager/9.2.2/b9984c6d90a4bd80 (running kernel: 7.0.2-6-pve)
|
||||
pve_version_full=""
|
||||
pve_version=""
|
||||
if command -v pveversion >/dev/null 2>&1; then
|
||||
pve_version_full="$(pveversion 2>/dev/null | head -1 || true)"
|
||||
# Extract the X.Y.Z between "pve-manager/" and "/"
|
||||
pve_version="$(printf '%s\n' "$pve_version_full" | sed -nE 's@^pve-manager/([0-9.]+)/.*@\1@p')"
|
||||
fi
|
||||
|
||||
# ── pbs_version ──
|
||||
# PBS is a separate package. If proxmox-backup-manager exists, host has PBS role.
|
||||
pbs_version=""
|
||||
if command -v proxmox-backup-manager >/dev/null 2>&1; then
|
||||
pbs_version="$(proxmox-backup-manager versions 2>/dev/null | awk '/^proxmox-backup-server/{print $2; exit}' || true)"
|
||||
fi
|
||||
|
||||
# ── roles ──
|
||||
roles_json='[]'
|
||||
if [[ -n "$pve_version" && -n "$pbs_version" ]]; then
|
||||
roles_json='["pve","pbs"]'
|
||||
elif [[ -n "$pve_version" ]]; then
|
||||
roles_json='["pve"]'
|
||||
elif [[ -n "$pbs_version" ]]; then
|
||||
roles_json='["pbs"]'
|
||||
else
|
||||
# No PVE, no PBS — exit with the unknown sentinel. Caller decides
|
||||
# whether to abort or generate a system-only manifest.
|
||||
roles_json='[]'
|
||||
fi
|
||||
|
||||
# ── kernel, boot_mode, root_fs ──
|
||||
kernel="$(uname -r)"
|
||||
if [[ -d /sys/firmware/efi ]]; then
|
||||
boot_mode="efi"
|
||||
else
|
||||
boot_mode="bios"
|
||||
fi
|
||||
root_fs="$(findmnt -no FSTYPE / 2>/dev/null || echo ext4)"
|
||||
|
||||
# ── CPU model / arch ──
|
||||
cpu_model="$(lscpu 2>/dev/null | awk -F: '/^Model name/{sub(/^[ \t]+/, "", $2); print $2; exit}')"
|
||||
cpu_arch="$(uname -m)"
|
||||
# Normalize to schema enum
|
||||
case "$cpu_arch" in
|
||||
x86_64|amd64) cpu_arch="x86_64" ;;
|
||||
aarch64|arm64) cpu_arch="aarch64" ;;
|
||||
esac
|
||||
|
||||
# ── memory_kb ──
|
||||
memory_kb="$(awk '/^MemTotal:/{print $2; exit}' /proc/meminfo 2>/dev/null || echo 0)"
|
||||
|
||||
# ── subscription_status ──
|
||||
subscription_status=""
|
||||
if command -v pvesubscription >/dev/null 2>&1; then
|
||||
subscription_status="$(pvesubscription get 2>/dev/null | awk -F: '/^status:/{sub(/^[ \t]+/,"",$2); print $2; exit}')"
|
||||
fi
|
||||
|
||||
# Build JSON. Use --arg for strings (always quoted), --argjson for
|
||||
# numbers/arrays/null. Empty strings → null per schema convention.
|
||||
jq -n \
|
||||
--arg hostname "$(hostname)" \
|
||||
--arg pve_version "$pve_version" \
|
||||
--arg pve_version_full "$pve_version_full" \
|
||||
--arg pbs_version "$pbs_version" \
|
||||
--argjson roles "$roles_json" \
|
||||
--arg kernel "$kernel" \
|
||||
--arg boot_mode "$boot_mode" \
|
||||
--arg root_fs "$root_fs" \
|
||||
--arg cpu_model "$cpu_model" \
|
||||
--arg cpu_arch "$cpu_arch" \
|
||||
--argjson memory_kb "$memory_kb" \
|
||||
--arg subscription_status "$subscription_status" \
|
||||
'{
|
||||
hostname: $hostname,
|
||||
pve_version: (if $pve_version == "" then null else $pve_version end),
|
||||
pve_version_full: (if $pve_version_full == "" then null else $pve_version_full end),
|
||||
pbs_version: (if $pbs_version == "" then null else $pbs_version end),
|
||||
roles: $roles,
|
||||
kernel: $kernel,
|
||||
boot_mode: $boot_mode,
|
||||
root_fs: $root_fs,
|
||||
cpu_model: $cpu_model,
|
||||
cpu_arch: $cpu_arch,
|
||||
memory_kb: $memory_kb,
|
||||
subscription_status: (if $subscription_status == "" then null else $subscription_status end)
|
||||
}'
|
||||
252
scripts/backup_restore/collectors/collect_storage.sh
Normal file
252
scripts/backup_restore/collectors/collect_storage.sh
Normal file
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==========================================================
|
||||
# ProxMenux backup manifest collector — storage_inventory
|
||||
# ==========================================================
|
||||
# ZFS pools (with stable by-id devices), LVM VGs + thin pools,
|
||||
# physical disks, PVE storage.cfg, and external mounts.
|
||||
# Read-only. Schema:
|
||||
# scripts/backup_restore/schema/manifest.schema.json
|
||||
# ==========================================================
|
||||
set -euo pipefail
|
||||
|
||||
# ── ZFS pools ──
|
||||
zfs_pools='[]'
|
||||
if command -v zpool >/dev/null 2>&1; then
|
||||
while IFS= read -r pool; do
|
||||
[[ -z "$pool" ]] && continue
|
||||
# type: parse zpool status — first vdev line after 'config:' header.
|
||||
# Single-device pool shows the device directly; mirror/raidz prefix the
|
||||
# vdev type. We look at the indented children list.
|
||||
pool_type="single"
|
||||
devices='[]'
|
||||
# `zpool status -P` outputs full /dev/disk/by-id/... paths for the
|
||||
# member disks. We isolate the first whitespace-delimited token on
|
||||
# each child line and decide:
|
||||
# - vdev type lines (mirror-0, raidz1-0, stripe, ...) → pool type
|
||||
# - leaf device lines (/dev/disk/by-id/* or /dev/sd*) → membership
|
||||
while IFS= read -r vdev_line; do
|
||||
token="$(printf '%s' "$vdev_line" | awk '{print $1}')"
|
||||
[[ -z "$token" || "$token" == "NAME" || "$token" == "$pool" ]] && continue
|
||||
case "$token" in
|
||||
mirror-*) pool_type="mirror" ;;
|
||||
raidz1-*) pool_type="raidz1" ;;
|
||||
raidz2-*) pool_type="raidz2" ;;
|
||||
raidz3-*) pool_type="raidz3" ;;
|
||||
stripe-*) pool_type="stripe" ;;
|
||||
/dev/disk/by-id/*)
|
||||
# Strip the /dev/disk/by-id/ prefix for the schema field;
|
||||
# leave any -partN suffix in place — the restore wizard uses
|
||||
# the exact same string to look the disk back up.
|
||||
dev_name="${token#/dev/disk/by-id/}"
|
||||
devices="$(jq --argjson acc "$devices" --arg d "$dev_name" -n '$acc + [$d]')"
|
||||
;;
|
||||
/dev/*)
|
||||
# Fallback: ZFS pool created with raw /dev/sdX paths. Record
|
||||
# them as-is; restore will need to remap manually.
|
||||
devices="$(jq --argjson acc "$devices" --arg d "$token" -n '$acc + [$d]')"
|
||||
;;
|
||||
esac
|
||||
done < <(zpool status -P "$pool" 2>/dev/null | awk '/^config:/{flag=1; next} /^errors:/{flag=0} flag')
|
||||
|
||||
size_bytes="$(zpool list -H -p -o size "$pool" 2>/dev/null || echo 0)"
|
||||
health="$(zpool list -H -o health "$pool" 2>/dev/null || echo UNKNOWN)"
|
||||
compression="$(zfs get -H -o value compression "$pool" 2>/dev/null || echo "")"
|
||||
mountpoint="$(zfs get -H -o value mountpoint "$pool" 2>/dev/null || echo "")"
|
||||
|
||||
zfs_pools="$(jq --argjson acc "$zfs_pools" \
|
||||
--arg name "$pool" \
|
||||
--arg type "$pool_type" \
|
||||
--argjson devices "$devices" \
|
||||
--arg mountpoint "$mountpoint" \
|
||||
--arg compression "$compression" \
|
||||
--argjson size_bytes "${size_bytes:-0}" \
|
||||
--arg health "$health" \
|
||||
-n '
|
||||
$acc + [{
|
||||
name: $name,
|
||||
type: $type,
|
||||
devices_by_id: $devices,
|
||||
mountpoint: $mountpoint,
|
||||
compression: $compression,
|
||||
size_bytes: $size_bytes,
|
||||
health: $health
|
||||
}]
|
||||
')"
|
||||
done < <(zpool list -H -o name 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
# ── LVM VGs + thin pools ──
|
||||
lvm_vgs='[]'
|
||||
if command -v vgs >/dev/null 2>&1; then
|
||||
# vgs --reportformat json --units b is reliable in lvm2 ≥ 2.02
|
||||
vg_json="$(vgs --reportformat json --units b --noheadings -o vg_name,vg_size 2>/dev/null || echo '{}')"
|
||||
while IFS= read -r vg_name; do
|
||||
[[ -z "$vg_name" || "$vg_name" == "null" ]] && continue
|
||||
vg_size="$(printf '%s' "$vg_json" | jq -r --arg n "$vg_name" '.report[0].vg[]? | select(.vg_name == $n) | .vg_size' | sed 's/[Bb]$//' | head -1)"
|
||||
# Thin pools in this VG
|
||||
thin_pools='[]'
|
||||
while IFS= read -r lv_line; do
|
||||
[[ -z "$lv_line" ]] && continue
|
||||
lv_name="$(printf '%s' "$lv_line" | awk '{print $1}')"
|
||||
lv_size="$(printf '%s' "$lv_line" | awk '{print $2}' | sed 's/[Bb]$//')"
|
||||
thin_pools="$(jq --argjson acc "$thin_pools" \
|
||||
--arg n "$lv_name" --argjson s "${lv_size:-0}" \
|
||||
-n '$acc + [{lv_name: $n, size_bytes: $s}]')"
|
||||
done < <(lvs --noheadings --units b -o lv_name,lv_size --select "vg_name=$vg_name && lv_attr=~^t" 2>/dev/null || true)
|
||||
|
||||
lvm_vgs="$(jq --argjson acc "$lvm_vgs" \
|
||||
--arg n "$vg_name" --argjson s "${vg_size:-0}" --argjson tp "$thin_pools" \
|
||||
-n '$acc + [{name: $n, size_bytes: $s, thin_pools: $tp}]')"
|
||||
done < <(printf '%s' "$vg_json" | jq -r '.report[0].vg[]?.vg_name' 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
# ── Physical disks (by-id resolution) ──
|
||||
physical_disks='[]'
|
||||
# Build name → by-id map by walking /dev/disk/by-id/. A single block
|
||||
# device usually has multiple by-id symlinks (ata-*, wwn-*, scsi-*, …).
|
||||
# We prefer the most human-readable identifier in this order:
|
||||
# ata-* → nvme-* → scsi-* → usb-* → wwn-*
|
||||
# This also makes the manifest consistent with what `zpool status -P`
|
||||
# reports (zpool defaults to ata-* / wwn-* depending on bus).
|
||||
declare -A by_id_for
|
||||
declare -A by_id_priority_for
|
||||
priority_for_id() {
|
||||
case "$1" in
|
||||
ata-*) echo 1 ;;
|
||||
nvme-*) echo 2 ;;
|
||||
scsi-*) echo 3 ;;
|
||||
usb-*) echo 4 ;;
|
||||
wwn-*) echo 5 ;;
|
||||
*) echo 9 ;;
|
||||
esac
|
||||
}
|
||||
if [[ -d /dev/disk/by-id ]]; then
|
||||
for link in /dev/disk/by-id/*; do
|
||||
[[ -L "$link" ]] || continue
|
||||
by_id="$(basename "$link")"
|
||||
# Skip partition symlinks — we want whole-disk only.
|
||||
[[ "$by_id" == *-part* ]] && continue
|
||||
target="$(basename "$(readlink -f "$link")")"
|
||||
[[ -z "$target" ]] && continue
|
||||
new_prio="$(priority_for_id "$by_id")"
|
||||
cur_prio="${by_id_priority_for[$target]:-99}"
|
||||
if (( new_prio < cur_prio )); then
|
||||
by_id_for["$target"]="$by_id"
|
||||
by_id_priority_for["$target"]="$new_prio"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# lsblk -d -b -J for whole disks
|
||||
lsblk_json="$(lsblk -d -b -o NAME,MODEL,SIZE,TYPE -J 2>/dev/null || echo '{}')"
|
||||
while IFS= read -r disk_line; do
|
||||
[[ -z "$disk_line" ]] && continue
|
||||
name="$(printf '%s' "$disk_line" | jq -r '.name')"
|
||||
model="$(printf '%s' "$disk_line" | jq -r '.model // ""')"
|
||||
size="$(printf '%s' "$disk_line" | jq -r '.size // 0')"
|
||||
type="$(printf '%s' "$disk_line" | jq -r '.type')"
|
||||
# Only PHYSICAL disks.
|
||||
# - skip non-disk types (rom, loop)
|
||||
# - skip zd* (ZFS zvols backing VMs)
|
||||
# - skip dm-* (LVM-mapped devices)
|
||||
# - skip loop* (defensive — type filter usually catches it)
|
||||
[[ "$type" != "disk" ]] && continue
|
||||
case "$name" in
|
||||
zd*|dm-*|loop*) continue ;;
|
||||
esac
|
||||
by_id="${by_id_for[$name]:-}"
|
||||
physical_disks="$(jq --argjson acc "$physical_disks" \
|
||||
--arg n "$name" --arg m "$model" --argjson s "${size:-0}" --arg bid "$by_id" \
|
||||
-n '
|
||||
$acc + [{
|
||||
name: $n,
|
||||
model: (if $m == "" then null else $m end),
|
||||
size_bytes: $s,
|
||||
by_id: (if $bid == "" then null else $bid end)
|
||||
}]
|
||||
')"
|
||||
done < <(printf '%s' "$lsblk_json" | jq -c '.blockdevices[]?' 2>/dev/null || true)
|
||||
|
||||
# ── PVE storage.cfg ──
|
||||
# Format is whitespace-key-value with blank-line separators:
|
||||
# <type>: <id>
|
||||
# key value
|
||||
# key value
|
||||
pve_storage='[]'
|
||||
if [[ -r /etc/pve/storage.cfg ]]; then
|
||||
current_type=""; current_id=""; current_extra='{}'
|
||||
flush() {
|
||||
if [[ -n "$current_id" ]]; then
|
||||
pve_storage="$(jq --argjson acc "$pve_storage" \
|
||||
--arg id "$current_id" --arg t "$current_type" --argjson e "$current_extra" \
|
||||
-n '$acc + [(($e) + {id: $id, type: $t})]')"
|
||||
fi
|
||||
current_type=""; current_id=""; current_extra='{}'
|
||||
}
|
||||
while IFS= read -r line; do
|
||||
if [[ -z "${line// }" ]]; then
|
||||
flush; continue
|
||||
fi
|
||||
if [[ "$line" =~ ^([a-z]+):[[:space:]]+([A-Za-z0-9_.-]+) ]]; then
|
||||
flush
|
||||
current_type="${BASH_REMATCH[1]}"
|
||||
current_id="${BASH_REMATCH[2]}"
|
||||
elif [[ "$line" =~ ^[[:space:]]+([a-z_]+)[[:space:]]+(.*)$ ]]; then
|
||||
key="${BASH_REMATCH[1]}"
|
||||
val="${BASH_REMATCH[2]}"
|
||||
case "$key" in
|
||||
# `content` is a comma-separated list — split into JSON array
|
||||
content)
|
||||
content_array="$(printf '%s\n' "$val" | tr ',' '\n' | jq -R . | jq -s .)"
|
||||
current_extra="$(jq --argjson e "$current_extra" --argjson c "$content_array" -n '$e + {content: $c}')"
|
||||
;;
|
||||
*)
|
||||
current_extra="$(jq --argjson e "$current_extra" --arg k "$key" --arg v "$val" -n '$e + {($k): $v}')"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done < /etc/pve/storage.cfg
|
||||
flush
|
||||
fi
|
||||
|
||||
# ── External mounts (NFS/CIFS/etc.) ──
|
||||
# Filter on filesystem types we care about for the manifest. Drop FUSE
|
||||
# pmxcfs (/etc/pve), tmpfs, devtmpfs, autofs, ZFS internals already
|
||||
# accounted for. NFS, CIFS, ISO mount points are the interesting ones.
|
||||
mounts='[]'
|
||||
if command -v findmnt >/dev/null 2>&1; then
|
||||
while IFS= read -r mline; do
|
||||
[[ -z "$mline" ]] && continue
|
||||
target="$(printf '%s' "$mline" | jq -r '.target')"
|
||||
source="$(printf '%s' "$mline" | jq -r '.source')"
|
||||
fstype="$(printf '%s' "$mline" | jq -r '.fstype')"
|
||||
options="$(printf '%s' "$mline" | jq -r '.options // ""')"
|
||||
mounts="$(jq --argjson acc "$mounts" \
|
||||
--arg t "$target" --arg s "$source" --arg f "$fstype" --arg o "$options" \
|
||||
-n '
|
||||
$acc + [{
|
||||
target: $t,
|
||||
source: $s,
|
||||
fstype: $f,
|
||||
options: (if $o == "" then null else $o end)
|
||||
}]
|
||||
')"
|
||||
done < <(findmnt -t nfs,nfs4,cifs,smbfs,fuseblk,fuse.glusterfs -J 2>/dev/null \
|
||||
| jq -c '.. | objects | select(.target?)' 2>/dev/null \
|
||||
| grep -vE '"target":"/etc/pve"' || true)
|
||||
fi
|
||||
|
||||
# Compose
|
||||
jq -n \
|
||||
--argjson zfs_pools "$zfs_pools" \
|
||||
--argjson lvm_vgs "$lvm_vgs" \
|
||||
--argjson physical_disks "$physical_disks" \
|
||||
--argjson pve_storage "$pve_storage" \
|
||||
--argjson mounts "$mounts" \
|
||||
'{
|
||||
zfs_pools: $zfs_pools,
|
||||
lvm: { vgs: $lvm_vgs },
|
||||
physical_disks: $physical_disks,
|
||||
pve_storage_cfg: $pve_storage,
|
||||
mounts: $mounts
|
||||
}'
|
||||
Reference in New Issue
Block a user