#!/bin/bash

set -e
config_bundle="fvm_disk_size fvm_name fvm_ram fvm_cpu migrate_log fuel_astute  dkvm_folder \
    admin_net_br admin_net_b_type other_net_bridges os_swap os_root  os_var os_varlibdocker \
    os_varlog fm_reboot del_vm max_worktime"
work_bundle="dkvm_ip admin_net_ip_bs dkvm_uri admin_net_ip admin_net_if   admin_net_nm \
    src_disk dst_disk s_base start_time fvm_admin_mac admin_net_mac"



descr_fm_reboot="Would you like to automatically reboot the machine and begin data sync \
        process? 'yes' or 'no'. Default is 'yes'"
descr_fvm_disk_size="Disk size for the destination VM. Default is '100g'"
descr_fvm_name="Name for the destination VM. Default is 'fuel_master'"
descr_fvm_ram="Amount of RAM (in MB) for the destination VM. Default is '2048'"
descr_fvm_cpu="Number of CPU cores for the destination VM. Default is '2'"
descr_migrate_log="Path for log file. Default is '/var/log/fuel-migrate.log'"
descr_fuel_astute="Location of Fuel configuration file. Default is '/etc/fuel/astute.yaml'"
descr_dkvm_folder="Path to folder on kvm host where disk will be created. \
        Default is '/var/lib/nova/'"
descr_admin_net_br="Bridge which will be used for connecting VM admin interface. \
        Default is 'br-fw-admin'"
descr_admin_net_b_type="Additional data to put inside network interface block definition. \
        For example \"<virtualport type='openvswitch'/>\" By default empty"
descr_other_net_bridges="Comma separated set of other net interfaces which will be created.\
        Contains 3*X parameters name_of_nic,bridge_on_kvm,additional_data. Where X is number \
        of additional nics, name_of_nic  looks like eth1, second two are similar to \
        admin_net_br and admin_net_b_type"
descr_os_swap="Size of swap volume. By default 2*RAM size"
descr_os_root="Size of root volume. By default the same as source"
descr_os_var="Size for the var volume. By default 20% of the remaining free space. \
        (Will be applied in order prompted here)"
descr_os_varlibdocker="Size of varlibdocker volume. By default 30% of the remaining free space."
descr_os_varlog="Size of varlog volume. By default 100% of the remaining free space."
descr_del_vm="Remove destination VM if it already exists? 'yes' or 'no'. Default is 'no'"
descr_max_worktime="Timeout for entire migration process in seconds. Default is 7200"

indent="\t\t\t\t"
wtw="$(($(stty size | awk  '{print $2}') - 32 ))"
lockfile_dir=/var/lock
fd=200
banner_file="c-msg.sh"
prog_name="fuel-migrate"
fuel_migrate_vars=/root/fuel-migrate.var


file_lock() {
    # create lock file

    eval "exec $fd>$lockfile_dir/$prog_name.lock"

    # acquire the lock
    if flock -n $fd ; then
        return 0
    else
        echo "Only one instance of $prog_name can run at a time."
        return 1
    fi

}


usage(){

[[ ! -z "$1" ]] && echo "$1"
echo -e "
Usage:

\t$prog_name DESTINATION_COMPUTE [ OPTIONS ]

\tDESTINATION_COMPUTE:\r${indent}Name or IP of compute node where VM will be created

\tOPTIONS:

$(var_describe $config_bundle)
"
exit 1
}


var_describe(){
    local dn
    until [[ -z "$1" ]] ; do
        dn="descr_$1"
        echo -n -e "\t--$1\r"
        # no there ""
        echo_indent ${!dn}
        shift
    done
}

# Formatted out with indent for current screen size
echo_indent(){
    [[ -z "$1" ]] &&  echo ""
    local cline=""
    until [[ -z "$1" ]] ; do
        if [[ "$(( ${#cline} + ${#1} + 1 ))" -gt "$wtw" ]] ; then
            echo -e "${indent}${cline}"
            cline=" $1"
        else
            cline="${cline} $1"
        fi
        shift
    done
    [[ ! -z "$cline" ]] && echo -e "${indent}${cline}"

}

# Permanent console output
cecho(){
    echo "$@" >&6
}

mytrim(){
# no ""
    echo $1
}

