@Danp Thanks! Finally I was able to get my script working. Also some contribution to "GROK" who assisted quite well!
It allows a VM to Snapshot itself, before running automated maintenance over cron. And it allows minimalistic retention of the snapshots created this way.
Happy to share it with the community:
#!/bin/bash
: <<'END'
Snapshot Management Script for Xen Orchestra
Usage: ./snapshot-vm.sh [OPTIONS]
Options:
-v Enable verbose output (detailed logging)
-s Silent mode (suppress all non-error output)
-r <number> Set retention limit (delete oldest snapshots beyond this number)
-n No-snapshot mode (skip snapshot creation, only manage retention)
Examples:
./snapshot-vm.sh # Create a snapshot, no retention
./snapshot-vm.sh -v -r 3 # Create a snapshot, keep latest 3, verbose
./snapshot-vm.sh -r 5 -n # Skip snapshot, keep latest 5
./snapshot-vm.sh -v -s -r 2 # Create a snapshot, keep latest 2, verbose but silent
Configuration:
Edit XO_URL, TOKEN, and SNAPSHOT_PREFIX at the top of the script.
END
# Configuration variables
XO_URL="http://your-xen-orchestra-server"
TOKEN="your-authentication-token"
VM_UUID=$(dmidecode -s system-uuid | tr '[:upper:]' '[:lower:]')
SNAPSHOT_PREFIX="AutoMaintenance" # Prefix for snapshot names
# Default retention (no deletion if not specified)
RETENTION=-1
# Flags
VERBOSE=0
SILENT=0
NOSNAPSHOT=0
# Parse command-line options
while getopts "vsr:n" opt; do
case $opt in
v) VERBOSE=1;;
s) SILENT=1;;
r) RETENTION="$OPTARG";;
n) NOSNAPSHOT=1;;
esac
done
# Function to print messages based on mode
print_msg() {
if [ $SILENT -eq 0 ]; then
if [ $VERBOSE -eq 1 ]; then
echo "$1"
else
echo -e "$1"
fi
fi
}
# Check if we got the UUID
if [ -z "$VM_UUID" ]; then
echo "Error: Could not retrieve system UUID"
exit 1
fi
# Function to check if jq is installed
check_jq() {
if ! command -v jq &> /dev/null; then
echo "Error: jq is required but not installed"
exit 1
fi
}
# Function to get VM name
get_vm_name() {
local vm_info=$(curl -s -X GET \
-b "authenticationToken=$TOKEN" \
-H "Accept: application/json" \
"$XO_URL/rest/v0/vms/$VM_UUID")
vm_name=$(echo "$vm_info" | jq -r '.name_label // "Unknown"')
}
# Function to trigger snapshot
trigger_snapshot() {
local SNAPSHOT_NAME="${SNAPSHOT_PREFIX}-$(date +%Y%m%d-%H%M%S)"
if [ $VERBOSE -eq 1 ]; then
local curl_cmd="curl -s -X POST \
-b \"authenticationToken=$TOKEN\" \
-H \"Content-Type: application/json\" \
-H \"Accept: application/json\" \
-d '{\"name_label\": \"$SNAPSHOT_NAME\"}' \
\"$XO_URL/rest/v0/vms/$VM_UUID/actions/snapshot\""
print_msg "Executing snapshot trigger command:"
print_msg "$curl_cmd"
print_msg "-----"
fi
response=$(curl -s -X POST \
-b "authenticationToken=$TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "{\"name_label\": \"$SNAPSHOT_NAME\"}" \
"$XO_URL/rest/v0/vms/$VM_UUID/actions/snapshot")
if [ $VERBOSE -eq 1 ]; then
print_msg "Snapshot trigger response: $response"
print_msg "-----"
fi
if [[ "$response" =~ /rest/v0/tasks/([a-z0-9]+) ]]; then
task_id="${BASH_REMATCH[1]}"
if [ $VERBOSE -eq 1 ]; then
print_msg "Snapshot triggered (Task ID: $task_id)"
fi
return 0
elif [[ "$response" =~ ^[a-z0-9]+$ ]]; then
task_id="$response"
if [ $VERBOSE -eq 1 ]; then
print_msg "Snapshot triggered (Task ID: $task_id)"
fi
return 0
else
print_msg "Error: Failed to get task ID. Response: $response"
exit 1
fi
}
# Function to monitor task status
monitor_task() {
local task_id=$1
local max_attempts=30
local attempt=1
if [ $VERBOSE -eq 1 ]; then
print_msg "Monitoring task $task_id..."
fi
while [ $attempt -le $max_attempts ]; do
status_response=$(curl -s -X GET \
-b "authenticationToken=$TOKEN" \
-H "Accept: application/json" \
"$XO_URL/rest/v0/tasks/$task_id")
if [ $VERBOSE -eq 1 ]; then
print_msg "Attempt $attempt - Raw response: $status_response"
fi
if [ -n "$status_response" ] && echo "$status_response" | jq -e . >/dev/null 2>&1; then
status=$(echo "$status_response" | jq -r '.status')
if [ $VERBOSE -eq 1 ]; then
print_msg "Status: $status"
else
print_msg "Taking Snapshot of VM: $vm_name... Status: $status"
fi
case "$status" in
"pending"|"running")
if [ $VERBOSE -eq 1 ]; then
print_msg "Task still in progress"
fi
sleep 5
((attempt++))
;;
"success")
if [ $VERBOSE -eq 1 ]; then
print_msg "Snapshot completed successfully"
else
print_msg "Taking Snapshot of VM: $vm_name... Status: Completed"
fi
return 0
;;
"failure")
error=$(echo "$status_response" | jq -r '.result.message // "Unknown error"')
print_msg "Task failed: $error"
exit 1
;;
*)
if [ $VERBOSE -eq 1 ]; then
print_msg "Unknown status: $status"
fi
sleep 5
((attempt++))
;;
esac
else
if [ $VERBOSE -eq 1 ]; then
print_msg "Invalid or empty response"
fi
sleep 5
((attempt++))
fi
done
print_msg "Timeout waiting for task completion"
exit 1
}
# Function to manage snapshot retention
manage_retention() {
if [ $RETENTION -lt 0 ]; then
print_msg "Retention not specified, skipping cleanup"
return 0
fi
# Get VM data with snapshot UUIDs
vm_data=$(curl -s -X GET \
-b "authenticationToken=$TOKEN" \
-H "Accept: application/json" \
"$XO_URL/rest/v0/vms/$VM_UUID")
if [ $VERBOSE -eq 1 ]; then
print_msg "Raw VM data response: $vm_data"
print_msg "-----"
fi
if ! echo "$vm_data" | jq -e . >/dev/null 2>&1; then
print_msg "Error: Invalid JSON response from VM endpoint"
print_msg "Response: $vm_data"
exit 1
fi
# Extract snapshot UUIDs
snapshot_uuids=$(echo "$vm_data" | jq -r '.snapshots[]')
# Fetch details for each snapshot, verify existence, and filter by prefix
auto_snapshots=""
for uuid in $snapshot_uuids; do
# Check existence with a HEAD request to avoid full body download
http_status=$(curl -s -o /dev/null -w "%{http_code}" -I \
-b "authenticationToken=$TOKEN" \
"$XO_URL/rest/v0/vm-snapshots/$uuid")
if [ "$http_status" -eq 404 ]; then
if [ $VERBOSE -eq 1 ]; then
print_msg "Snapshot $uuid does not exist (HTTP 404), skipping"
print_msg "-----"
fi
continue
elif [ "$http_status" -ne 200 ]; then
if [ $VERBOSE -eq 1 ]; then
print_msg "Unexpected HTTP status $http_status for snapshot $uuid, skipping"
print_msg "-----"
fi
continue
fi
# Fetch full snapshot data if it exists
snapshot_data=$(curl -s -X GET \
-b "authenticationToken=$TOKEN" \
-H "Accept: application/json" \
"$XO_URL/rest/v0/vm-snapshots/$uuid")
if [ $VERBOSE -eq 1 ]; then
print_msg "Snapshot $uuid data: $snapshot_data"
print_msg "-----"
fi
# Verify it’s a valid JSON response
if ! echo "$snapshot_data" | jq -e . >/dev/null 2>&1; then
print_msg "Error: Invalid JSON response for snapshot $uuid"
print_msg "Response: $snapshot_data"
continue
fi
# Filter for snapshots with the specified prefix
if echo "$snapshot_data" | jq -e ".name_label | startswith(\"$SNAPSHOT_PREFIX\")" >/dev/null 2>&1; then
snapshot_line=$(echo "$snapshot_data" | jq -r '[.id, .name_label, .snapshot_time] | join("\t")')
auto_snapshots="$auto_snapshots$snapshot_line\n"
fi
done
# Sort by snapshot_time
auto_snapshots=$(echo -e "$auto_snapshots" | sort -k3 -n | grep -v '^$')
if [ $VERBOSE -eq 1 ]; then
print_msg "Filtered and sorted $SNAPSHOT_PREFIX snapshots:"
print_msg "$auto_snapshots"
print_msg "-----"
fi
# Count current snapshots
snapshot_count=$(echo "$auto_snapshots" | wc -l)
print_msg "Current $SNAPSHOT_PREFIX snapshot count: $snapshot_count"
# If over retention limit, delete oldest
if [ $snapshot_count -gt $RETENTION ]; then
excess=$((snapshot_count - RETENTION))
print_msg "Excess snapshots to delete: $excess"
# Get IDs of oldest snapshots to delete
delete_ids=$(echo "$auto_snapshots" | head -n $excess | cut -f1)
for id in $delete_ids; do
if [ $VERBOSE -eq 1 ]; then
print_msg "Deleting snapshot $id"
print_msg "Delete command: curl -s -X DELETE -b \"authenticationToken=$TOKEN\" \"$XO_URL/rest/v0/vm-snapshots/$id\""
fi
delete_response=$(curl -s -X DELETE \
-b "authenticationToken=$TOKEN" \
"$XO_URL/rest/v0/vm-snapshots/$id")
if [ $VERBOSE -eq 1 ]; then
print_msg "Delete response: $delete_response"
print_msg "-----"
fi
# Treat empty response, {"status": "success"}, or "OK" as success
if [ -z "$delete_response" ] || echo "$delete_response" | jq -e '.status == "success"' >/dev/null 2>&1 || [ "$delete_response" = "OK" ]; then
print_msg "Successfully deleted snapshot $id"
else
print_msg "Warning: Failed to delete snapshot $id. Response: $delete_response"
fi
done
else
if [ $VERBOSE -eq 1 ]; then
print_msg "No excess snapshots to delete"
fi
fi
}
# Main execution
check_jq
get_vm_name
if [ $VERBOSE -eq 1 ]; then
print_msg "Using VM UUID: $VM_UUID"
fi
if [ $NOSNAPSHOT -eq 0 ]; then
trigger_snapshot
monitor_task "$task_id"
else
if [ $VERBOSE -eq 1 ]; then
print_msg "Skipping snapshot creation due to --nosnapshot flag"
fi
fi
manage_retention
exit 0
Cheers!