added proxmox backup script

This commit is contained in:
2026-07-13 00:48:28 -04:00
parent aeb58d45b6
commit b078fe66e2
2 changed files with 827 additions and 0 deletions
+286
View File
@@ -0,0 +1,286 @@
# Proxmox VE Backup & Restore Tool
A self-contained bash script for backing up and restoring all critical Proxmox VE configuration files. Archives are stored locally (never on the remote host) and can be used to rebuild a PVE node after boot drive failure.
## Overview
The script connects to a Proxmox node (locally or over SSH), collects configuration files from across the filesystem, packages them into a timestamped `.tar.gz` archive, and stores it on the machine running the script. Restore extracts a backup archive and pushes files back.
## Requirements
- **Source node:** Proxmox VE (any supported version)
- **Machine running the script:** Linux with `bash`, `tar`, `gzip`
- **Remote access:** SSH access to the Proxmox node (passwordless key recommended)
- **Permissions:** Root or passwordless sudo on the Proxmox node
- **rsync (optional):** Installed on both machines for incremental transfers — falls back to tar/cat/pipe if absent
## Usage
```
Usage: pve-backup.sh <command> [options]
Commands:
backup Create a backup of Proxmox configuration
restore Restore Proxmox configuration from a backup
list List available backups
verify Verify a backup archive's integrity (SHA256)
Backup options:
-d, --dir DIR Local backup directory (default: ./pve-backups)
-h, --host HOST Remote Proxmox host (SSH)
-u, --user USER SSH user (default: root)
-i, --identity FILE SSH identity file
Restore / Verify options:
-d, --dir DIR Directory containing backup archive
-f, --file FILE Specific backup archive to restore/verify
-y, --yes Skip confirmation prompts (DANGEROUS)
List options:
-d, --dir DIR Directory to list backups from
```
### Examples
```bash
# Local backup (must be root on the Proxmox node)
./pve-backup.sh backup
# Remote backup with custom directory
./pve-backup.sh backup -h 192.168.1.100 -d /mnt/backups
# Remote backup as non-root user (uses sudo -n automatically)
./pve-backup.sh backup -h 192.168.1.100 -u admin -i ~/.ssh/id_rsa
# Verify a backup archive
./pve-backup.sh verify -f pve-backups/pve-backup-prox1-20250101_120000.tar.gz
# Restore the latest backup
./pve-backup.sh restore
# Restore a specific backup archive
./pve-backup.sh restore -f /path/to/pve-backup-prox1-20250101_120000.tar.gz
# Non-interactive restore (for automation)
./pve-backup.sh restore -f /path/to/backup.tar.gz -y
# List all local backups
./pve-backup.sh list
```
### Defaults
| Setting | Default |
|---|---|
| Backup directory | `./pve-backups` (relative to script location) |
| SSH user | `root` |
| Archive name format | `pve-backup-{hostname}-{YYYYMMDD}_{HHMMSS}.tar.gz` |
## Transfer Method
The script uses one of two transfer methods depending on what's available:
### rsync (preferred)
When connecting to a remote host, the script checks if `rsync` is installed on the remote side. If yes:
- File-based configs (`/etc/pve/`, `/etc/network/interfaces`, APT sources, etc.) are transferred via `rsync -a`
- Subsequent backups only transfer changed files, reducing bandwidth and time
- Permissions and ownership are preserved natively by rsync
- If sudo is needed, `--rsync-path="sudo -n rsync"` is used automatically
### tar/cat/pipe (fallback)
If rsync is not available on the remote host, the script falls back to the original method:
- `/etc/pve/` is tarballed on the remote, piped through SSH, and extracted locally
- Individual files are fetched with `cat > file` over SSH
- Directories (APT sources, sysctl, cron) are tarballed with `cp -a`, then piped through SSH
- Slightly slower on repeated runs since everything is transferred every time
## What Gets Backed Up
### /etc/pve/ (pmxcfs snapshot)
The core Proxmox cluster filesystem. Transferred via `rsync -a` when available, otherwise tarballed and piped over SSH. Either way, runtime-only data is excluded:
| Included | Description |
|---|---|
| `datacenter.cfg` | Datacenter-wide settings |
| `storage.cfg` | Storage pool definitions |
| `user.cfg` | Users, groups, and permissions |
| `domains.cfg` | Authentication realms (LDAP, AD, etc.) |
| `vzdump.cfg` | Default backup job settings |
| `nodes/{node}/qemu-server/*.conf` | VM configuration files |
| `nodes/{node}/lxc/*.conf` | LXC container configuration files |
| `nodes/{node}/pve-ssl.pem` | Web UI SSL certificate |
| `nodes/{node}/pve-ssl.key` | Web UI SSL private key |
| `nodes/{node}/pveproxy-ssl.pem` | Custom SSL certificate (if set) |
| `nodes/{node}/pveproxy-ssl.key` | Custom SSL private key (if set) |
| `sdn/` | Software Defined Network configs (VLANs, VNets, subnets) |
| `ha/` | High Availability resources and groups |
| `firewall/` | Datacenter and node-level firewall rules |
| `priv/tfa.cfg` | TFA tokens (restore with caution) |
| `priv/shadow` | User password hashes |
| Excluded | Reason |
|---|---|
| `.rrd/` | Round-robin database (runtime stats, regenerated) |
| `priv/known_hosts` | SSH host keys (regenerated on boot) |
| `*.lock` | Runtime locks (ephemeral) |
### Network Configuration
| File | Description |
|---|---|
| `/etc/network/interfaces` | Bridge, bond, VLAN, and NIC definitions |
| `/etc/hostname` | System hostname |
| `/etc/hosts` | Static hostname resolution |
| `/etc/fstab` | Filesystem mount table (ZFS, NFS, CIFS mounts, etc.) |
| `/etc/resolv.conf` | DNS nameservers and search domains |
### APT Sources
| Path | Description |
|---|---|
| `/etc/apt/sources.list` | Main APT repository list |
| `/etc/apt/sources.list.d/` | Third-party and Proxmox enterprise/no-subscription repos |
| `/etc/apt/auth.conf` | APT authentication credentials |
### Cluster Configuration
| File | Description |
|---|---|
| `/etc/corosync/corosync.conf` | Corosync cluster ring and membership config |
### Ceph Storage
| File | Description |
|---|---|
| `/etc/ceph/ceph.conf` | Ceph cluster connection settings |
### System Tuning
| Path | Description |
|---|---|
| `/etc/sysctl.d/` | Kernel parameter overrides (network buffers, etc.) |
| `/etc/cron.d/` | System cron jobs |
| `/etc/cron.daily/` | Daily cron scripts |
### Package Manifest
| File | Contents |
|---|---|
| `packages.txt` | Full list of installed packages (`dpkg-query -W`) |
| `packages-manual.txt` | Manually installed packages (`apt-mark showmanual`) |
| `pveversion.txt` | Proxmox VE version string |
### Firewall Rules
| File | Description |
|---|---|
| `iptables.rules` | IPv4 iptables ruleset (via `iptables-save`) |
| `ip6tables.rules` | IPv6 ip6tables ruleset (via `ip6tables-save`) |
### Metadata (META file inside archive)
```
hostname: proxmox1
created: 2025-01-01T12:00:00Z
pveversion: pve-manager/8.2.4/123456789abc
pve_nodes: proxmox1 proxmox2 proxmox3
```
## Restore Process
1. Extracts the backup archive to a temporary directory
2. **Creates a rollback snapshot** of the current `/etc/pve` at `/var/tmp/pve-restore-rollback-{timestamp}/` (and also a tarball at `/etc/pve-rollback.tar.gz` on the remote node)
3. Restores `/etc/pve` by piping the archived tarball into `tar -x -C /etc/pve` on the target node
4. Restores network configs (`/etc/network/interfaces`, `hostname`, `hosts`, `resolv.conf`) by piping each file through SSH
5. Restores APT sources, sysctl, and cron overrides
6. Does **not** restore the package manifest — this is for reference only
After restore:
- **Reboot recommended** — network and hostname changes apply at boot
- **Restart `pveproxy`** if the web UI behaves oddly after `/etc/pve` restore
## SSH Non-Root Access (sudo)
If you connect with a non-root SSH user (`-u admin`), the script automatically detects this and prefixes all remote commands with `sudo -n`. The remote user must have passwordless sudo configured:
```bash
echo "admin ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/admin
```
If passwordless sudo is not available, the script exits with a clear error.
## Archive Format
Each backup is a gzipped tarball with this internal structure. The `/etc/pve/` data and sysconfig may be stored either as raw directories (when rsync was used) or as nested tarballs (when the fallback method was used):
```
pve-backup-proxmox1-20250101_120000.tar.gz
├── META # Metadata (hostname, date, version, nodes)
└── etc/
├── pve/
│ ├── datacenter.cfg # (raw files — rsync) or
│ └── pve-config.tar.gz # (nested tarball — fallback)
├── apt/
│ ├── sources.list # (raw files — rsync) or
│ └── apt-sources.tar.gz # (nested tarball — fallback)
└── system/
├── interfaces # /etc/network/interfaces
├── hostname # /etc/hostname
├── hosts # /etc/hosts
├── resolv.conf # /etc/resolv.conf
├── fstab # /etc/fstab
├── corosync.conf # /etc/corosync/corosync.conf
├── ceph.conf # /etc/ceph/ceph.conf
├── sysctl.d/ # (raw dir — rsync) or
├── sysconfig.tar.gz # (nested tarball — fallback)
├── packages.txt # Full package manifest
├── packages-manual.txt # Manually installed packages
├── pveversion.txt # pveversion output
├── iptables.rules # iptables-save output
└── ip6tables.rules # ip6tables-save output
```
The restore logic handles both layouts transparently.
## Verification
Every backup automatically generates a `.sha256` checksum file alongside the archive. You can verify integrity at any time:
```bash
# Verify the latest backup
./pve-backup.sh verify -f pve-backups/pve-backup-prox1-20250101_120000.tar.gz
```
The verify command checks:
1. The archive is a valid gzip'd tar file (no corruption)
2. The SHA256 checksum matches the stored `.sha256` file (no tampering)
If no `.sha256` file exists (e.g., from older backups), one is generated on the spot.
## Security Notes
- **Backup over SSH:** All data is transferred over the encrypted SSH channel. No data is stored on the remote host.
- **Sensitive data:** The archive contains SSL private keys, password hashes (`/etc/pve/priv/shadow`), and TFA tokens. Treat backup archives as sensitive credentials.
- **SSH keys:** Use `-i` to specify an SSH identity file. Agent forwarding is not required.
## Limitations
- Package manifest is for reference only — the script does not install/restore packages
- Firewall rules are exported via `iptables-save` — restoration requires manual `iptables-restore` (PVE firewall is handled via `/etc/pve/firewall/` in the pve-config archive)
- Ceph config is backed up, but the Ceph monitor maps and OSD tree are not
- VM/CT disk images and backups are **not** included — use the built-in Proxmox Backup Server or `vzdump` for those
## Future Enhancements (not yet implemented)
These are left as exercises for the reader or future development:
- **Retention / rotation** — `--keep N` flag to auto-prune backups older than N days (or keep only N most recent)
- **Cron / systemd timer** — drop-in unit for daily automated backups
- **Encryption** — GPG or `age` encrypt the archive before writing it to disk, decrypt on restore
- **Email / notification** — alert on backup failure or successful completion
- **Restore dry-run** — show what would be restored without actually pushing files
+541
View File
@@ -0,0 +1,541 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
BACKUP_DIR="$SCRIPT_DIR/pve-backups"
SSH_KEY=""
REMOTE_HOST=""
REMOTE_USER="root"
SUDO_CMD=""
HAS_RSYNC=0
SCRIPT_NAME=$(basename "$0")
usage() {
cat <<EOF
Usage: $SCRIPT_NAME <command> [options]
Commands:
backup Create a backup of Proxmox configuration
restore Restore Proxmox configuration from a backup
verify Verify a backup archive's integrity (SHA256)
list List available backups
Backup options:
-d, --dir DIR Local backup directory (default: $SCRIPT_DIR/pve-backups)
-h, --host HOST Remote Proxmox host (SSH)
-u, --user USER SSH user (default: root)
-i, --identity FILE SSH identity file
Restore / Verify options:
-d, --dir DIR Directory containing backup archive
-f, --file FILE Specific backup archive to restore/verify
-y, --yes Skip confirmation prompts (DANGEROUS)
List options:
-d, --dir DIR Directory to list backups from (default: $BACKUP_DIR)
Examples:
$SCRIPT_NAME backup
$SCRIPT_NAME backup -d /mnt/backup
$SCRIPT_NAME backup -h 192.168.1.100 -i ~/.ssh/id_rsa
$SCRIPT_NAME restore -f /mnt/backup/pve-backup-20250101_120000.tar.gz
$SCRIPT_NAME verify -f /mnt/backup/pve-backup-20250101_120000.tar.gz
$SCRIPT_NAME list
EOF
exit 1
}
log() { echo "[*] $*"; }
warn() { echo "[!] $*" >&2; }
die() { echo "[FATAL] $*" >&2; exit 1; }
# Parse common args (consume them, return remaining via ${PARSE_REMAINING[@]})
parse_common_args() {
local parsed=()
PARSE_REMAINING=()
while [[ $# -gt 0 ]]; do
case "$1" in
-d|--dir) BACKUP_DIR="$2"; shift 2 ;;
-h|--host) REMOTE_HOST="$2"; shift 2 ;;
-u|--user) REMOTE_USER="$2"; shift 2 ;;
-i|--identity) SSH_KEY="$2"; shift 2 ;;
-y|--yes) FORCE_YES=1; shift ;;
-f|--file) RESTORE_FILE="$2"; shift 2 ;;
--) shift; parsed+=("$@"); break ;;
-*)
PARSE_REMAINING=("$@")
return
;;
*) parsed+=("$1"); shift ;;
esac
done
PARSE_REMAINING=("${parsed[@]}")
}
build_ssh_cmd() {
local cmd=()
if [[ -n "$REMOTE_HOST" ]]; then
cmd=(ssh)
[[ -n "$SSH_KEY" ]] && cmd+=(-i "$SSH_KEY")
cmd+=("${REMOTE_USER}@${REMOTE_HOST}")
fi
echo "${cmd[@]}"
}
build_ssh_rsync_cmd() {
local cmd="ssh"
[[ -n "$SSH_KEY" ]] && cmd+=" -i $SSH_KEY"
echo "$cmd"
}
run_remote() {
local ssh_cmd
ssh_cmd=$(build_ssh_cmd)
if [[ -n "$ssh_cmd" ]]; then
$ssh_cmd $SUDO_CMD "$@"
else
${SUDO_CMD}bash -c "$*"
fi
}
run_remote_raw() {
local ssh_cmd
ssh_cmd=$(build_ssh_cmd)
if [[ -n "$ssh_cmd" ]]; then
$ssh_cmd "${SUDO_CMD}bash -s" <<< "$1"
else
${SUDO_CMD}bash -c "$1"
fi
}
check_proxmox() {
if [[ -z "$REMOTE_HOST" ]]; then
if [[ "$(id -u)" -ne 0 ]]; then
SUDO_CMD="sudo -n "
fi
elif [[ "$REMOTE_USER" != "root" ]]; then
SUDO_CMD="sudo -n "
fi
if ! run_remote "test -f /etc/pve/datacenter.cfg" 2>/dev/null; then
local where="${REMOTE_HOST:-locally}"
local who="${REMOTE_USER:-$(whoami)}"
if [[ -n "$SUDO_CMD" ]]; then
die "Cannot read /etc/pve $where — user '$who' needs passwordless sudo"
else
die "Cannot read /etc/pve $where — this script must be run as root"
fi
fi
}
check_rsync() {
HAS_RSYNC=0
if [[ -z "$REMOTE_HOST" ]]; then
return
fi
if run_remote "command -v rsync" &>/dev/null; then
HAS_RSYNC=1
log "rsync available on remote — using for incremental transfers"
else
log "rsync not found on remote — falling back to tar/cat transfer"
fi
}
# rsync wrapper: copies remote path(s) into dest dir.
# Handles SSH, sudo (via --rsync-path), and both file and directory sources.
# Prefix src with 'dir:' to treat it as a directory (--delete).
rsync_pull() {
local src="$1"
local dest="$2"
local is_dir=0
if [[ "$HAS_RSYNC" -ne 1 || -z "$REMOTE_HOST" ]]; then
return 1
fi
if [[ "$src" == dir:* ]]; then
is_dir=1
src="${src:4}"
fi
local rsync_ssh
rsync_ssh=$(build_ssh_rsync_cmd)
local rsync_opts=(-a)
[[ "$is_dir" -eq 1 ]] && rsync_opts+=(--delete)
[[ -n "$SUDO_CMD" ]] && rsync_opts+=(--rsync-path="${SUDO_CMD}rsync")
mkdir -p "$dest"
rsync "${rsync_opts[@]}" -e "$rsync_ssh" \
"${REMOTE_USER}@${REMOTE_HOST}:$src" "$dest" &>/dev/null
}
collect_pve_configs() {
local temp_dir="$1"
local dest="$temp_dir/etc"
mkdir -p "$dest/pve" "$dest/apt" "$dest/system"
# ---- /etc/pve ----
log "Backing up /etc/pve configuration..."
if rsync_pull "dir:/etc/pve/" "$dest/pve/" && [[ -f "$dest/pve/datacenter.cfg" ]]; then
# Clean up rsync'd noise
rm -rf "$dest/pve/.rrd" "$dest/pve/nodes/"*/qemu-server/*.lock \
"$dest/pve/nodes/"*/lxc/*.lock "$dest/pve/priv/known_hosts" 2>/dev/null || true
else
# Fallback: tar-based snapshot via SSH
rm -rf "$dest/pve" 2>/dev/null; mkdir "$dest/pve"
run_remote_raw '
set -euo pipefail
tmp=$(mktemp -d)
mkdir "$tmp/pve"
cd /etc/pve
tar cf - \
--exclude="./nodes/*/qemu-server/*.lock" \
--exclude="./nodes/*/lxc/*.lock" \
--exclude="./priv/known_hosts" \
--exclude=".rrd" \
. 2>/dev/null | tar xf - -C "$tmp/pve"
tar czf - -C "$tmp" pve
rm -rf "$tmp"
' > "$dest/pve/pve-config.tar.gz" 2>/dev/null
fi
# ---- Network & system files ----
log "Backing up network and system configs..."
local sysfiles=(
/etc/network/interfaces /etc/hostname /etc/hosts
/etc/resolv.conf /etc/mailname /etc/fstab
)
local rsync_ok=1
for f in "${sysfiles[@]}"; do
if ! rsync_pull "$f" "$dest/system/"; then
rsync_ok=0
break
fi
done
if [[ "$rsync_ok" -eq 0 ]]; then
for f in "${sysfiles[@]}"; do
run_remote "cat $f 2>/dev/null" > "$dest/system/$(basename "$f")" 2>/dev/null || true
done
fi
# ---- APT sources ----
log "Backing up APT sources..."
if ! rsync_pull "dir:/etc/apt/sources.list.d/" "$dest/apt/" || \
! rsync_pull "/etc/apt/sources.list" "$dest/apt/" || \
! rsync_pull "/etc/apt/auth.conf" "$dest/apt/"; then
# Fallback
rm -rf "$dest/apt" 2>/dev/null; mkdir "$dest/apt"
run_remote_raw '
set -euo pipefail
tmp=$(mktemp -d)
mkdir "$tmp/apt"
cp -a /etc/apt/sources.list "$tmp/apt/" 2>/dev/null || true
cp -a /etc/apt/sources.list.d "$tmp/apt/" 2>/dev/null || true
cp -a /etc/apt/auth.conf "$tmp/apt/" 2>/dev/null || true
tar czf - -C "$tmp" apt
rm -rf "$tmp"
' > "$dest/apt/apt-sources.tar.gz" 2>/dev/null
fi
# ---- Corosync ----
log "Backing up corosync config..."
if ! rsync_pull "/etc/corosync/corosync.conf" "$dest/system/"; then
run_remote "cat /etc/corosync/corosync.conf 2>/dev/null" > "$dest/system/corosync.conf" 2>/dev/null || true
fi
# ---- Ceph ----
log "Backing up Ceph config..."
if ! rsync_pull "/etc/ceph/ceph.conf" "$dest/system/"; then
run_remote "cat /etc/ceph/ceph.conf 2>/dev/null" > "$dest/system/ceph.conf" 2>/dev/null || true
fi
# ---- sysctl / cron ----
log "Backing up sysctl and cron..."
if ! rsync_pull "dir:/etc/sysctl.d/" "$dest/system/sysctl.d/" || \
! rsync_pull "dir:/etc/cron.d/" "$dest/system/cron.d/" || \
! rsync_pull "dir:/etc/cron.daily/" "$dest/system/cron.daily/"; then
# Fallback
rm -rf "$dest/system/sysctl.d" "$dest/system/cron.d" "$dest/system/cron.daily" 2>/dev/null
run_remote_raw '
tmp=$(mktemp -d)
mkdir "$tmp/etc"
cp -a /etc/sysctl.d "$tmp/etc/" 2>/dev/null || true
cp -a /etc/cron.d "$tmp/etc/" 2>/dev/null || true
cp -a /etc/cron.daily "$tmp/etc/" 2>/dev/null || true
tar czf - -C "$tmp" etc
rm -rf "$tmp"
' > "$dest/system/sysconfig.tar.gz" 2>/dev/null
fi
# ---- Package manifest (always via command) ----
log "Recording package manifest..."
run_remote "dpkg-query -W 2>/dev/null || true" > "$dest/system/packages.txt" 2>/dev/null || true
run_remote "apt-mark showmanual 2>/dev/null || true" > "$dest/system/packages-manual.txt" 2>/dev/null || true
run_remote "pveversion 2>/dev/null || true" > "$dest/system/pveversion.txt" 2>/dev/null || true
# ---- Firewall rules (always via command) ----
log "Backing up firewall rules..."
run_remote "iptables-save 2>/dev/null || true" > "$dest/system/iptables.rules" 2>/dev/null || true
run_remote "ip6tables-save 2>/dev/null || true" > "$dest/system/ip6tables.rules" 2>/dev/null || true
log "Config collection complete."
}
do_backup() {
check_proxmox
check_rsync
local stamp=$(run_remote "date +%Y%m%d_%H%M%S 2>/dev/null || date +%Y%m%d_%H%M%S")
local hostname=$(run_remote "hostname -s 2>/dev/null || echo unknown")
local temp_dir=$(mktemp -d)
local archive_name="pve-backup-${hostname}-${stamp}.tar.gz"
local archive_path="$BACKUP_DIR/$archive_name"
mkdir -p "$BACKUP_DIR" "$temp_dir"
collect_pve_configs "$temp_dir"
# Add metadata
cat > "$temp_dir/META" <<EOM
hostname: $hostname
created: $(date -u +%Y-%m-%dT%H:%M:%SZ)
pveversion: $(run_remote "pveversion 2>/dev/null || echo unknown")
pve_nodes: $(run_remote 'ls /etc/pve/nodes/ 2>/dev/null | tr "\n" " "' 2>/dev/null)
EOM
tar czf "$archive_path" -C "$temp_dir" $(ls -A "$temp_dir")
rm -rf "$temp_dir"
log "Backup written to: $archive_path"
echo " Size: $(du -h "$archive_path" | cut -f1)"
# Generate and store SHA256 checksum
(cd "$BACKUP_DIR" && sha256sum "$archive_name" > "$archive_path.sha256")
log "Checksum: $(cat "$archive_path.sha256")"
}
# Remote rollback + restore of /etc/pve via rsync or tar pipe.
# Handles both rsync'd directories and tarball archives.
_restore_pve() {
local src_dir="$1"
local ssh_cmd
ssh_cmd=$(build_ssh_cmd)
run_remote "tar czf /etc/pve-rollback.tar.gz -C /etc/pve . 2>/dev/null" 2>/dev/null || true
if [[ -f "$src_dir/pve-config.tar.gz" ]]; then
log " Restoring /etc/pve (from tarball)..."
run_remote "tar xzf - -C /etc/pve" < "$src_dir/pve-config.tar.gz" 2>/dev/null || \
warn "Failed to restore some /etc/pve files"
elif [[ -f "$src_dir/datacenter.cfg" ]]; then
log " Restoring /etc/pve (from directory)..."
local tried_rsync=0
if [[ -n "$ssh_cmd" ]] && command -v rsync &>/dev/null && run_remote "command -v rsync" &>/dev/null; then
local rsync_ssh
rsync_ssh=$(build_ssh_rsync_cmd)
local rsync_opts=(-a --delete)
[[ -n "$SUDO_CMD" ]] && rsync_opts+=(--rsync-path="${SUDO_CMD}rsync")
rsync "${rsync_opts[@]}" -e "$rsync_ssh" "$src_dir/" "${REMOTE_USER}@${REMOTE_HOST}:/etc/pve/" &>/dev/null && \
tried_rsync=1
fi
if [[ "$tried_rsync" -eq 0 ]]; then
tar czf - -C "$src_dir" . | run_remote "tar xzf - -C /etc/pve" 2>/dev/null || \
warn "Failed to restore /etc/pve files"
fi
fi
}
_restore_dir() {
local src_dir="$1" remote_path="$2" label="$3"
if [[ ! -d "$src_dir" ]] || ! ls -A "$src_dir" &>/dev/null; then
return
fi
log " Restoring $label..."
local ssh_cmd
ssh_cmd=$(build_ssh_cmd)
local tried_rsync=0
if [[ -n "$ssh_cmd" ]] && command -v rsync &>/dev/null && run_remote "command -v rsync" &>/dev/null; then
local rsync_ssh
rsync_ssh=$(build_ssh_rsync_cmd)
local rsync_opts=(-a)
[[ -n "$SUDO_CMD" ]] && rsync_opts+=(--rsync-path="${SUDO_CMD}rsync")
rsync "${rsync_opts[@]}" -e "$rsync_ssh" "$src_dir/" "${REMOTE_USER}@${REMOTE_HOST}:$remote_path" &>/dev/null && \
tried_rsync=1
fi
if [[ "$tried_rsync" -eq 0 ]]; then
tar czf - -C "$src_dir" . | run_remote "tar xzf - -C $remote_path" 2>/dev/null || \
warn "Failed to restore $label"
fi
}
do_restore() {
check_proxmox
local archive_file=""
if [[ -n "${RESTORE_FILE:-}" ]]; then
archive_file="$RESTORE_FILE"
else
local latest
latest=$(ls -t "$BACKUP_DIR"/pve-backup-*.tar.gz 2>/dev/null | head -1) || true
if [[ -z "$latest" ]]; then
die "No backup archives found in $BACKUP_DIR and no --file specified"
fi
archive_file="$latest"
fi
if [[ ! -f "$archive_file" ]]; then
die "Backup file not found: $archive_file"
fi
log "Restoring from: $archive_file"
if [[ "${FORCE_YES:-0}" -ne 1 ]]; then
echo ""
warn "WARNING: This will overwrite Proxmox configuration files!"
warn "Your current configs will be backed up to /var/tmp/pve-restore-rollback/"
echo ""
read -r -p "Are you sure you want to proceed? [y/N] " reply
if [[ ! "$reply" =~ ^[Yy]$ ]]; then
die "Restore cancelled."
fi
fi
local rollback_dir="/var/tmp/pve-restore-rollback-$(date +%Y%m%d_%H%M%S)"
log "Creating rollback snapshot at $rollback_dir..."
mkdir -p "$rollback_dir"
local temp_dir
temp_dir=$(mktemp -d)
tar xzf "$archive_file" -C "$temp_dir"
# Restore /etc/pve
_restore_pve "$temp_dir/etc/pve"
# Restore individual system files
for f in "$temp_dir"/etc/system/*; do
local basename_f
basename_f=$(basename "$f")
[[ -f "$f" ]] || continue
local target=""
case "$basename_f" in
interfaces) target="/etc/network/interfaces" ;;
hostname) target="/etc/hostname" ;;
hosts) target="/etc/hosts" ;;
resolv.conf) target="/etc/resolv.conf" ;;
fstab) target="/etc/fstab" ;;
corosync.conf) target="/etc/corosync/corosync.conf" ;;
ceph.conf) target="/etc/ceph/ceph.conf" ;;
mailname) target="/etc/mailname" ;;
*) continue ;;
esac
if [[ -s "$f" ]]; then
run_remote "mkdir -p $(dirname "$target") && cat > $target" < "$f" 2>/dev/null || warn "Failed to restore $target"
fi
done
# Restore APT sources
if [[ -f "$temp_dir/etc/apt/apt-sources.tar.gz" ]]; then
log "Restoring APT sources..."
run_remote "tar xzf - -C /" < "$temp_dir/etc/apt/apt-sources.tar.gz" 2>/dev/null || warn "Failed to restore APT sources"
elif [[ -d "$temp_dir/etc/apt" ]]; then
_restore_dir "$temp_dir/etc/apt" "/etc/apt" "APT sources"
fi
# Restore sysconfig
if [[ -f "$temp_dir/etc/system/sysconfig.tar.gz" ]]; then
log "Restoring sysctl/cron configs..."
run_remote "tar xzf - -C /" < "$temp_dir/etc/system/sysconfig.tar.gz" 2>/dev/null || warn "Failed to restore sysconfig"
elif [[ -d "$temp_dir/etc/system/sysctl.d" ]]; then
_restore_dir "$temp_dir/etc/system/sysctl.d" "/etc/sysctl.d" "sysctl"
_restore_dir "$temp_dir/etc/system/cron.d" "/etc/cron.d" "cron"
_restore_dir "$temp_dir/etc/system/cron.daily" "/etc/cron.daily" "cron.daily"
fi
rm -rf "$temp_dir"
log "Restore complete!"
log "Rollback snapshot: $rollback_dir"
echo ""
warn "NOTE: Some changes (e.g., network, hostname) require a reboot to take full effect."
warn "NOTE: Restored /etc/pve files sync via pmxcfs — restart pveproxy if web UI acts odd."
}
do_verify() {
local archive_file=""
if [[ -n "${RESTORE_FILE:-}" ]]; then
archive_file="$RESTORE_FILE"
else
die "Specify an archive with -f/--file"
fi
if [[ ! -f "$archive_file" ]]; then
die "File not found: $archive_file"
fi
local checksum_file="${archive_file}.sha256"
log "Verifying: $archive_file"
# Check tar integrity
if tar tzf "$archive_file" &>/dev/null; then
log " Archive integrity: OK (valid tar.gz)"
else
die " Archive integrity: FAILED — corrupt or truncated"
fi
# Check SHA256 if available
local archive_dir archive_basename
archive_dir=$(dirname "$archive_file")
archive_basename=$(basename "$archive_file")
if [[ -f "$checksum_file" ]]; then
if cd "$archive_dir" && sha256sum -c "$archive_basename.sha256" &>/dev/null; then
cd "$OLDPWD"
log " SHA256 checksum: OK (matches stored checksum)"
else
die " SHA256 checksum: FAILED — archive modified or corrupted"
fi
else
# Generate and save checksum if none exists
warn " SHA256 checksum: none found — generating now"
(cd "$archive_dir" && sha256sum "$archive_basename" > "$archive_basename.sha256")
log " Checksum saved to: $checksum_file"
fi
}
do_list() {
ls -lh "$BACKUP_DIR"/pve-backup-*.tar.gz 2>/dev/null || echo "No backups found in $BACKUP_DIR"
}
# ---- Main ----
COMMAND="${1:-}"
[[ -z "$COMMAND" ]] && usage
shift
FORCE_YES=0
RESTORE_FILE=""
parse_common_args "$@"
set -- "${PARSE_REMAINING[@]}"
case "$COMMAND" in
backup)
do_backup
;;
restore)
do_restore
;;
verify)
do_verify
;;
list)
do_list
;;
*)
usage
;;
esac