Backup scripts for kernal virtual machines using libvirt.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

232 lines
7.1 KiB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
  1. #!/bin/bash
  2. # Copyright (c) 2022 Mr. Gecko's Media (James Coleman). http://mrgeckosmedia.com/
  3. # This is for backing up block devices in virsh
  4. # which use image files such as qcow2.
  5. # This also works with GlusterFS so long as your
  6. # volume is mounted.
  7. # A file to prevent overlapping runs. This allows us to make assumptions
  8. # that we're the only backup actively running, which allows us to recover
  9. # if a snapshot exists before backing up.
  10. PIDFILE="/tmp/backup-image.pid"
  11. # If the pid file exists and process is running, exit.
  12. if [[ -f "$PIDFILE" ]]; then
  13. PID=$(cat "$PIDFILE")
  14. if ps -p "$PID" >/dev/null; then
  15. echo "Backup process already running, exiting."
  16. exit 1
  17. fi
  18. fi
  19. # Create a new pid file for this process.
  20. echo $BASHPID >"$PIDFILE"
  21. # The borg repository we're backing up to.
  22. export BORG_REPO='/media/Storage/Backup/kvm'
  23. # If you have a passphrase for your repository,
  24. # set it here or you can use bash to retrieve it.
  25. # export BORG_PASSPHRASE=''
  26. # Set answers for automation.
  27. export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes
  28. export BORG_RELOCATED_REPO_ACCESS_IS_OK=yes
  29. export BORG_CHECK_I_KNOW_WHAT_I_AM_DOING=NO
  30. export BORG_DELETE_I_KNOW_WHAT_I_AM_DOING=NO
  31. # Set to empty string to disable pruning.
  32. PRUNE_OPTIONS="--keep-daily 7 --keep-weekly 4 --keep-monthly 6"
  33. # Remove PID file on exit.
  34. cleanup() {
  35. rm "$PIDFILE"
  36. }
  37. trap cleanup EXIT
  38. # Allows providing an argument of a domain to specifically backup.
  39. BACKUP_DOMAIN="$1"
  40. # Failures should remove pid file and exit with status code 1.
  41. fail() {
  42. echo "$1"
  43. exit 1
  44. }
  45. # If the domain is running, commit the changes saved to the snapshot to the image to finish the backup.
  46. blockCommit() {
  47. DOMSTATUS="$1"
  48. DOMAIN="$2"
  49. DEV="$3"
  50. if [[ "$DOMSTATUS" == "running" ]]; then
  51. echo "Commit changes for $DOMAIN ($DEV)"
  52. if ! virsh blockcommit \
  53. "$DOMAIN" \
  54. "$DEV" \
  55. --active \
  56. --verbose \
  57. --pivot \
  58. --delete; then
  59. fail "Could not commit changes $DOMAIN ($DEV). This may be a major issue and VM may be broken now."
  60. fi
  61. fi
  62. }
  63. # I save the status in a temporary file so I can error out and exit if a failure occurs.
  64. DOMLIST_STATUS_TMP="/tmp/backup-image-domlist-tmp"
  65. while read -r _ DOMAIN DOMSTATUS; do
  66. # If the domain is empty, its not needed.
  67. if [[ -z "$DOMAIN" ]]; then
  68. continue
  69. fi
  70. # If a backup domain was provided, we're only going to backup that domain.
  71. if [[ -n "$BACKUP_DOMAIN" ]] && [[ "$BACKUP_DOMAIN" != "$DOMAIN" ]]; then
  72. continue
  73. fi
  74. # Get the images that need backing up.
  75. DEVS=()
  76. IMAGES=()
  77. BLKLIST_STATUS_TMP="/tmp/backup-image-blklist-tmp"
  78. while read -r DEV IMAGE; do
  79. # Ignore empty line or no image.
  80. if [[ -z "$IMAGE" ]] || [[ "$IMAGE" == "-" ]]; then
  81. continue
  82. fi
  83. # Ignore iso files.
  84. if [[ "$IMAGE" =~ \.iso$ ]]; then
  85. continue
  86. fi
  87. # Ignore non-image files.
  88. if ! [[ "$IMAGE" =~ ^\/ ]]; then
  89. continue
  90. fi
  91. # This image needs backing up.
  92. DEVS+=("$DEV")
  93. IMAGES+=("$IMAGE")
  94. done < <(
  95. virsh domblklist "$DOMAIN" | tail -n +3
  96. echo "${PIPESTATUS[0]}" >"$BLKLIST_STATUS_TMP"
  97. )
  98. # Get status from the block listing.
  99. status=1
  100. if [[ -f $BLKLIST_STATUS_TMP ]]; then
  101. status=$(cat "$BLKLIST_STATUS_TMP")
  102. rm "$BLKLIST_STATUS_TMP"
  103. fi
  104. # If status has an error, exit.
  105. if ((status!=0)); then
  106. fail "Domain block listing failed"
  107. fi
  108. # For each image we can backup, back it up.
  109. for ((i = 0; i < ${#DEVS[@]}; i++)); do
  110. DEV=${DEVS[$i]}
  111. IMAGE=${IMAGES[$i]}
  112. IMAGEEXTENSION="${IMAGE##*.}"
  113. IMAGESNAPSHOT="${IMAGE%.*}.backup"
  114. IMAGENAME=$(basename "$IMAGE")
  115. # If the domain is running, we need to snapshot the disk so we can backup cleanly.
  116. if [[ "$DOMSTATUS" == "running" ]]; then
  117. # If the snapshot file exists, we should commit changes before performing another snapshot.
  118. # We are assuming that we created the snapshot here, and that concurrent runs are not possible.
  119. if [[ -e "$IMAGESNAPSHOT" ]]; then
  120. # Commit any blocks.
  121. blockCommit "$DOMSTATUS" "$DOMAIN" "$DEV"
  122. fi
  123. # Its possible that the image extension was changed to backup if a snapshot was previously made.
  124. # We assume it should be qcow2, and if that does not exist we will exit.
  125. if [[ "$IMAGEEXTENSION" == "backup" ]]; then
  126. IMAGE="${IMAGE%.*}.qcow2"
  127. if ! [ -f "$IMAGE" ]; then
  128. fail "Unable to determine image name."
  129. fi
  130. fi
  131. echo "Creating snapshot for $DOMAIN ($DEV)"
  132. if ! virsh snapshot-create-as --domain "$DOMAIN" \
  133. --name backup \
  134. --no-metadata \
  135. --atomic \
  136. --disk-only \
  137. --diskspec "$DEV,snapshot=external"; then
  138. fail "Failed to create snapshot for $DOMAIN ($DEV)"
  139. fi
  140. fi
  141. # Backup the image.
  142. echo "Creating backup for $DOMAIN ($DEV [$IMAGE])"
  143. if ! pv "$IMAGE" | borg create \
  144. --verbose \
  145. --stats \
  146. --show-rc \
  147. --stdin-name "$IMAGENAME" \
  148. "::$DOMAIN-$DEV-{now}" -; then
  149. # Commit any blocks.
  150. blockCommit "$DOMSTATUS" "$DOMAIN" "$DEV"
  151. fail "Failed to backup $DOMAIN ($DEV)"
  152. fi
  153. # Prune if options are configured.
  154. if [[ -n "$PRUNE_OPTIONS" ]]; then
  155. echo "Pruning backups for $DOMAIN ($DEV)"
  156. if ! eval borg prune --list \
  157. --show-rc \
  158. --glob-archives "'$DOMAIN-$DEV-*'" \
  159. "$PRUNE_OPTIONS"; then
  160. # Commit any blocks.
  161. blockCommit "$DOMSTATUS" "$DOMAIN" "$DEV"
  162. fail "Failed to prune $DOMAIN ($DEV)"
  163. fi
  164. fi
  165. # Commit any blocks.
  166. blockCommit "$DOMSTATUS" "$DOMAIN" "$DEV"
  167. done
  168. # Backup the domain info.
  169. echo "Backing up $DOMAIN xml"
  170. if ! virsh dumpxml "$DOMAIN" | borg create \
  171. --verbose \
  172. --stats \
  173. --show-rc \
  174. "::$DOMAIN-xml-{now}" -; then
  175. fail "Failed to backup $DOMAIN"
  176. fi
  177. # Prune if options are configured.
  178. if [[ -n "$PRUNE_OPTIONS" ]]; then
  179. echo "Pruning backups for $IMAGE"
  180. if ! eval borg prune --list \
  181. --show-rc \
  182. --glob-archives "'$DOMAIN-xml-*'" \
  183. "$PRUNE_OPTIONS"; then
  184. fail "Failed to prune $DOMAIN"
  185. fi
  186. fi
  187. done < <(
  188. virsh list --all | tail -n +3
  189. echo "${PIPESTATUS[0]}" >"$DOMLIST_STATUS_TMP"
  190. )
  191. # Get status from the domain listing.
  192. status=1
  193. if [[ -f $DOMLIST_STATUS_TMP ]]; then
  194. status=$(cat "$DOMLIST_STATUS_TMP")
  195. rm "$DOMLIST_STATUS_TMP"
  196. fi
  197. # If status has an error, exit.
  198. if ((status!=0)); then
  199. fail "Domain listing failed"
  200. fi
  201. # Shrink repo.
  202. borg compact