Skip to content

Commit 4784ac6

Browse files
wip
1 parent 5add651 commit 4784ac6

File tree

1 file changed

+106
-146
lines changed

1 file changed

+106
-146
lines changed

backup/kvm_vm_backup/kvm_backup.sh

Lines changed: 106 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
6772
backup_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.
7386
vm_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

92116
case "$COMMAND_USE" in
93-
--active | --stopped | --clean) ;;
94-
*) usage ;;
117+
--active|--stopped|--clean) ;;
118+
*) usage ;;
95119
esac
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

Comments
 (0)