| #!/usr/bin/bash | |
| ## Licensed to the Apache Software Foundation (ASF) under one | |
| ## or more contributor license agreements. See the NOTICE file | |
| ## distributed with this work for additional information | |
| ## regarding copyright ownership. The ASF licenses this file | |
| ## to you under the Apache License, Version 2.0 (the | |
| ## "License"); you may not use this file except in compliance | |
| ## with the License. You may obtain a copy of the License at | |
| ## | |
| ## http://www.apache.org/licenses/LICENSE-2.0 | |
| ## | |
| ## Unless required by applicable law or agreed to in writing, | |
| ## software distributed under the License is distributed on an | |
| ## "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| ## KIND, either express or implied. See the License for the | |
| ## specific language governing permissions and limitations | |
| ## under the License. | |
| set -e | |
| # CloudStack B&R NAS Backup and Recovery Tool for KVM | |
| # TODO: do libvirt/logging etc checks | |
| ### Declare variables ### | |
| OP="" | |
| VM="" | |
| NAS_TYPE="" | |
| NAS_ADDRESS="" | |
| MOUNT_OPTS="" | |
| BACKUP_DIR="" | |
| DISK_PATHS="" | |
| logFile="/var/log/cloudstack/agent/agent.log" | |
| log() { | |
| [[ "$verb" -eq 1 ]] && builtin echo "$@" | |
| if [[ "$1" == "-ne" || "$1" == "-e" || "$1" == "-n" ]]; then | |
| builtin echo -e "$(date '+%Y-%m-%d %H-%M-%S>')" "${@: 2}" >> "$logFile" | |
| else | |
| builtin echo "$(date '+%Y-%m-%d %H-%M-%S>')" "$@" >> "$logFile" | |
| fi | |
| } | |
| vercomp() { | |
| local IFS=. | |
| local i ver1=($1) ver2=($3) | |
| # Compare each segment of the version numbers | |
| for ((i=0; i<${#ver1[@]}; i++)); do | |
| if [[ -z ${ver2[i]} ]]; then | |
| ver2[i]=0 | |
| fi | |
| if ((10#${ver1[i]} > 10#${ver2[i]})); then | |
| return 0 # Version 1 is greater | |
| elif ((10#${ver1[i]} < 10#${ver2[i]})); then | |
| return 2 # Version 2 is greater | |
| fi | |
| done | |
| return 0 # Versions are equal | |
| } | |
| sanity_checks() { | |
| hvVersion=$(virsh version | grep hypervisor | awk '{print $(NF)}') | |
| libvVersion=$(virsh version | grep libvirt | awk '{print $(NF)}' | tail -n 1) | |
| apiVersion=$(virsh version | grep API | awk '{print $(NF)}') | |
| # Compare qemu version (hvVersion >= 4.2.0) | |
| vercomp "$hvVersion" ">=" "4.2.0" | |
| hvStatus=$? | |
| # Compare libvirt version (libvVersion >= 7.2.0) | |
| vercomp "$libvVersion" ">=" "7.2.0" | |
| libvStatus=$? | |
| if [[ $hvStatus -eq 0 && $libvStatus -eq 0 ]]; then | |
| log -ne "Success... [ QEMU: $hvVersion Libvirt: $libvVersion apiVersion: $apiVersion ]" | |
| else | |
| echo "Failure... Your QEMU version $hvVersion or libvirt version $libvVersion is unsupported. Consider upgrading to the required minimum version of QEMU: 4.2.0 and Libvirt: 7.2.0" | |
| exit 1 | |
| fi | |
| log -ne "Environment Sanity Checks successfully passed" | |
| } | |
| ### Operation methods ### | |
| backup_running_vm() { | |
| mount_operation | |
| mkdir -p $dest | |
| name="root" | |
| echo "<domainbackup mode='push'><disks>" > $dest/backup.xml | |
| for disk in $(virsh -c qemu:///system domblklist $VM --details 2>/dev/null | awk '/disk/{print$3}'); do | |
| volpath=$(virsh -c qemu:///system domblklist $VM --details | awk "/$disk/{print $4}" | sed 's/.*\///') | |
| echo "<disk name='$disk' backup='yes' type='file' backupmode='full'><driver type='qcow2'/><target file='$dest/$name.$volpath.qcow2' /></disk>" >> $dest/backup.xml | |
| name="datadisk" | |
| done | |
| echo "</disks></domainbackup>" >> $dest/backup.xml | |
| # Start push backup | |
| virsh -c qemu:///system backup-begin --domain $VM --backupxml $dest/backup.xml > /dev/null 2>/dev/null | |
| # Backup domain information | |
| virsh -c qemu:///system dumpxml $VM > $dest/domain-config.xml 2>/dev/null | |
| virsh -c qemu:///system dominfo $VM > $dest/dominfo.xml 2>/dev/null | |
| virsh -c qemu:///system domiflist $VM > $dest/domiflist.xml 2>/dev/null | |
| virsh -c qemu:///system domblklist $VM > $dest/domblklist.xml 2>/dev/null | |
| until virsh -c qemu:///system domjobinfo $VM --completed --keep-completed 2>/dev/null | grep "Completed" > /dev/null; do | |
| sleep 5 | |
| done | |
| rm -f $dest/backup.xml | |
| sync | |
| # Print statistics | |
| virsh -c qemu:///system domjobinfo $VM --completed | |
| du -sb $dest | cut -f1 | |
| umount $mount_point | |
| rmdir $mount_point | |
| } | |
| backup_stopped_vm() { | |
| mount_operation | |
| mkdir -p $dest | |
| IFS="," | |
| name="root" | |
| for disk in $DISK_PATHS; do | |
| volUuid="${disk##*/}" | |
| qemu-img convert -O qcow2 $disk $dest/$name.$volUuid.qcow2 | tee -a "$logFile" | |
| name="datadisk" | |
| done | |
| sync | |
| ls -l --numeric-uid-gid $dest | awk '{print $5}' | |
| } | |
| delete_backup() { | |
| mount_operation | |
| rm -frv $dest | |
| sync | |
| umount $mount_point | |
| rmdir $mount_point | |
| } | |
| mount_operation() { | |
| mount_point=$(mktemp -d -t csbackup.XXXXX) | |
| dest="$mount_point/${BACKUP_DIR}" | |
| 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}) | tee -a "$logFile" | |
| if [ $? -eq 0 ]; then | |
| log -ne "Successfully mounted ${NAS_TYPE} store" | |
| else | |
| echo "Failed to mount ${NAS_TYPE} store" | |
| exit 1 | |
| fi | |
| } | |
| 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>" | |
| echo "" | |
| exit 1 | |
| } | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| -o|--operation) | |
| OP="$2" | |
| shift | |
| shift | |
| ;; | |
| -v|--vm) | |
| VM="$2" | |
| shift | |
| shift | |
| ;; | |
| -t|--type) | |
| NAS_TYPE="$2" | |
| shift | |
| shift | |
| ;; | |
| -s|--storage) | |
| NAS_ADDRESS="$2" | |
| shift | |
| shift | |
| ;; | |
| -m|--mount) | |
| MOUNT_OPTS="$2" | |
| shift | |
| shift | |
| ;; | |
| -p|--path) | |
| BACKUP_DIR="$2" | |
| shift | |
| shift | |
| ;; | |
| -d|--diskpaths) | |
| DISK_PATHS="$2" | |
| shift | |
| shift | |
| ;; | |
| -h|--help) | |
| usage | |
| shift | |
| ;; | |
| *) | |
| echo "Invalid option: $1" | |
| usage | |
| ;; | |
| esac | |
| done | |
| # Perform Initial sanity checks | |
| sanity_checks | |
| if [ "$OP" = "backup" ]; then | |
| STATE=$(virsh -c qemu:///system list | grep $VM | awk '{print $3}') | |
| if [ "$STATE" = "running" ]; then | |
| backup_running_vm | |
| else | |
| backup_stopped_vm | |
| fi | |
| elif [ "$OP" = "delete" ]; then | |
| delete_backup | |
| fi |