# Obtain parameter from yaml file
yaml_var(){
    local fn="$1"
    local py_var=""
    shift
    until [[ -z "$1" ]] ; do
        py_var="$py_var['$1']"
        shift
    done
    if [[ ! -z "py_cmd" ]] ; then
        python -c "import yaml; a = yaml.load(open('$fn', 'r')); print a$py_var"
    fi
}


# save set vars for usage later
save_vars(){
    echo "# Saved at $(date) ">"$fuel_migrate_vars"
    until [[ -z "$1" ]] ; do
        [[ ! -z "${!1}" ]] && echo "$1=\"$(mytrim ${!1})\"" >>"$fuel_migrate_vars"
        shift
    done
}

# create network definition template
ifaces(){
    until [[ -z "$1" ]] ; do
        echo "     <interface type='bridge'>
      <source bridge='$2'/>
      <target dev='vfm_$1'/>"
      [[ ! -z "$3" ]] && echo "$3"
        echo "     <model type='virtio'/>
    </interface>"
        shift
        shift
        shift
    done
}

# Create destination VM
create_vm(){
    cecho "Create VM ${fvm_name} on ${dkvm_ip}"
    # check is kvm present
    virsh -c "${dkvm_uri}" list
    # check is vm is present
    vm_state="$(virsh -c "${dkvm_uri}" list --all | awk "/ ${fvm_name} / {print \$3}")"
    if [[ "${del_vm}" == "yes" ]] ; then
        # destroy vm if it exist
        [[ "$vm_state" ==  "running" ]] &&  virsh -c "${dkvm_uri}" destroy "${fvm_name}"
        [[ ! -z "$vm_state" ]] && virsh -c "${dkvm_uri}" undefine "${fvm_name}"
    else
        if [[ ! -z "$vm_state" ]] ; then
            echo "${fvm_name}  already exist on ${dkvm_ip} "
            exit 1
        fi
    fi
    # create disk
    ssh "${dkvm_ip}" qemu-img create -f raw "${dkvm_folder}/${fvm_name}.img" "${fvm_disk_size}"
    # make template for virsh
    IFS=","
    cat >/tmp/vm.xml <<EOF
<domain type='kvm'>
  <name>${fvm_name}</name>
  <memory unit='MiB'>${fvm_ram}</memory>
  <vcpu >${fvm_cpu}</vcpu>
  <os>
    <type arch='x86_64' >hvm</type>
    <boot dev='hd'/>
    <boot dev='network'/>
 </os>
  <features>
    <acpi/>
    <apic/>
  </features>
  <clock offset='utc'/>
  <on_poweroff>destroy</on_poweroff>
  <on_reboot>restart</on_reboot>
  <on_crash>restart</on_crash>
  <devices>
    <disk type='file' device='disk'>
      <driver name='qemu' type='raw' cache='writeback'/>
      <source file='${dkvm_folder}/${fvm_name}.img'/>
      <target dev='vda' bus='virtio'/>
    </disk>
    <controller type='usb' index='0'>
    </controller>
    <controller type='ide' index='0'>
    </controller>

    <interface type='bridge'>
      <source bridge='${admin_net_br}'/>
      <target dev='vfm_${admin_net_if}'/>"
      ${admin_net_b_type}
      <model type='virtio'/>
    </interface>
$(ifaces $other_net_bridges)
    <input type='mouse' bus='ps2'/>
    <graphics type='vnc' autoport='yes' listen='0.0.0.0'/>
    <video>
      <model type='cirrus' />
    </video>
    <memballoon model='virtio'>
    </memballoon>
  </devices>
</domain>
EOF
    IFS=" "
    virsh -c "${dkvm_uri}" define /tmp/vm.xml
    virsh -c "${dkvm_uri}" autostart ${fvm_name}
    virsh -c "${dkvm_uri}" start ${fvm_name}
    fvm_admin_mac="$(virsh -c "${dkvm_uri}"  domiflist "${fvm_name}" | awk "/vfm_${admin_net_if}/ { print \$5}")"
    #wait until vm up
    while [[ -z "${admin_net_ip_bs}" ]] ; do
        timeout
        sleep 10
        admin_net_ip_bs="$(mytrim $(fuel node | awk -F'|' "/${fvm_admin_mac}/ {print \$5}"))"
    done

}

