-
Notifications
You must be signed in to change notification settings - Fork 1.4k
nasbackup.sh: add timeout, cleanup trap, space check, quiesce support #12843
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
0deb4c4
9cbf928
d64286b
295d8a8
f7b490b
af4fbb8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,20 @@ logFile="/var/log/cloudstack/agent/agent.log" | |
|
|
||
| EXIT_CLEANUP_FAILED=20 | ||
|
|
||
| # Backup job timeout in seconds (default: 6 hours). Guards the libvirt | ||
| # domjobinfo wait loop so a stuck QEMU backup eventually fails the script | ||
| # instead of holding the agent's command slot indefinitely. | ||
| BACKUP_TIMEOUT=${BACKUP_TIMEOUT:-21600} | ||
|
|
||
| # Minimum free space required on the mounted backup target, in bytes | ||
| # (default: 1 GiB). Checked after mount, before any qemu-img convert, so | ||
| # we fail fast rather than mid-write when the NAS is near-full. | ||
| MIN_FREE_SPACE=${MIN_FREE_SPACE:-1073741824} | ||
|
|
||
| # Guards cleanup() against double-execution when both an explicit call | ||
| # and the EXIT trap fire (e.g. error path calls cleanup; exit 1 → trap). | ||
| CLEANUP_DONE=0 | ||
|
|
||
| log() { | ||
| [[ "$verb" -eq 1 ]] && builtin echo "$@" | ||
| if [[ "$1" == "-ne" || "$1" == "-e" || "$1" == "-n" ]]; then | ||
|
|
@@ -111,6 +125,7 @@ get_linstor_uuid_from_path() { | |
|
|
||
| backup_running_vm() { | ||
| mount_operation | ||
| check_free_space | ||
| mkdir -p "$dest" || { echo "Failed to create backup directory $dest"; exit 1; } | ||
|
|
||
| name="root" | ||
|
|
@@ -160,16 +175,26 @@ backup_running_vm() { | |
| virsh -c qemu:///system domiflist $VM > $dest/domiflist.xml 2>/dev/null | ||
| virsh -c qemu:///system domblklist $VM > $dest/domblklist.xml 2>/dev/null | ||
|
|
||
| # Bound the wait so a stuck QEMU backup eventually aborts instead of holding | ||
| # the agent's command slot until the orchestrator-level timeout fires. | ||
| local elapsed=0 | ||
| while true; do | ||
| status=$(virsh -c qemu:///system domjobinfo $VM --completed --keep-completed | awk '/Job type:/ {print $3}') | ||
| case "$status" in | ||
| Completed) | ||
| break ;; | ||
| Failed) | ||
| echo "Virsh backup job failed" | ||
| cleanup ;; | ||
| cleanup | ||
| exit 1 ;; | ||
| esac | ||
| if [[ $elapsed -ge $BACKUP_TIMEOUT ]]; then | ||
| echo "Backup timed out after ${BACKUP_TIMEOUT}s for VM $VM" | ||
| virsh -c qemu:///system domjobabort $VM > /dev/null 2>&1 || true | ||
| exit 1 | ||
| fi | ||
| sleep 5 | ||
| elapsed=$((elapsed + 5)) | ||
| done | ||
|
|
||
| # Use qemu-img convert to sparsify linstor backups which get bloated due to virsh backup-begin. | ||
|
|
@@ -204,6 +229,7 @@ backup_running_vm() { | |
|
|
||
| backup_stopped_vm() { | ||
| mount_operation | ||
| check_free_space | ||
| mkdir -p "$dest" || { echo "Failed to create backup directory $dest"; exit 1; } | ||
|
|
||
| IFS="," | ||
|
|
@@ -218,15 +244,21 @@ backup_stopped_vm() { | |
| volUuid="${disk##*/}" | ||
| fi | ||
| output="$dest/$name.$volUuid.qcow2" | ||
| if ! qemu-img convert -O qcow2 "$disk" "$output" > "$logFile" 2> >(cat >&2); then | ||
| if ! qemu-img convert -O qcow2 "$disk" "$output" >> "$logFile" 2> >(cat >&2); then | ||
| echo "qemu-img convert failed for $disk $output" | ||
| cleanup | ||
| exit 1 | ||
| fi | ||
| name="datadisk" | ||
| done | ||
| sync | ||
|
|
||
| ls -l --numeric-uid-gid $dest | awk '{print $5}' | ||
|
|
||
| # Unmount on success so the EXIT trap's cleanup (which removes an incomplete $dest) | ||
| # does not delete the just-completed backup. Mirrors backup_running_vm/delete_backup. | ||
| umount $mount_point | ||
| rmdir $mount_point | ||
| } | ||
|
|
||
| delete_backup() { | ||
|
|
@@ -253,28 +285,77 @@ mount_operation() { | |
| if [ ${NAS_TYPE} == "cifs" ]; then | ||
| MOUNT_OPTS="${MOUNT_OPTS},nobrl" | ||
| fi | ||
| mount -t ${NAS_TYPE} ${NAS_ADDRESS} ${mount_point} $([[ ! -z "${MOUNT_OPTS}" ]] && echo -o ${MOUNT_OPTS}) 2>&1 | tee -a "$logFile" | ||
| if [ $? -eq 0 ]; then | ||
| # Test mount's own exit in an if-condition: under `set -eo pipefail` the previous | ||
|
DaanHoogland marked this conversation as resolved.
Outdated
|
||
| # `mount ... | tee` form aborted before the check on failure, and `$?` captured tee's | ||
| # status (always 0), masking mount failures. Output is appended to the agent log. | ||
| if mount -t "${NAS_TYPE}" "${NAS_ADDRESS}" "${mount_point}" $([[ -n "${MOUNT_OPTS}" ]] && echo -o "${MOUNT_OPTS}") >> "$logFile" 2>&1; then | ||
| log -ne "Successfully mounted ${NAS_TYPE} store" | ||
| else | ||
| echo "Failed to mount ${NAS_TYPE} store" | ||
| exit 1 | ||
| fi | ||
| } | ||
|
jmsperu marked this conversation as resolved.
|
||
|
|
||
| check_free_space() { | ||
| local free_bytes | ||
| free_bytes=$(df -P "$mount_point" 2>/dev/null | awk 'NR==2 {print $4}') | ||
| if [[ -n "$free_bytes" ]]; then | ||
| # df -P reports 1K blocks; convert to bytes. | ||
| free_bytes=$((free_bytes * 1024)) | ||
| if [[ $free_bytes -lt $MIN_FREE_SPACE ]]; then | ||
| echo "Insufficient free space on backup target: $((free_bytes / 1048576)) MB available, $((MIN_FREE_SPACE / 1048576)) MB required" | ||
| exit 1 | ||
| fi | ||
| log -ne "Backup target has $((free_bytes / 1073741824)) GB free space" | ||
| fi | ||
| } | ||
|
|
||
| cleanup() { | ||
| # Idempotent: skip if a prior explicit call already ran. Without this guard, | ||
| # the EXIT trap would re-run cleanup and fail on the already-unmounted point. | ||
| [[ $CLEANUP_DONE -eq 1 ]] && return 0 | ||
| CLEANUP_DONE=1 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this line move to the end of the method, when cleanup is actually done?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It deliberately sits at the top, and I've renamed it in af4fbb8 so that is obvious. The flag means "cleanup has been entered", not "cleanup finished": cleanup() can itself |
||
|
|
||
| local status=0 | ||
|
|
||
| rm -rf "$dest" || { echo "Failed to delete $dest"; status=1; } | ||
| umount "$mount_point" || { echo "Failed to unmount $mount_point"; status=1; } | ||
| rmdir "$mount_point" || { echo "Failed to remove mount point $mount_point"; status=1; } | ||
| # If the VM was paused mid-backup (e.g. backup-begin succeeded but the script | ||
| # is exiting on error or signal), resume it. Without this a failed backup | ||
| # leaves the guest stuck in 'paused' state until an operator intervenes. | ||
| if [[ -n "$VM" ]]; then | ||
| local vm_state | ||
| vm_state=$(virsh -c qemu:///system domstate "$VM" 2>/dev/null || true) | ||
| if [[ "$vm_state" == "paused" ]]; then | ||
| log -ne "Resuming paused VM $VM during backup cleanup" | ||
| if ! virsh -c qemu:///system resume "$VM" > /dev/null 2>&1; then | ||
| echo "Failed to resume VM $VM" | ||
| status=1 | ||
| fi | ||
| fi | ||
| fi | ||
|
|
||
| if [[ -n "$dest" && -d "$dest" ]]; then | ||
| rm -rf "$dest" || { echo "Failed to delete $dest"; status=1; } | ||
| fi | ||
| if [[ -n "$mount_point" && -d "$mount_point" ]]; then | ||
| # Only umount if it is actually a mount — otherwise (mount failed, or stats/delete | ||
| # already unmounted) a umount error would wrongly flag cleanup as failed via the EXIT trap. | ||
| if mountpoint -q "$mount_point"; then | ||
| umount "$mount_point" 2>/dev/null || { echo "Failed to unmount $mount_point"; status=1; } | ||
| fi | ||
| rmdir "$mount_point" 2>/dev/null || true | ||
| fi | ||
|
jmsperu marked this conversation as resolved.
|
||
|
|
||
| if [[ $status -ne 0 ]]; then | ||
| echo "Backup cleanup failed" | ||
| exit $EXIT_CLEANUP_FAILED | ||
| fi | ||
| } | ||
|
|
||
| # Trap ensures cleanup runs on any exit path — including SIGTERM/SIGINT and | ||
| # unexpected errors caught by set -e — not just the explicit failure branches. | ||
| # Prevents orphan NFS mounts from accumulating after non-graceful exits. | ||
| trap cleanup EXIT | ||
|
|
||
| function usage { | ||
| echo "" | ||
| echo "Usage: $0 -o <operation> -v|--vm <domain name> -t <storage type> -s <storage address> -m <mount options> -p <backup path> -d <disks path> -q|--quiesce <true|false>" | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.