# virt-p2v.sh is a shell script which performs a physical to
# 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.
-# greeting=no
+# Use 'greeting=no' to suppress greeting and final verification screens.
greeting=
override_remote_host=
# (only if override_remote_transport is 'ssh')
override_remote_directory=
-# list of devices to send, separated by spaces, if empty ask user
+# 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 /dev/sda1 snap
+# snapshot sda1 snap
# creates a snapshot of /dev/sda1 called /dev/mapper/snap
-
-
-# XXX Error checking.
function snapshot {
- dev=$1
- name=$2
+ 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.
- sectors=`blockdev --getsize $dev`
+ local sectors=`blockdev --getsize /dev/$dev`
dmsetup create ${name}_org \
- --table="0 $sectors snapshot-origin $dev"
+ --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/ram1 n 64"
+ --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.
# 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:]' _`
+}
#----------------------------------------------------------------------
-# General script setup.
+# Network configuration functions.
-# We can safely write files into /tmp without modifying anything.
-cd /tmp
+# `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.
# (unless [Back] is pressed, in which case it jumps back to the previous
# state). Finally the 'exit' state causes us to quit the loop.
-state=hostname
+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 \
- --nocancel \
+ --extra-button --extra-label "Back" --nocancel \
--inputbox "Remote host" 10 50 "$remote_host" \
2> line
- remote_host=`cat line`
- state=port
+ 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 \
if [ $? -eq 3 ]; then state=hostname
else
remote_port=`cat line`
- state=exit
+ 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=hostname
+ state=transport
;;
esac
done
clear
-echo remote_host $remote_host
-echo remote_port $remote_port
\ No newline at end of file
+
+#----------------------------------------------------------------------
+# 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/sd*
+ 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
+