# make cmd string for sgdisk. Size of last will be truncated (extended) to fit
# current disk size
clone_part_str(){
    lastsec="$(( $3 * ${src_bs} / ${dst_bs}))"
    [[ $1 == "$pcount" ]] &&  lastsec=""
    echo " -n$1:$(( $2 * ${src_bs} / ${dst_bs})):${lastsec} -t $1:$6 -c $1:$7"
}

# partitioning source disk
mark_dst_disk(){
    cecho "Create partition table"
    # clear partition table
    ssh "${admin_net_ip_bs}" sgdisk -o $dst_disk
    src_bs="$(sgdisk -p $src_disk | awk  '/Logical sector size: / {print $4}')"
    dst_bs="$(ssh ${admin_net_ip_bs} sgdisk -p $dst_disk | awk  '/Logical sector size: / {print $4}')"
    pcount="$(sgdisk -p $src_disk | tail -n 1 | awk '{print $1}')"
    tstr="-a 1 "
    # get current config and create cmd line for sgdisk
    sgdisk -p $src_disk | sed -n '/^ /p' >/tmp/fuel_migrate.tmp
    while read part ; do
        tstr="$tstr  $(clone_part_str $part)"
        export tstr
    done </tmp/fuel_migrate.tmp
    ssh "${admin_net_ip_bs}" "sgdisk $dst_disk $tstr"

}


mk_lvs_fs(){

# to-do get current fuel data
    cecho "Create lvm volumes and file-systems"

    ssh "${admin_net_ip_bs}" vgcreate os ${dst_disk}4
    ssh "${admin_net_ip_bs}" lvcreate os -n root -L ${os_root}
    ssh "${admin_net_ip_bs}" lvcreate os -n swap -L ${os_swap}
    ssh "${admin_net_ip_bs}" lvcreate os -n var -l${os_var}free
    ssh "${admin_net_ip_bs}" lvcreate os -n varlibdocker -l${os_varlibdocker}free
    ssh "${admin_net_ip_bs}" lvcreate os -n varlog -l${os_varlog}free
    ssh "${admin_net_ip_bs}"  "mkfs.ext4 /dev/os/root; mkfs.ext4 /dev/os/var; mkfs.ext4  /dev/os/varlog; mkfs.ext4  /dev/os/varlibdocker; mkswap /dev/os/swap"

}


rsync_vol(){
    cecho "Start syncing $2"
    ssh "${admin_net_ip_bs}" "mkdir -p ${s_base}${1};"
    ssh "${admin_net_ip_bs}" "mount -t ext4 -o  user_xattr,acl  ${2} ${s_base}${1}"
    # Why -X doesn't work ?!!
    time rsync -z -axAHS --numeric-ids ${1}/ $admin_net_ip_bs:${s_base}${1}/
}


rsync_data(){
    # use dd to create small 1:1 partitions to avoid FS issues
    dd if="${src_disk}1" | ssh "${admin_net_ip_bs}" "dd of=${dst_disk}1"
    dd if="${src_disk}2" | ssh "${admin_net_ip_bs}" "dd of=${dst_disk}2"
    dd if="${src_disk}3" | ssh "${admin_net_ip_bs}" "dd of=${dst_disk}3"

    rsync_vol / /dev/os/root
    rsync_vol /var /dev/os/var
    rsync_vol /var/lib/docker /dev/os/varlibdocker
    rsync_vol /var/log /dev/os/varlog
}


fix_config(){
    cecho "Make grub-loader and fix config files"
    # make grub loader
    ssh "${admin_net_ip_bs}" "sed  -i 's#${src_disk}#${dst_disk}#'  ${s_base}/etc/mtab"
    ssh "${admin_net_ip_bs}" "mount -o bind /dev/ ${s_base}/dev/"
    ssh "${admin_net_ip_bs}" "mount ${dst_disk}3  ${s_base}/boot/"
    ssh "${admin_net_ip_bs}" "chroot ${s_base} grub-install --recheck ${dst_disk}"
    # fix configs
    ssh "${admin_net_ip_bs}" "sed -i '/HWADDR=/d' ${s_base}/etc/sysconfig/network-scripts/ifcfg-*"
    ssh "${admin_net_ip_bs}" "sed -i 's/${admin_net_mac}/${fvm_admin_mac}/' ${s_base}${fuel_astute}"
    ssh "${admin_net_ip_bs}" "cp  /etc/udev/rules.d/70-persistent-net.rules  ${s_base}/etc/udev/rules.d/70-persistent-net.rules"
    ssh "${admin_net_ip_bs}" "cat /root/.ssh/authorized_keys >>${s_base}/root/.ssh/authorized_keys"
    ssh "${admin_net_ip_bs}" "umount ${s_base}/var/log ${s_base}/var/lib/docker ${s_base}/var ${s_base}/boot ${s_base}/dev ${s_base}" || true
}

