mirror of
https://github.com/lephisto/crossover.git
synced 2025-12-06 04:09:20 +01:00
Initial Commit
This commit is contained in:
414
crossover
Executable file
414
crossover
Executable file
@@ -0,0 +1,414 @@
|
||||
#!/bin/bash
|
||||
|
||||
#set -x
|
||||
|
||||
# Cross Pool Migration Tool for Proxmox VMs using Ceph.
|
||||
# Author: Bastian Mäuser <bma@netz.org>
|
||||
|
||||
declare -r VERSION=0.1
|
||||
declare -r NAME=$(basename "$0")
|
||||
declare -r PROGNAME=${NAME%.*}
|
||||
|
||||
declare -r PVE_DIR="/etc/pve"
|
||||
declare -r PVE_NODES="$PVE_DIR/nodes"
|
||||
declare -r QEMU='qemu-server'
|
||||
declare -r QEMU_CONF_CLUSTER="$PVE_NODES/*/$QEMU"
|
||||
declare -r EXT_CONF='.conf'
|
||||
|
||||
declare -r LOG_FILE=$(mktemp)
|
||||
|
||||
declare -A -g pvnode
|
||||
|
||||
declare opt_destination
|
||||
declare opt_vm_ids=''
|
||||
declare -i opt_debug=0
|
||||
declare -i opt_dry_run=0
|
||||
declare -i opt_syslog=0
|
||||
declare -i opt_lock=1
|
||||
declare -i opt_keeplock=0
|
||||
declare -i opt_overwrite=0
|
||||
|
||||
declare -r redstconf='^\/etc\/pve\/nodes\/(.*)\/qemu-server\/([0-9]+).conf$'
|
||||
declare -r recephimg='([a-zA-Z0-9]+)\:(.*)'
|
||||
|
||||
function usage(){
|
||||
shift
|
||||
|
||||
if [ "$1" != "--no-logo" ]; then
|
||||
cat << EOF
|
||||
_____
|
||||
| |___ ___ ___ ___ ___ _ _ ___ ___
|
||||
| --| _| . |_ -|_ -| . | | | -_| _|
|
||||
|_____|_| |___|___|___|___|\_/|___|_|
|
||||
|
||||
EOF
|
||||
fi
|
||||
|
||||
cat << EOF
|
||||
Proxmox Mirror tool for Ceph and Proxmox
|
||||
|
||||
Usage:
|
||||
$PROGNAME <COMMAND> [ARGS] [OPTIONS]
|
||||
$PROGNAME help
|
||||
$PROGNAME version
|
||||
|
||||
$PROGNAME mirror --vmid=<string> --destination=<destionationhost>
|
||||
Commands:
|
||||
version Show version program
|
||||
help Show help program
|
||||
mirror Relpicate a stopped VM to another Cluster (full clone)
|
||||
|
||||
Options:
|
||||
--vmid The ID of the VM/CT, comma separated (es. 100,101,102),
|
||||
'all-???' for all known guest systems in specific host (es. all-pve1, all-\$(hostname)),
|
||||
'all' for all known guest systems in cluster,
|
||||
'storage-???' storage Proxmox VE (pool Ceph)
|
||||
--destination 'Target PVE Host
|
||||
--pool 'Target Ceph Pool
|
||||
--nolock 'Don't lock source VM on Transfer
|
||||
--keeplock 'Keep source VM locked on Transfer
|
||||
--overwrite 'Overwrite Destination
|
||||
--debug 'Show Debug Output
|
||||
|
||||
Report bugs to <mephisto@mephis.to>
|
||||
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
function parse_opts(){
|
||||
# local action=$1
|
||||
shift
|
||||
|
||||
local args
|
||||
args=$(getopt \
|
||||
--options '' \
|
||||
--longoptions=vmid:,destination:,pool:,nolock,keeplock,overwrite,dry-run,debug \
|
||||
--name "$PROGNAME" \
|
||||
-- "$@") \
|
||||
|| end_process 128
|
||||
|
||||
eval set -- "$args"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--vmid) opt_vm_ids=$2; shift 2;;
|
||||
--destination) opt_destination=$2; shift 2;;
|
||||
--pool) opt_pool=$2; shift 2;;
|
||||
--dry-run) opt_dry_run=1; shift;;
|
||||
--debug) opt_debug=1; shift;;
|
||||
--nolock) opt_lock=0; shift;;
|
||||
--keeplock) opt_keeplock=1; shift;;
|
||||
--overwrite) opt_overwrite=1; shift;;
|
||||
--) shift; break;;
|
||||
*) break;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ $opt_debug -eq 1 ]; then
|
||||
log info "============================================"
|
||||
log info "Proxmox Crosspool Migration: $VERSION";
|
||||
log info "============================================"
|
||||
log info "Proxmox VE Version:"
|
||||
|
||||
pveversion
|
||||
|
||||
log info "============================================"
|
||||
fi
|
||||
|
||||
[ -z "$opt_vm_ids" ] && { log info "VM id is not set."; end_process 1; }
|
||||
|
||||
if [ "$opt_vm_ids" = "all" ]; then
|
||||
#all in cluster
|
||||
|
||||
local data=''
|
||||
data=$(get_vm_ids "$QEMU_CONF_CLUSTER/*$EXT_CONF")
|
||||
vm_ids=$(echo "$data" | tr ',' '\n')
|
||||
|
||||
elif [[ "$opt_vm_ids" == "all-"* ]]; then
|
||||
#all in specific host
|
||||
|
||||
local host=${opt_vm_ids#*-}
|
||||
|
||||
if ! exist_file "$PVE_NODES/$host"; then
|
||||
log info "Host not found!"
|
||||
end_process 1
|
||||
fi
|
||||
|
||||
local data=''
|
||||
data=$(get_vm_ids "$PVE_NODES/$host/$QEMU/*$EXT_CONF")
|
||||
[ -z "$data" ] && { log info "VM id is not set."; end_process 1; }
|
||||
|
||||
vm_ids=$(echo "$data" | tr ',' '\n')
|
||||
|
||||
elif [[ "$opt_vm_ids" == "storage-"* ]]; then
|
||||
#all in specific storage (pool Ceph)
|
||||
|
||||
local storage=${opt_vm_ids#*-}
|
||||
|
||||
if ! pvesm list "$storage" > /dev/null 2>&1; then
|
||||
log info "Pool '$storage' not found in Proxmox VE storage."
|
||||
end_process 1
|
||||
fi
|
||||
|
||||
vm_ids=$(pvesm list "$storage" | awk '{print $4}' | awk '!a[$0]++' )
|
||||
|
||||
else
|
||||
#comma separated
|
||||
vm_ids=$(echo "$opt_vm_ids" | tr ',' "\n")
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
function map_vmids_to_host(){
|
||||
for node in $(/usr/bin/pvecm nodes | tail +5 | tr -s ' ' | cut -d' ' -f 4)
|
||||
do
|
||||
for vm in $(ssh root@$node qm list|tail +2|tr -s ' '|cut -f 2 -d' ')
|
||||
do
|
||||
pvnode[$vm]=$node
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
function exist_file(){
|
||||
local file=''
|
||||
for file in $1; do
|
||||
[ -e "$file" ] && return 0 || return 1
|
||||
break
|
||||
done
|
||||
}
|
||||
|
||||
function get_vm_ids(){
|
||||
local data=''
|
||||
local conf=''
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
for conf in $1; do
|
||||
[ ! -e "$conf" ] && break
|
||||
|
||||
conf=$(basename "$conf")
|
||||
[ "$data" != '' ] && data="$data,"
|
||||
data="$data${conf%.*}"
|
||||
done
|
||||
shift
|
||||
done
|
||||
|
||||
echo "$data"
|
||||
}
|
||||
|
||||
function get_config_file(){
|
||||
local file_config=''
|
||||
|
||||
if exist_file "$QEMU_CONF_CLUSTER/$vm_id$EXT_CONF"; then
|
||||
file_config=$(ls $QEMU_CONF_CLUSTER/$vm_id$EXT_CONF)
|
||||
|
||||
else
|
||||
log error "VM $vm_id - Unknown technology or VMID not found: $QEMU_CONF_CLUSTER/$vm_id$EXT_CONF"
|
||||
end_process 128
|
||||
fi
|
||||
|
||||
echo "$file_config"
|
||||
}
|
||||
|
||||
function get_disks_from_config(){
|
||||
local disks;
|
||||
local file_config=$1
|
||||
|
||||
#disks available for vm/ct
|
||||
#exclude no backup
|
||||
#read current config
|
||||
disks=$(while read -r line; do
|
||||
[[ "$line" == "" ]] && break
|
||||
echo "$line"
|
||||
done < "$file_config" | \
|
||||
grep -P '^(?:((?:virtio|ide|scsi|sata|mp)\d+)|rootfs): ' | \
|
||||
grep -v -P 'cdrom|none' | \
|
||||
grep -v -P 'backup=0' | \
|
||||
awk '{ split($0,a,","); split(a[1],b," "); print b[2]}')
|
||||
|
||||
echo "$disks"
|
||||
}
|
||||
|
||||
function log(){
|
||||
local level=$1
|
||||
shift 1
|
||||
local message=$*
|
||||
|
||||
case $level in
|
||||
debug)
|
||||
if [ $opt_debug -eq 1 ]; then
|
||||
echo -e "$(date "+%F %T") DEBUG: $message";
|
||||
echo -e "$(date "+%F %T") DEBUG: $message" >> "$LOG_FILE";
|
||||
fi
|
||||
;;
|
||||
|
||||
info)
|
||||
echo -e "$message";
|
||||
echo -e "$message" >> "$LOG_FILE";
|
||||
[ $opt_syslog -eq 1 ] && logger -t "$PROGNAME" "$message"
|
||||
;;
|
||||
|
||||
warn)
|
||||
echo "WARNING: $message" 1>&2
|
||||
echo -e "$message" >> "$LOG_FILE";
|
||||
[ $opt_syslog -eq 1 ] && logger -t "$PROGNAME" -p daemon.warn "$message"
|
||||
;;
|
||||
|
||||
error)
|
||||
echo "ERROR: $message" 1>&2
|
||||
echo -e "$message" >> "$LOG_FILE";
|
||||
[ $opt_syslog -eq 1 ] && logger -t "$PROGNAME" -p daemon.err "$message"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "$message" 1>&2
|
||||
echo -e "$message" >> "$LOG_FILE";
|
||||
[ $opt_syslog -eq 1 ] && logger -t "$PROGNAME" "$message"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Do offline Mirror of a VM to another Procmox Ceph Cluster
|
||||
function mirror() {
|
||||
local -i rc=0;
|
||||
local vmname
|
||||
parse_opts "$@"
|
||||
|
||||
log info "ACTION: Mirror"
|
||||
log info "Start mirror $(date "+%F %T")"
|
||||
|
||||
#create pid file
|
||||
local pid_file="/var/run/$PROGNAME.pid"
|
||||
if [[ -e "$pid_file" ]]; then
|
||||
local pid; pid=$(cat "${pid_file}")
|
||||
if ps -p "$pid" > /dev/null 2>&1; then
|
||||
log error "Process already running with pid ${pid}"
|
||||
end_process 1
|
||||
fi
|
||||
fi
|
||||
|
||||
map_vmids_to_host
|
||||
|
||||
for vm_id in $vm_ids; do
|
||||
local file_config; file_config=$(get_config_file)
|
||||
[ -z "$file_config" ] && continue
|
||||
local disk=''
|
||||
|
||||
log debug "Checking for VM on Destination Host $opt_destination $QEMU_CONF_CLUSTER"
|
||||
conf_on_destination=$(ssh root@$opt_destination "ls -d $QEMU_CONF_CLUSTER/$vm_id$EXT_CONF 2>/dev/null")
|
||||
[[ "$conf_on_destination" =~ $redstconf ]]
|
||||
host_on_destination=${BASH_REMATCH[1]}
|
||||
echo "Host on destination: $host_on_destination"
|
||||
|
||||
status=$(ssh root@${pvnode[$vm_id]} qm status $vm_id|cut -d' ' -f 2)
|
||||
if [ $status == "running" ]; then
|
||||
echo "Source VM is running .. exiting"
|
||||
end_process 1
|
||||
fi
|
||||
|
||||
if [ $opt_lock -eq 1 ]; then
|
||||
ssh root@${pvnode[$vm_id]} qm set $vm_id --lock backup
|
||||
fi
|
||||
if [ -z "$host_on_destination" ]; then
|
||||
log info "VMID not present on remote host, transmitting"
|
||||
scpjob="scp $PVE_NODES/${pvnode[$vm_id]}/$QEMU/$vm_id.conf $opt_destination:$PVE_NODES/$opt_destination/$QEMU/$vm_id.conf"
|
||||
if ! do_run "$scpjob"; then
|
||||
log error "Transmitting VM Configuration failed"
|
||||
end_process 1
|
||||
fi
|
||||
fi
|
||||
for disk in $(get_disks_from_config "$file_config"); do
|
||||
log debug "VMID: $vm_id Disk: $disk"
|
||||
image_spec=$(get_image_spec "$disk")
|
||||
[[ $disk =~ $recephimg ]]
|
||||
image_name=${BASH_REMATCH[2]}
|
||||
[ -z "$image_spec" ] && continue
|
||||
chkjob=$(ssh $opt_destination rbd ls -l $opt_pool|grep $image_name|wc -l)
|
||||
if [ $chkjob -gt 0 ] && [ $opt_overwrite -eq 0 ]; then
|
||||
log error "Image $opt_pool/$image_name exists on destination, no permission to --overwrite"
|
||||
else
|
||||
if [ $chkjob -gt 0 ] && [ $opt_overwrite -eq 1 ]; then
|
||||
deljob=$(ssh $opt_destination rbd rm $opt_pool/$image_name)
|
||||
fi
|
||||
xmitjob="rbd export --rbd-concurrent-management-ops 8 $image_spec -|pv -r|ssh $opt_destination rbd import --image-format 2 - $opt_pool/$image_name"
|
||||
if ! do_run $xmitjob; then
|
||||
log error "Transmitting Image failed"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ ! $opt_keeplock -eq 1 ]; then
|
||||
ssh root@${pvnode[$vm_id]} qm unlock $vm_id
|
||||
log info "Unlocking VM $vm_id"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
function do_run(){
|
||||
local cmd=$*;
|
||||
local -i rc=0;
|
||||
|
||||
if [ $opt_dry_run -eq 1 ]; then
|
||||
echo "$cmd"
|
||||
rc=$?
|
||||
else
|
||||
log debug "$cmd"
|
||||
eval "$cmd"
|
||||
rc=$?
|
||||
[ $rc != 0 ] && log error "$cmd"
|
||||
log debug "return $rc ps ${PIPESTATUS[@]}"
|
||||
fi
|
||||
|
||||
return $rc
|
||||
}
|
||||
|
||||
function end_process(){
|
||||
local -i rc=$1;
|
||||
# if ! [[ -z "$startts" && -z "$endts" ]]; then
|
||||
# local -i runtime=$(expr $endts - $startts)
|
||||
# local -i bps=$(expr $bytecount/$runtime)
|
||||
# fi
|
||||
# local subject="Ceph [VM:$vmok/$vmtotal SS:$snapshotok/$snapshottotal EX:$exportok/$exporttotal] [$(bytesToHuman "$bytecount")@$(bytesToHuman "$bps")/s]"
|
||||
# [ $rc != 0 ] && subject="$subject [ERROR]"
|
||||
|
||||
#send email
|
||||
# local mail;
|
||||
# local mailhead="Backup $imgcount Images in $vmcount VMs (Bytes: $bytecount)"
|
||||
# for mail in $(echo "$opt_addr_mail" | tr "," "\n"); do
|
||||
# do_run "cat '$LOG_FILE' | mail -s '$subject' '$mail'"
|
||||
# done
|
||||
|
||||
#remove log
|
||||
# rm "$LOG_FILE"
|
||||
|
||||
exit "$rc";
|
||||
}
|
||||
|
||||
function get_image_spec(){
|
||||
local image_spec;
|
||||
local disk="$1"
|
||||
|
||||
#if krbd enable
|
||||
image_spec=$(pvesm path "$disk" | grep '^/dev/rbd/' | sed -e "s/^\/dev\/rbd\///")
|
||||
if [ -z "$image_spec" ]; then
|
||||
image_spec=$(pvesm path "$disk" | grep '/ceph/' | awk '{ split($0,a,":"); print a[2]}')
|
||||
fi
|
||||
|
||||
echo "$image_spec"
|
||||
}
|
||||
|
||||
function main(){
|
||||
[ $# = 0 ] && usage;
|
||||
|
||||
#command
|
||||
case "$1" in
|
||||
version) echo "$VERSION";;
|
||||
help) usage "$@";;
|
||||
mirror) mirror "$@";;
|
||||
*) usage;;
|
||||
esac
|
||||
|
||||
exit 0;
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user