gzip network traffic.
[virt-p2v.git] / virt-p2v.sh
1 #!/bin/bash
2 #
3 # virt-p2v.sh is a shell script which performs a physical to
4 # virtual conversion of local disks.
5 #
6 # Copyright (C) 2007 Red Hat Inc.
7 # Written by Richard W.M. Jones <rjones@redhat.com>
8 #
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program; if not, write to the Free Software
21 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
22 #
23 # $Id$
24
25 export PATH=/usr/sbin:/sbin:/usr/local/bin:/usr/kerberos/bin:/usr/bin:/bin
26
27 # The defaults here make a generic virt-p2v.sh script, but if you want
28 # to build a partially-/fully-automatic P2V solution, then you can set
29 # these variables to something, and the script won't ask the user for
30 # input.
31
32 # Use 'greeting=no' to suppress greeting and final verification screens.
33 greeting=
34
35 override_remote_host=
36 override_remote_port=
37
38 # can be 'ssh' or 'tcp'
39 override_remote_transport=
40
41 # eg. override_remote_directory=/var/lib/xen/images
42 # (only if override_remote_transport is 'ssh')
43 override_remote_directory=
44
45 # list of local physical devices to send, separated by spaces,
46 # if empty ask user.
47 # this is usually a list of whole disk devices (eg. "sda")
48 override_devices_to_send=""
49
50 # the root filesystem containing /etc/fstab (eg. "sda3" or
51 # "VolGroup00/LogVol00")
52 override_root_filesystem=""
53
54 # network configuration
55 #  - if empty, ask user
56 #  - "auto" means try to autoconfigure from root filesystem
57 # (XXX needs to contain more ways to override in future)
58 override_network=""
59
60 #----------------------------------------------------------------------
61 # Logging.
62
63 function log {
64     echo "$@" >> /tmp/virt-p2v.log
65 }
66
67 #----------------------------------------------------------------------
68 # Helper functions.
69
70 # 'matches_regexp regexp string' returns true if string matches regexp.
71 # It uses egrep for extended regexps.
72 function matches_regexp {
73     local re="$1"; shift
74     echo "$@" | grep -Esq "$re"
75 }
76
77 # 'string_contains needle haystack' returns true if needle in haystack.
78 function string_contains {
79     echo "$2" | grep -Fsq "$1"
80 }
81
82 # 'word_in_list word ...'.  '...' (list of parameters) is a list of
83 # words.  Returns true if 'word' is in the list.
84 function word_in_list {
85     local word="$1"; shift
86     local w
87     for w in "$@"; do
88         [ "$w" = "$word" ] && return 0
89     done
90     return 1
91 }
92
93 #----------------------------------------------------------------------
94 # I/O functions
95
96 # Use the function read_line instead of the shell built-in read.
97 # It reads from the console.
98 function read_line {
99     read "$*" </dev/console
100 }
101
102 # Launch a bash subshell connected to the console.
103 function shell {
104     PS1='\u@\h:\w\$ ' bash </dev/console >/dev/console 2>&1
105 }
106
107 #----------------------------------------------------------------------
108 # Device mapper snapshotting.
109
110 next_ramdisk=1
111
112 # Create a device-mapper snapshot of a device with ramdisk overlay.
113 # Example:
114 #   snapshot sda1 snap
115 # creates a snapshot of /dev/sda1 called /dev/mapper/snap
116 function snapshot {
117     local dev=$1 name=$2
118
119     # Next free ramdisk (/dev/ram$i)
120     local i=$next_ramdisk
121     next_ramdisk=$(($next_ramdisk+1))
122
123     # Get size of the device in sectors.
124     local sectors=`blockdev --getsize /dev/$dev`
125
126     dmsetup create ${name}_org \
127         --table="0 $sectors snapshot-origin /dev/$dev"
128     if [ $? -ne 0 ]; then exit 1; fi
129     dmsetup create $name \
130         --table="0 $sectors snapshot /dev/mapper/${name}_org /dev/ram$i n 64"
131     if [ $? -ne 0 ]; then exit 1; fi
132 }
133
134 # Drop an existing snapshot created by snapshot function.
135 # Example:
136 #   drop_snapshot snap
137 # drops a snapshot called /dev/mapper/snap
138 function drop_snapshot {
139     local name=$1
140
141     dmsetup remove /dev/mapper/$name
142     dmsetup remove /dev/mapper/${name}_org
143 }
144
145 #----------------------------------------------------------------------
146 # Searching for devices, partitions and LVs.
147
148 # Convert "/dev/foo" into "foo".  Returns $device.
149 function strip_slash_dev {
150     device=$(echo "$1" | sed 's|^/dev/||')
151 }
152
153 # The 'lvs' utility returns devices like '/dev/sda2(0)'.  I've no
154 # idea what the number in parentheses is, but remove /dev/ and the
155 # strange number.
156 function device_of_lvs_device {
157     strip_slash_dev "$1"
158     device=$(echo "$device" | sed 's|(.*)$||')
159 }
160
161 # All functions in this section assume that physical block devices
162 # (things corresponding to hard disks in the system) are called
163 # hd[a-z]+ or sd[a-z]+
164
165 # Get list of physical block devices.  Sets variable $devices
166 # to something like "sda sdb".
167 function search_devices {
168     devices=$(cd /sys/block && /bin/ls -d [hs]d*)
169 }
170
171 # Get list of partitions from a physical block device.  Sets
172 # variable $partitions to something like "sda1 sda2 sda3".
173 # See also: search_parts
174 function get_partitions {
175     partitions=$(cd /sys/block/"$1" && /bin/ls -d "$1"*)
176 }
177
178 # Given a partition (eg. "sda1" or "VolGroup00/LogVol00") return
179 # the physical block device which contains it.  In the case where
180 # we are given an LV, this is supposed to do the right thing and
181 # go back through the layers until it gets to the physical block
182 # device.  Returns $device set to something like "sda".
183 function block_device_of_part {
184     local part="$1" vg_name lv_name pv
185
186     if matches_regexp '^[hs]d[a-z]+[0-9]*$' "$part"; then
187         device=$(echo "$part" | sed 's|[0-9]*$||')
188         return 0
189     fi
190
191     # Not a partition name, so it's a LV name.  Ask lvs to
192     # do the hard work.
193     lvs --noheadings -o vg_name,lv_name,devices > lvs
194     while read vg_name lv_name pv; do
195         if [ "$vg_name/$lv_name" = "$part" \
196             -o "mapper/$vg_name-$lv_name" = "$part" ]; then
197             device_of_lvs_device "$pv"  ;# sets $device to (eg.) "sda1"
198             block_device_of_part "$device"
199             return 0
200         fi
201     done < lvs
202
203     # Help ... block device not found.
204     log block_device_of_part: block device cannot be resolved: $part
205     device="$part"
206 }
207
208 # search_parts $devices examines the list of devices and looks for
209 # partitions or LVs on just those devices.  Sets variable $parts to
210 # something like "sda1 VolGroup00/LogVol00".
211 function search_parts {
212     local vg_name lv_name pv pvs="" device partition partitions
213
214     parts=""
215
216     lvs --noheadings -o vg_name,lv_name,devices > lvs
217     while read vg_name lv_name pv; do
218         # Get just the partition name (eg. "sda2").
219         device_of_lvs_device "$pv"; pv="$device"
220         # Get just the block device name (eg. "sda").
221         block_device_of_part "$pv"
222
223         log search_parts: pv $pv device $device lv $vg_name/$lv_name
224
225         # A device in our list of devices to consider?
226         if word_in_list $device "$@"; then
227             log search_parts: adding $vg_name/$lv_name
228             parts="$parts $vg_name/$lv_name"
229             pvs="$pvs $pv"
230         fi
231     done < lvs
232
233     log search_parts: after lvs, parts $parts pvs $pvs
234
235     # Look for non-LVM partitions, but ignore any which are PVs
236     # as identified above.
237     for device in "$@"; do
238         get_partitions "$device"
239         for partition in $partitions; do
240             if ! word_in_list $partition $pvs; then
241                 log search_parts: adding $partition
242                 parts="$parts $partition"
243             fi
244         done
245     done
246
247     # $parts is returned.
248 }
249
250 #----------------------------------------------------------------------
251 # Network configuration functions.
252
253 # `auto_network' tries to configure the network from the
254 # root filesystem.  Returns true or false.
255 function auto_network {
256     # Make sure this file exists, otherwise Fedora gives a warning.
257     touch /etc/resolv.conf
258
259     pushd /etc/sysconfig
260
261     mv network network.saved
262     mv networking networking.saved
263     mv network-scripts network-scripts.saved
264
265     # Originally I symlinked these, but that causes dhclient to
266     # keep open /mnt/root (as its cwd is in network-scripts subdir).
267     # So now we will copy them recursively instead.
268     cp -r /mnt/root/etc/sysconfig/network .
269     cp -r /mnt/root/etc/sysconfig/networking .
270     cp -r /mnt/root/etc/sysconfig/network-scripts .
271
272     /etc/init.d/network start
273     local status=$?
274
275     rm -rf network networking network-scripts
276     mv network.saved network
277     mv networking.saved networking
278     mv network-scripts.saved network-scripts
279
280     popd
281
282     ping -c 3 $remote_host
283
284     if [ "$greeting" != "no" ]; then
285         echo "Did automatic network configuration work?"
286         echo "(Hint: if not sure, there is a shell on console [ALT] [F2])"
287         echo -n "    (y/n) "
288         local line
289         read_line line </dev/console
290         if [ "$line" = "y" -o "$line" = "yes" ]; then return 0; fi
291         return 1
292     fi
293
294     # In non-interactive mode, return the status of /etc/init.d/network.
295     return $status
296 }
297
298
299 #----------------------------------------------------------------------
300 # General script setup.
301
302 # We can safely write files into /tmp without modifying anything.
303 cd /tmp
304
305 #----------------------------------------------------------------------
306 # Dialog with the user.
307
308 log
309 log virt-p2v starting up at `date`
310
311 if [ "$greeting" != "no" ]; then
312     dialog \
313         --title "virt-p2v" \
314         --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
315 fi
316
317 # Get configuration from the user.
318
319 # To make the [Back] button work, we make this into a looping state
320 # machine.  Each state asks a question and jumps to the next state
321 # (unless [Back] is pressed, in which case it jumps back to the previous
322 # state).  Finally the 'exit' state causes us to quit the loop.
323
324 remote_port=22
325 remote_directory=/var/lib/xen/images
326 state=transport
327
328 while [ "$state" != "exit" ]; do
329     log next state = $state
330     case "$state" in
331         transport)
332             if [ -n "$override_remote_transport" ]; then
333                 remote_transport="$override_remote_transport"
334                 state=hostname
335             else
336                 case "$remote_transport" in
337                     tcp) ssh_on=off; tcp_on=on;;
338                     *) ssh_on=on; tcp_on=off;;
339                 esac
340                 dialog \
341                     --nocancel \
342                     --radiolist "Connection type" 10 50 2 \
343                     "ssh" "SSH (secure shell - recommended)" $ssh_on \
344                     "tcp" "TCP socket" $tcp_on \
345                     2> line
346                 remote_transport=`cat line`
347                 state=hostname
348             fi
349             ;;
350         hostname)
351             if [ -n "$override_remote_host" ]; then
352                 remote_host="$override_remote_host"
353                 state=port
354             else
355                 dialog \
356                     --extra-button --extra-label "Back" --nocancel \
357                     --inputbox "Remote host" 10 50 "$remote_host" \
358                     2> line
359                 if [ $? -eq 3 ]; then state=transport
360                 else
361                     remote_host=`cat line`
362                     if [ -n "$remote_host" ]; then state=port; fi
363                     # else stay in same state and demand a hostname!
364                 fi
365             fi
366             ;;
367         port)
368             if [ -n "$override_remote_port" ]; then
369                 remote_port="$override_remote_port"
370                 state=directory
371             else
372                 dialog \
373                     --extra-button --extra-label "Back" --nocancel \
374                     --inputbox "Remote port" 10 50 "$remote_port" \
375                     2> line
376                 if [ $? -eq 3 ]; then state=hostname
377                 else
378                     remote_port=`cat line`
379                     state=directory
380                 fi
381             fi
382             ;;
383         directory)
384             if [ "$remote_transport" = "tcp" ]; then
385                 state=devices
386             elif [ -n "$override_remote_directory" ]; then
387                 remote_directory="$override_remote_directory"
388                 state=devices
389             else
390                 dialog \
391                     --extra-button --extra-label "Back" --nocancel \
392                     --inputbox "Remote directory containing guest images" \
393                     10 50 \
394                     "$remote_directory" \
395                     2> line
396                 if [ $? -eq 3 ]; then state=port
397                 else
398                     remote_directory=`cat line`
399                     state=devices
400                 fi
401             fi
402             ;;
403
404         # Block devices configuration.
405         devices)
406             if [ -n "$override_devices_to_send" ]; then
407                 devices_to_send="$override_devices_to_send"
408                 state=root
409             else
410                 # Returns the list of physical devices in $devices
411                 search_devices
412
413                 log devices returned by search_devices: $devices
414
415                 deviceslist=""
416                 for d in $devices; do
417                     if word_in_list $d $devices_to_send; then
418                         stat=on
419                     else
420                         stat=off
421                     fi
422                     gigs=$(($(blockdev --getsize /dev/$d)/2/1024/1024))
423                     deviceslist="$deviceslist $d /dev/$d(${gigs}GB) $stat"
424                 done
425
426                 dialog \
427                     --extra-button --extra-label "Back" --nocancel \
428                     --single-quoted \
429                     --checklist "Pick disks to send" 15 50 8 \
430                     $deviceslist \
431                     2> line
432                 if [ $? -eq 3 ]; then state=directory
433                 else
434                     devices_to_send=`cat line`
435                     state=root
436                 fi
437             fi
438             ;;
439
440         # Root filesystem.
441         root)
442             if [ -n "$override_root_filesystem" ]; then
443                 root_filesystem="$override_root_filesystem"
444                 state=network
445             else
446                 # Returns the list of possible partitions / LVs in $parts
447                 search_parts $devices_to_send
448
449                 log partitions returned by search_parts: $parts
450
451                 partslist=""
452                 for r in $parts; do
453                     if word_in_list $r $root_filesystem; then
454                         stat=on
455                     else
456                         stat=off
457                     fi
458                     partslist="$partslist $r /dev/$r $stat"
459                 done
460
461                 dialog \
462                     --extra-button --extra-label "Back" --nocancel \
463                     --single-quoted \
464                     --radiolist "Pick partition containing the root (/) filesystem" 10 70 5 \
465                     $partslist \
466                     2> line
467                 if [ $? -eq 3 ]; then state=devices
468                 else
469                     root_filesystem=`cat line`
470                     state=network
471                 fi
472             fi
473             ;;
474
475         # Network configuration.
476         network)
477             if [ -n "$override_network" ]; then
478                 network="$override_network"
479                 state=verify
480             else
481                 dialog \
482                     --extra-button --extra-label "Back" --nocancel \
483                     --radiolist "Network configuration" 10 70 5 \
484                     "auto" "Auto-configure from root filesystem" on \
485                     "ask" "Manual configuration" off \
486                     "sh" "Configure from the shell" off \
487                     2> line
488                 if [ $? -eq 3 ]; then state=root
489                 else
490                     network=`cat line`
491                     state=verify
492                 fi
493             fi
494             ;;
495
496         # Verify configuration.
497         verify)
498             if [ "$greeting" = "no" ]; then
499                 state=exit
500             else
501                 dialog \
502                     --title "Summary" \
503                     --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?" \
504                     18 70
505                 if [ $? -eq 1 ]; then
506                     state=transport
507                 else
508                     state=exit
509                 fi
510             fi
511             ;;
512         *)
513             echo "Invalid state: $state"
514             state=transport
515             ;;
516     esac
517 done
518
519 clear
520
521 #----------------------------------------------------------------------
522 # De-activate all volume groups and switch to new dm-only LVM config.
523 log deactivating volume groups
524
525 vgchange -a n
526 mv /etc/lvm/lvm.conf /etc/lvm/lvm.conf.old
527 mv /etc/lvm/lvm.conf.new /etc/lvm/lvm.conf
528 rm -f /etc/lvm/cache/.cache
529
530 # Snapshot the block devices.
531 for d in $devices_to_send; do
532     log snapshotting block device /dev/$d ...
533
534     snapshot $d snap_$d
535
536     # The block devices are whole disks.  Use kpartx to repartition them.
537     log running kpartx -a /dev/mapper/snap_$d ...
538     kpartx -a /dev/mapper/snap_$d
539 done
540
541 # Rescan for LVs.
542 log running vgscan
543 vgscan
544 vgchange -a y
545
546 # Mount the root filesystem on /mnt/root.  If it's a physical
547 # device then we want to mount (eg) /dev/mapper/snap_sda2.
548 # If it's a LVM device then we can just mount the LVM partition.
549
550 log mount $root_filesystem as /mnt/root
551
552 if [ -f /dev/mapper/snap_$root_filesystem ]; then
553     mount /dev/mapper/snap_$root_filesystem /mnt/root
554 else
555     mount /dev/$root_filesystem /mnt/root
556 fi
557
558 #----------------------------------------------------------------------
559 # Now see if we can get a network configuration.
560 log network configuration $network
561
562 case "$network" in
563     sh)
564         echo "Network configuration"
565         echo
566         echo "Please configure the network from this shell."
567         echo
568         echo "When finished, exit with ^D or exit"
569         echo
570         shell
571         ;;
572
573     ask)
574         # XXX Not implemented
575         echo "Sorry, we didn't implement this one yet."
576         shell
577         ;;
578
579     auto)
580         echo "Trying to auto-configure network from root filesystem ..."
581         echo
582         if ! auto_network; then
583             echo "Auto-configuration failed.  Starting a shell."
584             echo
585             shell
586         fi
587 esac
588
589 #----------------------------------------------------------------------
590 # Rewrite /mnt/root/etc/fstab
591
592 log rewriting /etc/fstab
593
594 cp /mnt/root/etc/fstab /mnt/root/etc/fstab.p2vsaved
595 while read dev mountpoint fstype options freq passno; do
596     # If device is /dev/sd* then on the target fullvirt machine
597     # it will be /dev/sd*
598     if matches_regexp "^/dev/sd[[:alpha:]]+[[:digit:]]+$" "$dev"; then
599         dev=`echo $dev | sed 's|^/dev/sd|/dev/hd|'`
600     fi
601
602     # Print out again with all the fields nicely lined up.
603     printf "%-23s %-23s %-7s %-15s %d %d\n" \
604         "$dev" "$mountpoint" "$fstype" "$options" "$freq" "$passno"
605 done < /mnt/root/etc/fstab.p2vsaved > /mnt/root/etc/fstab
606
607 #----------------------------------------------------------------------
608 # XXX Look for other files which might need to be changed here ...
609
610
611
612 # We've now finished with /mnt/root (the real root filesystem),
613 # so unmount it and synch everything.
614 umount /mnt/root
615 sync
616
617 #----------------------------------------------------------------------
618 # Send the device snapshots (underlying device + changes in ramdisk)
619 # to the remote server.
620
621 log sending disks
622
623 # XXX Effectively this is using the hostname derived from network
624 # configuration, but we might want to ask the user instead.
625 # XXX How do we ensure that we won't overwrite target files?  Currently
626 # tries to use the current date as a uniquifier.
627
628 # Names will be something like
629 # p2v-oirase-200709011249-hda.img
630 basename=p2v-`hostname -s|tr -cd '[0-9a-zA-Z]'`-`date +'%Y%m%d%H%M'`
631
632 for dev in $devices_to_send; do
633     rdev=`echo $dev | sed 's|^sd|hd|'`
634     name="$basename-$rdev.img"
635     log sending $dev to $name
636
637     sectors=`blockdev --getsize /dev/mapper/snap_$dev`
638
639     gigs=$(($sectors/2/1024/1024))
640     echo "Sending /dev/$dev (${gigs} GB) to remote machine"
641
642     dd if=/dev/mapper/snap_$dev | gzip --best |
643     case "$remote_transport" in
644         ssh)
645             ssh -p "$remote_port" "$remote_host" \
646                 "zcat > $remote_directory/$name"
647             ;;
648         tcp)
649             echo "p2v $name $sectors" > header
650             cat header - | nc "$remote_host" "$remote_port"
651             ;;
652     esac
653 done
654
655
656 #----------------------------------------------------------------------
657 # Clean up.
658
659 #for d in $devices_to_send; do
660 #    kpartx -d /dev/mapper/snap_$d
661 #    drop_snapshot snap_$d
662 #done
663
664 # This file must end with a newline
665