Add proxmox-snapshot-rotation.sh
This commit is contained in:
parent
1b155193f8
commit
3aec57a08c
1 changed files with 260 additions and 0 deletions
260
proxmox-snapshot-rotation.sh
Normal file
260
proxmox-snapshot-rotation.sh
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script Configuration
|
||||||
|
readonly SCRIPT_NAME=$(basename "$0")
|
||||||
|
readonly LOG_DIR="/var/log/proxmox-snapshots"
|
||||||
|
readonly LOG_FILE="${LOG_DIR}/${SCRIPT_NAME%.*}.log"
|
||||||
|
readonly LOCK_FILE="/var/run/${SCRIPT_NAME%.*}.lock"
|
||||||
|
readonly MAX_LOG_SIZE_MB=50
|
||||||
|
readonly MAX_LOG_FILES=5
|
||||||
|
|
||||||
|
# Variables
|
||||||
|
DATE=$(date +"%Y%m%d%H")
|
||||||
|
DEFAULT_KEEP=24 # Default number of snapshots to keep
|
||||||
|
DEFAULT_RETAIN_DAYS=7 # Default days to keep LVM archive files
|
||||||
|
DRY_RUN=false
|
||||||
|
QUIET=false
|
||||||
|
KEEP=$DEFAULT_KEEP
|
||||||
|
RETAIN_DAYS=$DEFAULT_RETAIN_DAYS
|
||||||
|
VERBOSE=false
|
||||||
|
|
||||||
|
# Error codes
|
||||||
|
readonly E_LOCK=200
|
||||||
|
readonly E_PERMISSIONS=201
|
||||||
|
readonly E_INVALID_ARG=202
|
||||||
|
|
||||||
|
# Logging functions
|
||||||
|
setup_logging() {
|
||||||
|
# Create log directory if it doesn't exist
|
||||||
|
if [[ ! -d "$LOG_DIR" ]]; then
|
||||||
|
mkdir -p "$LOG_DIR" || {
|
||||||
|
echo "ERROR: Unable to create log directory: $LOG_DIR"
|
||||||
|
exit $E_PERMISSIONS
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rotate logs if main log file exceeds max size
|
||||||
|
if [[ -f "$LOG_FILE" ]]; then
|
||||||
|
local size_mb=$(du -m "$LOG_FILE" | cut -f1)
|
||||||
|
if (( size_mb >= MAX_LOG_SIZE_MB )); then
|
||||||
|
for ((i=MAX_LOG_FILES-1; i>=1; i--)); do
|
||||||
|
[[ -f "${LOG_FILE}.$i" ]] && mv "${LOG_FILE}.$i" "${LOG_FILE}.$((i+1))"
|
||||||
|
done
|
||||||
|
mv "$LOG_FILE" "${LOG_FILE}.1"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure log file exists and is writable
|
||||||
|
touch "$LOG_FILE" 2>/dev/null || {
|
||||||
|
echo "ERROR: Unable to create/access log file: $LOG_FILE"
|
||||||
|
exit $E_PERMISSIONS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
local level=$1
|
||||||
|
shift
|
||||||
|
local message="[$(date +'%Y-%m-%d %H:%M:%S')] [$level] $*"
|
||||||
|
|
||||||
|
if [[ "$QUIET" == "false" || "$level" == "ERROR" ]]; then
|
||||||
|
echo "$message"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == "false" ]]; then
|
||||||
|
echo "$message" >> "$LOG_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Help function
|
||||||
|
show_help() {
|
||||||
|
cat << EOF
|
||||||
|
Usage: $SCRIPT_NAME [OPTIONS]
|
||||||
|
Automates the creation and rotation of Proxmox VM snapshots.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-n, --dry-run Show what would be done without making changes
|
||||||
|
-q, --quiet Suppress output (except errors)
|
||||||
|
-v, --verbose Enable verbose logging
|
||||||
|
-k, --keep NUM Number of snapshots to keep (default: $DEFAULT_KEEP)
|
||||||
|
--retain-days NUM Days to keep LVM archives (default: $DEFAULT_RETAIN_DAYS)
|
||||||
|
-h, --help Show this help message
|
||||||
|
|
||||||
|
Example:
|
||||||
|
$SCRIPT_NAME --keep 48 --retain-days 14
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parameter parsing with validation
|
||||||
|
parse_parameters() {
|
||||||
|
while [[ "$#" -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-n|--dry-run) DRY_RUN=true ;;
|
||||||
|
-q|--quiet) QUIET=true ;;
|
||||||
|
-v|--verbose) VERBOSE=true ;;
|
||||||
|
-k|--keep)
|
||||||
|
if [[ ! $2 =~ ^[0-9]+$ ]]; then
|
||||||
|
log "ERROR" "Invalid value for --keep: $2"
|
||||||
|
exit $E_INVALID_ARG
|
||||||
|
fi
|
||||||
|
KEEP="$2"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--retain-days)
|
||||||
|
if [[ ! $2 =~ ^[0-9]+$ ]]; then
|
||||||
|
log "ERROR" "Invalid value for --retain-days: $2"
|
||||||
|
exit $E_INVALID_ARG
|
||||||
|
fi
|
||||||
|
RETAIN_DAYS="$2"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help) show_help ;;
|
||||||
|
*)
|
||||||
|
log "ERROR" "Unknown option: $1"
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Lock management
|
||||||
|
create_lock() {
|
||||||
|
if [[ -e "$LOCK_FILE" ]]; then
|
||||||
|
local pid=$(cat "$LOCK_FILE")
|
||||||
|
if kill -0 "$pid" 2>/dev/null; then
|
||||||
|
log "ERROR" "Script is already running with PID $pid"
|
||||||
|
exit $E_LOCK
|
||||||
|
else
|
||||||
|
log "WARN" "Removing stale lock file"
|
||||||
|
rm -f "$LOCK_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo $$ > "$LOCK_FILE"
|
||||||
|
trap 'rm -f "$LOCK_FILE"' EXIT
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to delete old LVM archive files
|
||||||
|
delete_old_lvm_archives() {
|
||||||
|
local archive_path="/etc/lvm/archive"
|
||||||
|
if [[ -d "$archive_path" ]]; then
|
||||||
|
log "INFO" "Cleaning LVM archive files older than $RETAIN_DAYS days..."
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
find "$archive_path" -type f -mtime +"$RETAIN_DAYS" -print
|
||||||
|
else
|
||||||
|
local count=$(find "$archive_path" -type f -mtime +"$RETAIN_DAYS" -exec rm {} + -print | wc -l)
|
||||||
|
log "INFO" "Removed $count old LVM archive files"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "WARN" "LVM archive directory not found: $archive_path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to handle snapshot creation
|
||||||
|
create_snapshot() {
|
||||||
|
local type=$1
|
||||||
|
local vmid=$2
|
||||||
|
local snap_cmd=$3
|
||||||
|
local snapshot_name="auto_$DATE"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
log "INFO" "[Dry-Run] Would create snapshot: $snap_cmd snapshot $vmid $snapshot_name"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "INFO" "Creating snapshot for $type $vmid: $snapshot_name"
|
||||||
|
if ! $snap_cmd snapshot "$vmid" "$snapshot_name" 2>/tmp/snap_error.$$; then
|
||||||
|
local error=$(cat /tmp/snap_error.$$)
|
||||||
|
log "ERROR" "Failed to create snapshot for $type $vmid: $error"
|
||||||
|
rm -f /tmp/snap_error.$$
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
rm -f /tmp/snap_error.$$
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to handle snapshot deletion
|
||||||
|
delete_snapshot() {
|
||||||
|
local type=$1
|
||||||
|
local vmid=$2
|
||||||
|
local snap_cmd=$3
|
||||||
|
local snapshot_name=$4
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
log "INFO" "[Dry-Run] Would delete snapshot: $snap_cmd delsnapshot $vmid $snapshot_name"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "INFO" "Deleting snapshot for $type $vmid: $snapshot_name"
|
||||||
|
if ! $snap_cmd delsnapshot "$vmid" "$snapshot_name" 2>/tmp/snap_error.$$; then
|
||||||
|
local error=$(cat /tmp/snap_error.$$)
|
||||||
|
log "ERROR" "Failed to delete snapshot for $type $vmid: $error"
|
||||||
|
rm -f /tmp/snap_error.$$
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
rm -f /tmp/snap_error.$$
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main function
|
||||||
|
main() {
|
||||||
|
setup_logging
|
||||||
|
create_lock
|
||||||
|
parse_parameters "$@"
|
||||||
|
|
||||||
|
log "INFO" "Starting snapshot management (Keep: $KEEP, Retain days: $RETAIN_DAYS)"
|
||||||
|
[[ "$DRY_RUN" == true ]] && log "INFO" "Running in DRY-RUN mode"
|
||||||
|
|
||||||
|
# Clean old LVM archives first
|
||||||
|
delete_old_lvm_archives
|
||||||
|
|
||||||
|
# Fetch and process VM list
|
||||||
|
log "INFO" "Fetching VM list from pvesh..."
|
||||||
|
local vm_list
|
||||||
|
if ! vm_list=$(pvesh get /cluster/resources --type vm --output-format text --human-readable 0 --noborder --noheader 2>/tmp/pvesh_error.$$); then
|
||||||
|
local error=$(cat /tmp/pvesh_error.$$)
|
||||||
|
log "ERROR" "Failed to fetch VM list: $error"
|
||||||
|
rm -f /tmp/pvesh_error.$$
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rm -f /tmp/pvesh_error.$$
|
||||||
|
|
||||||
|
echo "$vm_list" | awk '{split($1, a, "/"); TYPE=a[1]; VMID=a[2]; STATUS=$(NF-2); print TYPE, VMID, STATUS}' | \
|
||||||
|
while read -r TYPE VMID STATUS; do
|
||||||
|
if [[ "$STATUS" != "running" ]]; then
|
||||||
|
[[ "$VERBOSE" == true ]] && log "INFO" "Skipping $TYPE $VMID - Status: $STATUS"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine snapshot command
|
||||||
|
local SNAP_CMD
|
||||||
|
case "$TYPE" in
|
||||||
|
lxc) SNAP_CMD="pct" ;;
|
||||||
|
qemu) SNAP_CMD="qm" ;;
|
||||||
|
*)
|
||||||
|
log "WARN" "Unknown VM type: $TYPE for VMID $VMID"
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Create new snapshot
|
||||||
|
create_snapshot "$TYPE" "$VMID" "$SNAP_CMD" || continue
|
||||||
|
|
||||||
|
# Manage snapshot rotation
|
||||||
|
local SNAPSHOTS
|
||||||
|
SNAPSHOTS=($($SNAP_CMD listsnapshot "$VMID" | grep -oP 'auto_\d{10}' | sort -r))
|
||||||
|
local SNAP_COUNT=${#SNAPSHOTS[@]}
|
||||||
|
|
||||||
|
if (( SNAP_COUNT > KEEP )); then
|
||||||
|
log "INFO" "Found $SNAP_COUNT snapshots for $TYPE $VMID, keeping $KEEP"
|
||||||
|
for (( i=KEEP; i<SNAP_COUNT; i++ )); do
|
||||||
|
delete_snapshot "$TYPE" "$VMID" "$SNAP_CMD" "${SNAPSHOTS[$i]}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
log "INFO" "Snapshot management completed successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Execute main function with all arguments
|
||||||
|
main "$@"
|
Loading…
Reference in a new issue