Add default path and port to dialog.
[virt-p2v.git] / virt-p2v.sh
index 16d3fd9..2bc82a4 100755 (executable)
@@ -1,9 +1,702 @@
 #!/bin/bash
 #
 # virt-p2v.sh is a shell script which performs a physical to
-# virtual conversion of local disks, interactively.
+# virtual conversion of local disks.
 #
-# By Richard W.M. Jones <rjones@redhat.com>
+# Copyright (C) 2007 Red Hat Inc.
+# Written by Richard W.M. Jones <rjones@redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+# $Id$
+
+# Because we're running from a start-up script, we don't have much
+# of a login environment, so set one up.
+export PATH=/usr/sbin:/sbin:/usr/local/bin:/usr/kerberos/bin:/usr/bin:/bin
+export HOME=/root
+export LOGNAME=root
+
+# The defaults here make a generic virt-p2v.sh script, but if you want
+# to build a partially-/fully-automatic P2V solution, then you can set
+# these variables to something, and the script won't ask the user for
+# input.
+
+# Use 'greeting=no' to suppress greeting and final verification screens.
+greeting=
+
+override_remote_host=
+override_remote_port=
+
+# can be 'ssh' or 'tcp'
+override_remote_transport=
+
+# eg. override_remote_directory=/var/lib/xen/images
+# (only if override_remote_transport is 'ssh')
+override_remote_directory=
+
+# list of local physical devices to send, separated by spaces,
+# if empty ask user.
+# this is usually a list of whole disk devices (eg. "sda")
+override_devices_to_send=""
+
+# the root filesystem containing /etc/fstab (eg. "sda3" or
+# "VolGroup00/LogVol00")
+override_root_filesystem=""
+
+# network configuration
+#  - if empty, ask user
+#  - "auto" means try to autoconfigure from root filesystem
+# (XXX needs to contain more ways to override in future)
+override_network=""
+
+#----------------------------------------------------------------------
+# General script setup and logging.
+
+exec 2>> /tmp/virt-p2v.log
+
+function log {
+    echo "$@" 1>&2
+}
+
+log
+log virt-p2v starting up at `date`
+
+# The first and only parameter must be the tty.  Connect
+# stdin/stdout to this device.
+if [ -n "$1" ]; then
+    log connecting to /dev/$1
+    exec </dev/$1 >/dev/$1
+fi
+
+# We can safely write files into /tmp without modifying anything.
+cd /tmp
+
+#----------------------------------------------------------------------
+# Helper functions.
+
+# 'matches_regexp regexp string' returns true if string matches regexp.
+# It uses egrep for extended regexps.
+function matches_regexp {
+    local re="$1"; shift
+    echo "$@" | grep -Esq "$re"
+}
+
+# 'string_contains needle haystack' returns true if needle in haystack.
+function string_contains {
+    echo "$2" | grep -Fsq "$1"
+}
+
+# 'word_in_list word ...'.  '...' (list of parameters) is a list of
+# words.  Returns true if 'word' is in the list.
+function word_in_list {
+    local word="$1"; shift
+    local w
+    for w in "$@"; do
+       [ "$w" = "$word" ] && return 0
+    done
+    return 1
+}
+
+#----------------------------------------------------------------------
+# I/O functions
+
+# Use the function read_line instead of the shell built-in read.
+# It reads from the console.
+function read_line {
+    read "$*"
+}
+
+# Launch a bash subshell connected to the console.
+function shell {
+    PS1='\u@\h:\w\$ ' bash
+}
+
+#----------------------------------------------------------------------
+# Device mapper snapshotting.
+
+next_ramdisk=1
+
+# Create a device-mapper snapshot of a device with ramdisk overlay.
+# Example:
+#   snapshot sda1 snap
+# creates a snapshot of /dev/sda1 called /dev/mapper/snap
+function snapshot {
+    local dev=$1 name=$2
+
+    # Next free ramdisk (/dev/ram$i)
+    local i=$next_ramdisk
+    next_ramdisk=$(($next_ramdisk+1))
+
+    # Get size of the device in sectors.
+    local sectors=`blockdev --getsize /dev/$dev`
+
+    dmsetup create ${name}_org \
+       --table="0 $sectors snapshot-origin /dev/$dev"
+    if [ $? -ne 0 ]; then exit 1; fi
+    dmsetup create $name \
+       --table="0 $sectors snapshot /dev/mapper/${name}_org /dev/ram$i n 64"
+    if [ $? -ne 0 ]; then exit 1; fi
+}
+
+# Drop an existing snapshot created by snapshot function.
+# Example:
+#   drop_snapshot snap
+# drops a snapshot called /dev/mapper/snap
+function drop_snapshot {
+    local name=$1
+
+    dmsetup remove /dev/mapper/$name
+    dmsetup remove /dev/mapper/${name}_org
+}
+
+#----------------------------------------------------------------------
+# Searching for devices, partitions and LVs.
+
+# Convert "/dev/foo" into "foo".  Returns $device.
+function strip_slash_dev {
+    device=$(echo "$1" | sed 's|^/dev/||')
+}
+
+# The 'lvs' utility returns devices like '/dev/sda2(0)'.  I've no
+# idea what the number in parentheses is, but remove /dev/ and the
+# strange number.
+function device_of_lvs_device {
+    strip_slash_dev "$1"
+    device=$(echo "$device" | sed 's|(.*)$||')
+}
+
+# All functions in this section assume that physical block devices
+# (things corresponding to hard disks in the system) are called
+# hd[a-z]+ or sd[a-z]+
+
+# Get list of physical block devices.  Sets variable $devices
+# to something like "sda sdb".
+function search_devices {
+    devices1=$(cd /sys/block && /bin/ls -d [hs]d*)
+    log search_devices: devices1: $devices1
+    # Ignore devices which fail 'blockdev --getsize' - probably
+    # removable devices or other strange ones.
+    devices=""
+    for d in $devices1; do
+       if blockdev --getsize /dev/$d > /dev/null; then
+           devices="$devices${devices:+ }$d"
+       fi
+    done
+    log search_devices: devices: $devices
+}
+
+# Get list of partitions from a physical block device.  Sets
+# variable $partitions to something like "sda1 sda2 sda3".
+# See also: search_parts
+function get_partitions {
+    partitions=$(cd /sys/block/"$1" && /bin/ls -d "$1"*)
+}
+
+# Given a partition (eg. "sda1" or "VolGroup00/LogVol00") return
+# the physical block device which contains it.  In the case where
+# we are given an LV, this is supposed to do the right thing and
+# go back through the layers until it gets to the physical block
+# device.  Returns $device set to something like "sda".
+function block_device_of_part {
+    local part="$1" vg_name lv_name pv
+
+    if matches_regexp '^[hs]d[a-z]+[0-9]*$' "$part"; then
+       device=$(echo "$part" | sed 's|[0-9]*$||')
+       return 0
+    fi
+
+    # Not a partition name, so it's a LV name.  Ask lvs to
+    # do the hard work.
+    lvs --noheadings -o vg_name,lv_name,devices > lvs
+    while read vg_name lv_name pv; do
+       if [ "$vg_name/$lv_name" = "$part" \
+           -o "mapper/$vg_name-$lv_name" = "$part" ]; then
+           device_of_lvs_device "$pv"  ;# sets $device to (eg.) "sda1"
+           block_device_of_part "$device"
+           return 0
+       fi
+    done < lvs
+
+    # Help ... block device not found.
+    log block_device_of_part: block device cannot be resolved: $part
+    device="$part"
+}
+
+# search_parts $devices examines the list of devices and looks for
+# partitions or LVs on just those devices.  Sets variable $parts to
+# something like "sda1 VolGroup00/LogVol00".
+function search_parts {
+    local vg_name lv_name pv pvs="" device partition partitions
+
+    parts=""
+
+    lvs --noheadings -o vg_name,lv_name,devices > lvs
+    while read vg_name lv_name pv; do
+       # Get just the partition name (eg. "sda2").
+       device_of_lvs_device "$pv"; pv="$device"
+       # Get just the block device name (eg. "sda").
+       block_device_of_part "$pv"
+
+       log search_parts: pv $pv device $device lv $vg_name/$lv_name
+
+       # A device in our list of devices to consider?
+       if word_in_list $device "$@"; then
+           log search_parts: adding $vg_name/$lv_name
+           parts="$parts $vg_name/$lv_name"
+           pvs="$pvs $pv"
+       fi
+    done < lvs
+
+    log search_parts: after lvs, parts $parts pvs $pvs
+
+    # Look for non-LVM partitions, but ignore any which are PVs
+    # as identified above.
+    for device in "$@"; do
+       get_partitions "$device"
+       for partition in $partitions; do
+           if ! word_in_list $partition $pvs; then
+               log search_parts: adding $partition
+               parts="$parts $partition"
+           fi
+       done
+    done
+
+    # $parts is returned.
+}
+
+# This generates a snapshot device name from a device name.
+# eg. sda -> snap_sda
+# Sets $dname.
+function snap_name {
+    dname=`echo -n snap_"$1" | tr -cs '[:alnum:]' _`
+}
+
+#----------------------------------------------------------------------
+# Network configuration functions.
+
+# `auto_network' tries to configure the network from the
+# root filesystem.  Returns true or false.
+function auto_network {
+    # Make sure this file exists, otherwise Fedora gives a warning.
+    touch /etc/resolv.conf
+
+    pushd /etc/sysconfig
+
+    mv network network.saved
+    mv networking networking.saved
+    mv network-scripts network-scripts.saved
+
+    # Originally I symlinked these, but that causes dhclient to
+    # keep open /mnt/root (as its cwd is in network-scripts subdir).
+    # So now we will copy them recursively instead.
+    cp -r /mnt/root/etc/sysconfig/network .
+    cp -r /mnt/root/etc/sysconfig/networking .
+    cp -r /mnt/root/etc/sysconfig/network-scripts .
+
+    /etc/init.d/network start
+    local status=$?
+
+    rm -rf network networking network-scripts
+    mv network.saved network
+    mv networking.saved networking
+    mv network-scripts.saved network-scripts
+
+    popd
+
+    ping -c 3 $remote_host
+
+    if [ "$greeting" != "no" ]; then
+       echo "Did automatic network configuration work?"
+       echo "(Hint: if not sure, there is a shell on console [ALT] [F2])"
+       echo -n "    (y/n) "
+       local line
+       read_line line
+       if [ "$line" = "y" -o "$line" = "yes" ]; then return 0; fi
+       return 1
+    fi
+
+    # In non-interactive mode, return the status of /etc/init.d/network.
+    return $status
+}
+
+#----------------------------------------------------------------------
+# Dialog with the user.
+
+if [ "$greeting" != "no" ]; then
+    dialog \
+       --title "virt-p2v" \
+       --msgbox "\nWelcome to virt-p2v, a live CD for migrating a physical machine to a virtualized host.\n\nTo continue press the Return key.\n\nTo get a shell you can use [ALT] [F2] and log in as root with no password." 17 50
+fi
+
+# Get configuration from the user.
+
+# To make the [Back] button work, we make this into a looping state
+# machine.  Each state asks a question and jumps to the next state
+# (unless [Back] is pressed, in which case it jumps back to the previous
+# state).  Finally the 'exit' state causes us to quit the loop.
+
+remote_port=22
+remote_directory=/var/lib/xen/images
+state=transport
+
+while [ "$state" != "exit" ]; do
+    log next state = $state
+    case "$state" in
+       transport)
+           if [ -n "$override_remote_transport" ]; then
+               remote_transport="$override_remote_transport"
+               state=hostname
+           else
+               case "$remote_transport" in
+                   tcp) ssh_on=off; tcp_on=on;;
+                   *) ssh_on=on; tcp_on=off;;
+               esac
+               dialog \
+                   --nocancel \
+                   --radiolist "Connection type" 10 50 2 \
+                   "ssh" "SSH (secure shell - recommended)" $ssh_on \
+                   "tcp" "TCP socket" $tcp_on \
+                   2> line
+               remote_transport=`cat line`
+               state=hostname
+           fi
+           ;;
+       hostname)
+           if [ -n "$override_remote_host" ]; then
+               remote_host="$override_remote_host"
+               state=port
+           else
+               dialog \
+                   --extra-button --extra-label "Back" --nocancel \
+                   --inputbox "Remote host" 10 50 "$remote_host" \
+                   2> line
+               if [ $? -eq 3 ]; then state=transport
+               else
+                   remote_host=`cat line`
+                   if [ -n "$remote_host" ]; then state=port; fi
+                   # else stay in same state and demand a hostname!
+               fi
+           fi
+           ;;
+       port)
+           if [ -n "$override_remote_port" ]; then
+               remote_port="$override_remote_port"
+               state=directory
+           else
+               dialog \
+                   --extra-button --extra-label "Back" --nocancel \
+                   --inputbox "Remote port" 10 50 "$remote_port" \
+                   2> line
+               if [ $? -eq 3 ]; then state=hostname
+               else
+                   remote_port=`cat line`
+                   state=directory
+               fi
+           fi
+           ;;
+       directory)
+           if [ "$remote_transport" = "tcp" ]; then
+               state=devices
+           elif [ -n "$override_remote_directory" ]; then
+               remote_directory="$override_remote_directory"
+               state=devices
+           else
+               dialog \
+                   --extra-button --extra-label "Back" --nocancel \
+                   --inputbox "Remote directory containing guest images" \
+                   10 50 \
+                   "$remote_directory" \
+                   2> line
+               if [ $? -eq 3 ]; then state=port
+               else
+                   remote_directory=`cat line`
+                   state=devices
+               fi
+           fi
+           ;;
+
+       # Block devices configuration.
+       devices)
+           if [ -n "$override_devices_to_send" ]; then
+               devices_to_send="$override_devices_to_send"
+               state=root
+           else
+               # Returns the list of physical devices in $devices
+               search_devices
+
+               log devices returned by search_devices: $devices
+
+               deviceslist=""
+               for d in $devices; do
+                   if word_in_list $d $devices_to_send; then
+                       stat=on
+                   else
+                       stat=off
+                   fi
+                   log running blockdev --getsize /dev/$d
+                   gigs=$(($(blockdev --getsize /dev/$d)/2/1024/1024))
+                   log /dev/$d has size $gigs
+                   deviceslist="$deviceslist $d /dev/$d(${gigs}GB) $stat"
+               done
+               log deviceslist="$deviceslist"
+
+               dialog \
+                   --extra-button --extra-label "Back" --nocancel \
+                   --single-quoted \
+                   --checklist "Pick disks to send" 15 50 8 \
+                   $deviceslist \
+                   2> line
+               if [ $? -eq 3 ]; then state=directory
+               else
+                   devices_to_send=`cat line`
+                   state=root
+               fi
+           fi
+           ;;
+
+       # Root filesystem.
+       root)
+           if [ -n "$override_root_filesystem" ]; then
+               root_filesystem="$override_root_filesystem"
+               state=network
+           else
+               # Returns the list of possible partitions / LVs in $parts
+               search_parts $devices_to_send
+
+               log partitions returned by search_parts: $parts
+
+               partslist=""
+               for r in $parts; do
+                   if word_in_list $r $root_filesystem; then
+                       stat=on
+                   else
+                       stat=off
+                   fi
+                   partslist="$partslist $r /dev/$r $stat"
+               done
+
+               dialog \
+                   --extra-button --extra-label "Back" --nocancel \
+                   --single-quoted \
+                   --radiolist "Pick partition containing the root (/) filesystem" 10 70 5 \
+                   $partslist \
+                   2> line
+               if [ $? -eq 3 ]; then state=devices
+               else
+                   root_filesystem=`cat line`
+                   state=network
+               fi
+           fi
+           ;;
+
+       # Network configuration.
+       network)
+           if [ -n "$override_network" ]; then
+               network="$override_network"
+               state=verify
+           else
+               dialog \
+                   --extra-button --extra-label "Back" --nocancel \
+                   --radiolist "Network configuration" 10 70 5 \
+                   "auto" "Auto-configure from root filesystem" on \
+                   "ask" "Manual configuration" off \
+                   "sh" "Configure from the shell" off \
+                   2> line
+               if [ $? -eq 3 ]; then state=root
+               else
+                   network=`cat line`
+                   state=verify
+               fi
+           fi
+           ;;
+
+       # Verify configuration.
+       verify)
+           if [ "$greeting" = "no" ]; then
+               state=exit
+           else
+               dialog \
+                   --title "Summary" \
+                   --yesno "Transport: $remote_transport\nRemote host: $remote_host\nRemote port: $remote_port\nRemote directory (ssh only): $remote_directory\nDisks to send: $devices_to_send\nRoot filesystem: $root_filesystem\nNetwork configuration: $network\n\nProceed with these settings?" \
+                   18 70
+               if [ $? -eq 1 ]; then
+                   state=transport
+               else
+                   state=exit
+               fi
+           fi
+           ;;
+       *)
+           echo "Invalid state: $state"
+           state=transport
+           ;;
+    esac
+done
+
+clear
+
+#----------------------------------------------------------------------
+# De-activate all volume groups and switch to new dm-only LVM config.
+log deactivating volume groups
+
+vgchange -a n
+mv /etc/lvm/lvm.conf /etc/lvm/lvm.conf.old
+mv /etc/lvm/lvm.conf.new /etc/lvm/lvm.conf
+rm -f /etc/lvm/cache/.cache
+
+# Snapshot the block devices.
+for d in $devices_to_send; do
+    snap_name $d
+    log snapshotting block device /dev/$d to $dname ...
+
+    snapshot $d $dname
+
+    # The block devices are whole disks.  Use kpartx to repartition them.
+    log running kpartx -a /dev/mapper/$dname ...
+    kpartx -a /dev/mapper/$dname
+done
+
+# Rescan for LVs.
+log running vgscan
+vgscan
+vgchange -a y
+
+# Mount the root filesystem on /mnt/root.  If it's a physical
+# device then we want to mount (eg) /dev/mapper/snap_sda2.
+# If it's a LVM device then we can just mount the LVM partition.
+
+log mount $root_filesystem as /mnt/root
+
+snap_name $root_filesystem
+root_filesystem_dname="$dname"
+
+if [ -b /dev/mapper/$root_filesystem_dname ]; then
+    mount /dev/mapper/$root_filesystem_dname /mnt/root
+else
+    mount /dev/$root_filesystem /mnt/root
+fi
+
+#----------------------------------------------------------------------
+# Now see if we can get a network configuration.
+log network configuration $network
+
+case "$network" in
+    sh)
+       echo "Network configuration"
+       echo
+       echo "Please configure the network from this shell."
+       echo
+       echo "When finished, exit with ^D or exit"
+       echo
+       shell
+       ;;
+
+    ask)
+       # XXX Not implemented
+       echo "Sorry, we didn't implement this one yet."
+       shell
+       ;;
+
+    auto)
+       echo "Trying to auto-configure network from root filesystem ..."
+       echo
+       if ! auto_network; then
+           echo "Auto-configuration failed.  Starting a shell."
+           echo
+           shell
+       fi
+esac
+
+#----------------------------------------------------------------------
+# Rewrite /mnt/root/etc/fstab
+
+log rewriting /etc/fstab
+
+cp /mnt/root/etc/fstab /mnt/root/etc/fstab.p2vsaved
+while read dev mountpoint fstype options freq passno; do
+    # If device is /dev/sd* then on the target fullvirt machine
+    # it will be /dev/hd*
+    if matches_regexp "^/dev/sd[[:alpha:]]+[[:digit:]]+$" "$dev"; then
+       dev=`echo $dev | sed 's|^/dev/sd|/dev/hd|'`
+    fi
+
+    # Print out again with all the fields nicely lined up.
+    printf "%-23s %-23s %-7s %-15s %d %d\n" \
+       "$dev" "$mountpoint" "$fstype" "$options" "$freq" "$passno"
+done < /mnt/root/etc/fstab.p2vsaved > /mnt/root/etc/fstab
+
+#----------------------------------------------------------------------
+# XXX Look for other files which might need to be changed here ...
+
+
+
+# We've now finished with /mnt/root (the real root filesystem),
+# so unmount it and synch everything.
+umount /mnt/root
+sync
+
+#----------------------------------------------------------------------
+# Send the device snapshots (underlying device + changes in ramdisk)
+# to the remote server.
+
+log sending disks
+
+# XXX Effectively this is using the hostname derived from network
+# configuration, but we might want to ask the user instead.
+# XXX How do we ensure that we won't overwrite target files?  Currently
+# tries to use the current date as a uniquifier.
+
+# Names will be something like
+# p2v-oirase-200709011249-hda.img
+basename=p2v-`hostname -s|tr -cd '[0-9a-zA-Z]'`-`date +'%Y%m%d%H%M'`
+
+for dev in $devices_to_send; do
+    rdev=`echo $dev | sed 's|^sd|hd|'`
+    name="$basename-$rdev.img"
+    log sending $dev to $name
+
+    snap_name $dev
+
+    sectors=`blockdev --getsize /dev/mapper/$dname`
+
+    gigs=$(($sectors/2/1024/1024))
+    echo "Sending /dev/$dev (${gigs} GB) to remote machine"
+
+    dd if=/dev/mapper/$dname | gzip --best |
+    case "$remote_transport" in
+       ssh)
+           ssh -p "$remote_port" "$remote_host" \
+               "zcat > $remote_directory/$name"
+           ;;
+       tcp)
+           echo "p2v $name $sectors" > header
+           echo > newline
+           cat header - newline | nc "$remote_host" "$remote_port"
+           ;;
+    esac
+done
+
+
+#----------------------------------------------------------------------
+# Clean up.
 
+#for d in $devices_to_send; do
+#    snap_name $d
+#    kpartx -d /dev/mapper/$dname
+#    drop_snapshot $dname
+#done
 
+# This file must end with a newline