fix_udev_rule(){
    # make udev on destination vm use the same network interface name as on source
    fvm_admin_mac=$(virsh -c "${dkvm_uri}"  domiflist "${fvm_name}" | awk "/vfm_${admin_net_if}/ { print \$5}")
    ssh "${admin_net_ip_bs}" "sed -i -e 's/\(${fvm_admin_mac}.* NAME=\"\)[^\"]*\(\".*\)/\1${admin_net_if}\2/' /etc/udev/rules.d/70-persistent-net.rules"
    until [[ -z $1 ]] ; do
        vfm_mac=$(virsh -c "${dkvm_uri}"  domiflist "${fvm_name}" | awk "/vfm_$1/ { print \$5}")
        ssh "${admin_net_ip_bs}" "sed -i -e 's/\(${vfm_mac}.* NAME=\"\)[^\"]*\(\".*\)/\1${1}\2/' /etc/udev/rules.d/70-persistent-net.rules"
        shift || true
        shift || true
        shift || true
    done
}


set_log(){
    #  keep current console output on file descriptor &6  and redirect stderr and stdout into file
    if [[ ! -z "$migrate_log" ]] ; then
        exec 6>&1
        exec 1>>$migrate_log
        exec 2>&1
    fi
    set -x
}

timeout(){
    if [[ "$(( $(date +%s) - ${start_time} ))" -ge "$max_worktime" ]] ; then
        echo "Timeout !"
        return 1
    fi
    return 0
}

# Stop script execution if script is sourced
[[ "${BASH_SOURCE[0]}" != "${0}" ]] && return
# Show usage if run without parameters.
[[ -z $1 ]] && usage
# Check if already running
file_lock

if [[ "$1" == "start" ]] && [[ -f "$fuel_migrate_vars" ]] ; then

    source "$fuel_migrate_vars"
    set_log
    cecho "Start data sync phase"
    mv -f "$fuel_migrate_vars" "${fuel_migrate_vars}.old"
    ifup "$admin_net_if"
    /etc/init.d/network start
    /etc/init.d/sshd start
    initctl start tty TTY=/dev/tty2
    cecho "You may login from tty2. Switch by Ctr-Alt-F2"
    time rsync_data
    fix_config
    rm -f /etc/rc3.d/S00fuel_migrate
    cecho "Reboot destination VM"
    ssh -tt "${admin_net_ip_bs}" "reboot"
    cecho "Stop network and up with new settings"
    /etc/init.d/network stop
    cecho "Source fuelmaster can be obtained by ssh ${admin_net_ip_bs}"
    ifconfig "$admin_net_if" "${admin_net_ip_bs}" netmask "$admin_net_nm"

    until ssh -tt ${admin_net_ip} ls ; do
        timeout  || reboot
        sleep 20
    done


    cat >/tmp/${banner_file} <<EOF
#!/bin/bash
echo "

                     Congratulation! You are on cloned Fuel now!

    The migration tasks have completed. The clone should be up and functioning
correctly. You should now check the new system and inspect the log stored in
${migrate_log}.

    After the reboot of the Fuel master, it may require additional time before
it becomes fully operational. Please allow at least 10 minutes for the system
to become active.

    The source Fuel master is still up in maintenance mode. Once you have
verified that the new Fuel master is functioning correctly, you can shut down
or wipe the original Fuel master. The source Fuel master can be reached by
using ssh to ${admin_net_ip_bs}. Should you want to wipe the disk for bootstraping,
you may do so by running the following:

# ssh ${admin_net_ip_bs} dd if=/dev/zero of=/dev/null count=1024 bs=1024 conv=fdatasync
# ssh ${admin_net_ip_bs} reboot -f

    To disable this message, run:

