@@ -57,173 +57,133 @@ EOF
5757 exit 1
5858}
5959
60- # start new log.
61- starting_logfile () {
62- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Starting backup of $ACTIVEVM " | tee $LOGFILE
63- mkdir -p " $BACKUP_DIR /$ACTIVEVM "
60+ init_log () {
61+ (( LOG_INITIALIZED )) && return
62+ mkdir -p " $( dirname " $LOGFILE " ) "
63+ exec >> >( tee -a " $LOGFILE " ) 2>&1
64+ LOG_INITIALIZED=1
6465}
6566
67+ start_log () {
68+ mkdir -p " $BACKUP_DIR /$ACTIVEVM "
69+ echo " $( date +' %Y-%m-%d %H:%M:%S' ) Starting backup of $ACTIVEVM "
70+ }
6671# backup config of VM.
6772backup_vm_config () {
68- RESULT_CMD=$( virsh dumpxml " $ACTIVEVM " > " $BACKUP_DIR /$ACTIVEVM /$ACTIVEVM .xml" )
69- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Dumping xml... ${RESULT_CMD// \\ n/ } " | tee -a $LOGFILE
73+ virsh dumpxml " $ACTIVEVM " > " $BACKUP_DIR /$ACTIVEVM /$ACTIVEVM " .xml
74+ echo " $( date ' +%Y-%m-%d %H:%M:%S' ) Saved $ACTIVEVM domain XML"
75+ }
76+
77+ # double‑checks the path we are about to delete.
78+ safe_rm () {
79+ local target=" $1 "
80+ [[ -z " $target " || " $target " == " /" ]] && fatal " Refusing to remove empty or root path"
81+ [[ ! -e " $target " ]] && fatal " safe_rm: '$target ' does not exist"
82+ rm -rf --one-file-system -- " $target "
7083}
7184
7285# Getting a list and a path of disk images.
7386vm_disks_get () {
74- DISK_LIST=$( virsh domblklist " $ACTIVEVM " | awk ' {if(NR>2)print}' | awk ' {print $1}' )
75- DISK_PATH=$( virsh domblklist " $ACTIVEVM " | awk ' {if(NR>2)print}' | awk ' {print $2}' )
76- echo " $( date +' %Y-%m-%d %H:%M:%S' ) VM disk(s) / path of disk(s): ${DISK_LIST// $' \n ' / , } -> ${DISK_PATH// $' \n ' / , } " |
77- tee -a $LOGFILE
87+ # robust domblklist parsing using separator
88+ mapfile -t DISK_INFO < <( virsh domblklist --details --type disk --noheadings --separator ' |' " $ACTIVEVM " 2> /dev/null)
89+ DISK_LIST=()
90+ DISK_PATH=()
91+ for line in " ${DISK_INFO[@]} " ; do
92+ IFS=' |' read -r target source _type _dev <<< " $line"
93+ DISK_LIST+=(" $target " )
94+ DISK_PATH+=(" $source " )
95+ done
96+ echo " $( date ' +%Y-%m-%d %H:%M:%S' ) Disk targets: ${DISK_LIST[*]} " ;
97+ echo " $( date ' +%Y-%m-%d %H:%M:%S' ) Disk paths : ${DISK_PATH[*]} " ;
7898}
7999
80100# Getting a block device which is a snapshot.
81- get_vm_shapshots () {
82- virsh domblklist " $1 " | grep ' .snapshot ' | awk ' {print $2} '
101+ get_snapshots () {
102+ virsh snapshot-list --domain " $ACTIVEVM " --no-metadata --name 2> /dev/null || true
83103}
84104
105+
85106# Entry point.
107+ set -euo pipefail
108+ IFS=$' \n\t '
109+ shopt -s nocasematch
110+
86111[[ $# -lt 2 ]] && usage
87- COMMAND_USE=" $1 "
88- shift
112+ COMMAND_USE=" $1 " ; shift
89113
90114[[ $EUID -ne 0 ]] && fatal " Please run as root (e.g. sudo $0 ...)"
91115
92116case " $COMMAND_USE " in
93- --active | --stopped | --clean) ;;
94- * ) usage ;;
117+ --active| --stopped| --clean) ;;
118+ * ) usage ;;
95119esac
96120
97- #
98- # making backup of running VMs on (active)
99- #
100- if [[ $COMMAND_USE == " --active" ]]; then
101- for ACTIVEVM in " ${@ } " ; do
102- starting_logfile
103- backup_vm_config
121+ LOG_INITIALIZED=0
122+ init_log
123+
124+ for ACTIVEVM in " $@ " ; do
125+ SNAPSHOT_NAME=" snapshot-${ACTIVEVM} -$( date +%s%N) "
126+ start_log
127+ backup_vm_config
128+ vm_disks_get
129+
130+ if [[ $COMMAND_USE == " --active" ]]; then
131+ echo " Creating live snapshot $SNAPSHOT_NAME for $ACTIVEVM "
132+ if ! get_snapshots | grep -Fxq " $SNAPSHOT_NAME " ; then
133+ virsh snapshot-create-as --domain " $ACTIVEVM " " $SNAPSHOT_NAME " --disk-only \
134+ --atomic --quiesce --no-metadata
135+ else
136+ echo " Snapshot $SNAPSHOT_NAME already exists – skipping create"
137+ fi
138+
139+ # refresh disk list after snapshot so we copy the backing image layer
104140 vm_disks_get
105141
106- # making a snapshot
107- (
108- VM_SNAPSHOT_CHECK=" $( get_vm_shapshots " $ACTIVEVM " ) "
109- if [[ -z " $VM_SNAPSHOT_CHECK " ]]; then
110- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Creating snapshot of $ACTIVEVM "
111- echo " $( date +' %Y-%m-%d %H:%M:%S' ) $( virsh snapshot-create-as --domain " $ACTIVEVM " snapshot --disk-only \
112- --atomic --quiesce --no-metadata 2>&1 ) "
113- else
114- echo " $ACTIVEVM already contains a snapshot: $VM_SNAPSHOT_CHECK , skipping creation."
115- echo " Perhaps a previous backup job was interrupted."
116- fi
117- if [[ ! -f " $( get_vm_shapshots " $ACTIVEVM " | sed ' s|\(.*\)/.*|\1|' ) /$ACTIVEVM .snapshot" ]]; then
118- echo " $( date +' %Y-%m-%d %H:%M:%S' ) WARNING! Snapshot wasn't created."
119- echo " $( date +' %Y-%m-%d %H:%M:%S' ) There's no guaranty that resulting copy of VM may have consistent data."
120- fi
121-
122- for PATH_ITEM in $DISK_PATH ; do
123- # getting filename from the path
124- FILENAME=$( basename " $PATH_ITEM " )
125- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Device image name is: $FILENAME " | tee -a $LOGFILE
126- if [[ $PATH_ITEM == " -" ]] || [[ $PATH_ITEM =~ \. iso$ ]] || [[ $PATH_ITEM == \. ISO$ ]]; then
127- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Looks like removable media device slot, skipping"
128- else
129- # backup disk
130- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Creating backup of $ACTIVEVM $PATH_ITEM \
131- $( cp " $PATH_ITEM " " $BACKUP_DIR /$ACTIVEVM /$FILENAME " 2>&1 ) "
132- fi
133- done
134-
135- for DISK_ITEM in $DISK_LIST ; do
136- # getting a path to a snapshot
137- SNAPSHOT_PATH=$( virsh domblklist " $ACTIVEVM " | grep " $DISK_ITEM " | awk ' {print $2}' )
138- if [[ $SNAPSHOT_PATH == " -" ]] || [[ $SNAPSHOT_PATH =~ \. iso$ ]] || [[ $SNAPSHOT_PATH == \. ISO$ ]]; then
139- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Device path is $SNAPSHOT_PATH ."
140- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Looks like removable media device, skipping"
141- else
142- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Commit $SNAPSHOT_PATH of $ACTIVEVM to $DISK_ITEM image"
143-
144- # block-commit snapshot to disk image
145- RESULT_CMD=$( virsh blockcommit " $ACTIVEVM " " $DISK_ITEM " --active --verbose --pivot 2> /dev/null ||
146- echo " Nothing to commit with $DISK_ITEM or just failed." )
147- echo " $( date +' %Y-%m-%d %H:%M:%S' ) ${RESULT_CMD// $' \n ' / } "
148- if [[ $SNAPSHOT_PATH =~ \. snapshot$ ]]; then
149- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Removing snapshot $SNAPSHOT_PATH . $( rm -f " $SNAPSHOT_PATH " ) " 2>&1
150- else
151- echo " $( date +' %Y-%m-%d %H:%M:%S' ) $SNAPSHOT_PATH is not snapshot, skipping."
152- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Looks like you have copied images from running machine or no" \
153- " snapshot created"
154- fi
155- fi
156- done
157- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Backup of $ACTIVEVM finished" | tee -a $LOGFILE
158- ) 2>&1 | tee -a $LOGFILE
159- done
160- fi
161-
162- #
163- # making backup of stopped VMs (stop, backup, run)
164- #
165- if [[ $COMMAND_USE = " --stopped" ]]; then
166- for ACTIVEVM in " ${@ } " ; do
167- starting_logfile
168- backup_vm_config
142+ for SRC in " ${DISK_PATH[@]} " ; do
143+ FILENAME=$( basename " $SRC " )
144+ [[ " $SRC " == " -" || " $SRC " == * .iso ]] && { echo " Skip removable/media: $SRC " && continue ; }
145+ echo " Copying $SRC -> $BACKUP_DIR /$ACTIVEVM /$FILENAME " ;
146+ cp --reflink=auto --sparse=always " $SRC " " $BACKUP_DIR /$ACTIVEVM /$FILENAME "
147+ done
148+
149+ # commit + remove snapshot layer
150+ for disk in " ${DISK_LIST[@]} " ; do
151+ virsh blockcommit " $ACTIVEVM " " $disk " --active --verbose --pivot || echo " Nothing to commit for $disk "
152+ done
169153 vm_disks_get
170-
171- COUNTER=100
172- (
173- # creating backup subdirectory
174- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Creating backup subdirectory... $( mkdir " $BACKUP_DIR /$ACTIVEVM " 2>&1 &&
175- echo " OK." ) "
176- # shutdown VM
177- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Shutting down $ACTIVEVM ... $( virsh shutdown " $ACTIVEVM " 2>&1 |
178- sed -z " s/\n//g" ) "
179- # wait while VM is not running
180- while (virsh list | grep " $ACTIVEVM " > /dev/null) && [[ $COUNTER -gt 0 ]]; do
181- sleep 3
182- (( COUNTER-- )) || true
183- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Waiting $ACTIVEVM becomes down."
184- done
185-
186- # perform force power-off if VM is still running
187- if (virsh list | grep " $ACTIVEVM " > /dev/null); then
188- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Unable to shutdown $ACTIVEVM . Performing force power-off... $( virsh \
189- destroy " $ACTIVEVM " 2>&1 | sed -z " s/\n//g" ) " 2>&1
190-
191- while (virsh list | grep " $ACTIVEVM " > /dev/null) && [[ $COUNTER -gt 0 ]]; do
192- sleep 1
193- (( COUNTER++ )) || true
194- done
195-
196- else
197- echo " $( date +' %Y-%m-%d %H:%M:%S' ) $ACTIVEVM stopped."
198- fi
199-
200- for PATH_ITEM in $DISK_PATH ; do
201- # getting filename from the path
202- FILENAME=$( basename " $PATH_ITEM " )
203- if [[ $PATH_ITEM == " -" ]] || [[ $PATH_ITEM =~ \. iso$ ]] || [[ $PATH_ITEM == \. ISO$ ]]; then
204- # skip "-" (not mounted) and ".iso"/".ISO" (CD-ROM image)
205- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Device image name is: $FILENAME "
206- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Looks like removable media device, skipping"
207- else
208- # backup disk
209- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Copying $ACTIVEVM $PATH_ITEM image... $( cp -rf " $PATH_ITEM " \
210- " $BACKUP_DIR /$ACTIVEVM /$FILENAME " 2>&1 ) "
211- fi
212- done
213-
214- # run VM
215- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Starting $ACTIVEVM $( virsh start " $ACTIVEVM " 2>&1 | sed -z " s/\n//g" ) "
216- ) 2>&1 | tee -a $LOGFILE
217- done
218- fi
219-
220- #
221- # clean previous backups
222- #
223- if [[ $COMMAND_USE = " --clean" ]]; then
224- for ACTIVEVM in " ${@ } " ; do
225- # clean content of the folder
226- echo " $( date +' %Y-%m-%d %H:%M:%S' ) Performing clean-up of $ACTIVEVM in $BACKUP_DIR ... $( rm \
227- -rfv " ${BACKUP_DIR:? } /$ACTIVEVM " 2>&1 && echo " OK" ) " 2>&1 | tee -a $LOGFILE
228- done
229- fi
154+ # remove snapshot file(s)
155+ for p in " ${DISK_PATH[@]} " ; do
156+ [[ $p == * .snapshot ]] || continue
157+ echo " Removing leftover snapshot layer $p "
158+ rm -f -- " $p "
159+ done
160+ echo " Backup of $ACTIVEVM finished"
161+
162+ elif [[ $COMMAND_USE == " --stopped" ]]; then
163+ echo " Shutting down $ACTIVEVM "
164+ virsh shutdown " $ACTIVEVM " || true
165+ COUNTER=40 # 40*3=120s
166+ while virsh list --name | grep -Fxq " $ACTIVEVM " && (( COUNTER-- > 0 )) ; do
167+ sleep 3
168+ done
169+ if virsh list --name | grep -Fxq " $ACTIVEVM " ; then
170+ echo " Force‑off $ACTIVEVM "
171+ virsh destroy " $ACTIVEVM "
172+ fi
173+
174+ for SRC in " ${DISK_PATH[@]} " ; do
175+ FILENAME=$( basename " $SRC " )
176+ [[ " $SRC " == " -" || " $SRC " == * .iso ]] && { echo " Skip removable/media: $SRC " && continue ; }
177+ cp --reflink=auto --sparse=always " $SRC " " $BACKUP_DIR /$ACTIVEVM /$FILENAME "
178+ done
179+
180+ echo " Starting $ACTIVEVM "
181+ virsh start " $ACTIVEVM "
182+
183+ else # --clean
184+ echo " Cleaning backups of $ACTIVEVM "
185+ safe_rm " $BACKUP_DIR /$ACTIVEVM " || true
186+ fi
187+
188+ echo " Completed $ACTIVEVM "
189+ done
0 commit comments