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