X-Git-Url: http://git.annexia.org/?a=blobdiff_plain;ds=sidebyside;f=virt-p2v.sh;h=2bc82a492ada752703b775bc0575988777102cb8;hb=52433696d9755eb0e6cc5caa0f35b6690ace67fd;hp=44184c703b5c17616b128e125e7fa86d0b8e555f;hpb=cc7a490c49e75e0c0b29098f212ed3c0f3f73612;p=virt-p2v.git diff --git a/virt-p2v.sh b/virt-p2v.sh index 44184c7..2bc82a4 100755 --- a/virt-p2v.sh +++ b/virt-p2v.sh @@ -3,18 +3,37 @@ # virt-p2v.sh is a shell script which performs a physical to # virtual conversion of local disks. # -# By Richard W.M. Jones -# # Copyright (C) 2007 Red Hat Inc. +# Written by Richard W.M. Jones +# +# 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= @@ -27,30 +46,108 @@ override_remote_transport= # (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 +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. @@ -58,16 +155,181 @@ function snapshot { # 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. @@ -85,24 +347,53 @@ fi # (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 \ @@ -111,18 +402,301 @@ while [ "$state" != "exit" ]; do 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/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 +