# rm /etc/profile.d/${banner_file}

    If the new Fuel master is not functioning correctly, you may return the
source Fuel master back into an operational state by running:

# ssh ${admin_net_ip_bs} reboot && ssh -tt ${dkvm_ip} virsh destroy ${fvm_name}

    This command will reboot the source Fuel master and remove the new
Fuel master clone. To prevent IP conflict, this command should be as indicated.
If you are performing this command over ssh, your session may hang and you may
need to reestablish your ssh connection.


"
EOF

    chmod +x /tmp/${banner_file}
    rsync /tmp/${banner_file} ${admin_net_ip}:/etc/profile.d/${banner_file}
    # refresh log on destination node
    rsync ${migrate_log} ${admin_net_ip}:${migrate_log}

    while true ; do sleep 3600 ; done

else
    dkvm_ip="$(ssh $1 echo \$SSH_CONNECTION)"
    dkvm_ip="$(echo $dkvm_ip | awk '{print $3}')"
    shift
    for i in "$@" ; do
        if [[ "${i:0:2}" == "--" ]] ; then
            vname="${i:2}"
            vvalue="${vname#*=}"
            vname="${vname%=*}"
            [[ "$vvalue" == "$vname" ]] && vvalue=yes
            if  echo " $config_bundle " | grep -q " $vname " ; then
                eval "${vname%=*}=${vvalue}"
            else
                usage "Invalid options $i"
            fi
        else
            usage "Invalid options $i"
        fi

    done

    migrate_log="${migrate_log:-/var/log/fuel-migrate.log}"
    set_log
    date
    start_time=$(date +%s)

    # trap unexpected exit
    trap "{ set +x ; flock -u $fd ; cecho \"ERROR: look at ${migrate_log}\"; tail ${migrate_log} >&6; exit ; }" EXIT

    max_worktime="${max_worktime:-7200}"
    fuel_astute="${fuel_astute:-/etc/fuel/astute.yaml}"
    dkvm_ip="${dkvm_ip:?Destination compute not set}"
    dkvm_folder="${dkvm_folder:-/var/lib/nova/}"

    fvm_disk_size="${fvm_disk_size:-100g}"
    fvm_name="${fvm_name:-fuel_master}"
    fvm_ram="${fvm_ram:-2048}"
    fvm_cpu="${fvm_cpu:-2}"

    dkvm_uri="qemu+ssh://root@${dkvm_ip}/system"

    admin_net_ip="$(yaml_var $fuel_astute ADMIN_NETWORK ipaddress)"
    admin_net_if="$(yaml_var $fuel_astute ADMIN_NETWORK interface)"
    admin_net_nm="$(yaml_var $fuel_astute ADMIN_NETWORK netmask)"
    admin_net_mac="$(yaml_var $fuel_astute ADMIN_NETWORK mac)"
    admin_net_br="${admin_net_br:-br-fw-admin}"
    s_base=/tmp/root
    fm_reboot="${fm_reboot:-yes}"

    src_disk="$(mount | awk '/\/boot / { gsub (substr($1,length($1)),"",$1); print $1 } \')"
    dst_disk="${dst_disk:-/dev/vda}"

    [[ -z "$os_swap" ]] && os_swap="$((${fvm_ram}*2))"
    [[ -z "$os_root" ]] && os_root="$(lvdisplay --units g -C --noheadings -o lv_size /dev/os/root)"

    os_var="${os_var:-20%}"
    os_varlibdocker="${os_varlibdocker:-30%}"
    os_varlog="${os_varlog:-100%}"
    del_vm=${del_vm:-no}

    create_vm
    IFS=","
    # no ""
    fix_udev_rule $other_net_bridges
    IFS=" "
    mark_dst_disk
    mk_lvs_fs
    save_vars $config_bundle $work_bundle
    # create fake service for rsyncing
    ln -fsv "$(readlink -e $0)" /etc/rc3.d/S00fuel_migrate
    flock -u $fd
    if [[ "$fm_reboot" == "yes" ]] ; then
        cecho "Rebooting to begin the data sync process"
        reboot
    else
        cecho "Destination VM has been created. Please reboot node to begin data sync process."
    fi
    trap - EXIT
fi
