Kindlier way to do network configuration.
[virt-p2v.git] / virt-p2v.ml
1 #!/usr/bin/ocamlrun /usr/bin/ocaml
2 #load "unix.cma";;
3 #directory "+extlib";;
4 #load "extLib.cma";;
5 #directory "+pcre";;
6 #load "pcre.cma";;
7 #directory "+xml-light";;
8 #load "xml-light.cma";;
9
10 (* virt-p2v.ml is a script which performs a physical to
11  * virtual conversion of local disks.
12  *
13  * Copyright (C) 2007-2008 Red Hat Inc.
14  * Written by Richard W.M. Jones <rjones@redhat.com>
15  *
16  * This program is free software; you can redistribute it and/or modify
17  * it under the terms of the GNU General Public License as published by
18  * the Free Software Foundation; either version 2 of the License, or
19  * (at your option) any later version.
20  *
21  * This program is distributed in the hope that it will be useful,
22  * but WITHOUT ANY WARRANTY; without even the implied warranty of
23  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24  * GNU General Public License for more details.
25  *
26  * You should have received a copy of the GNU General Public License
27  * along with this program; if not, write to the Free Software
28  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
29  *)
30
31 open Unix
32 open Printf
33 open ExtList
34 open ExtString
35
36 type state = { greeting : bool;
37                remote_host : string option; remote_port : string option;
38                remote_directory : string option;
39                network : network option;
40                devices_to_send : string list option;
41                root_filesystem : partition option;
42                hypervisor : hypervisor option;
43                architecture : architecture option;
44                memory : int option; vcpus : int option;
45                mac_address : string option;
46              }
47 and network = Auto | Shell
48 and partition = Part of string * string (* eg. "hda", "1" *)
49               | LV of string * string   (* eg. "VolGroup00", "LogVol00" *)
50 and hypervisor = Xen | QEMU | KVM
51 and architecture = I386 | X86_64 | IA64 | PPC | PPC64 | SPARC | SPARC64
52                  | OtherArch of string | UnknownArch
53
54 (*----------------------------------------------------------------------*)
55 (* TO MAKE A CUSTOM virt-p2v SCRIPT, adjust the defaults in this section.
56  *
57  * If left as they are, then this will create a generic virt-p2v script
58  * which asks the user for each question.  If you set the defaults here
59  * then you will get a custom virt-p2v which is partially or even fully
60  * automated and won't ask the user any questions.
61  *
62  * Note that 'None' means 'no default' (ie. ask the user) whereas
63  * 'Some foo' means use 'foo' as the answer.
64  *)
65 let defaults = {
66   (* If greeting is true, wait for keypress after boot and during
67    * final verification.  Set to 'false' for less interactions.
68    *)
69   greeting = true;
70
71   (* Remote host and port.  Set to 'Some "host"' and 'Some "port"',
72    * else ask the user.
73    *)
74   remote_host = None;
75   remote_port = None;
76
77   (* Remote directory.  Set to 'Some "path"' to set up a
78    * directory path, else ask the user.
79    *)
80   remote_directory = None;
81
82   (* List of devices to send.  Set to 'Some ["sda"; "sdb"]' for
83    * example to select /dev/sda and /dev/sdb.
84    *)
85   devices_to_send = None;
86
87   (* The root filesystem containing /etc/fstab.  Set to
88    * 'Some (Part ("sda", "3"))' or 'Some (LV ("VolGroup00", "LogVol00"))'
89    * for example, else ask user.
90    *)
91   root_filesystem = None;
92
93   (* Network configuration: Set to 'Some Auto' (try to set it up
94    * automatically, or 'Some Shell' (give the user a shell).
95    *)
96   network = None;
97
98   (* Hypervisor: Set to 'Some Xen', 'Some QEMU' or 'Some KVM'. *)
99   hypervisor = None;
100
101   (* Architecture: Set to 'Some X86_64' (or another architecture).
102    * If set to 'Some UnknownArch' then we try to autodetect the
103    * right architecture.
104    *)
105   architecture = None;
106
107   (* Memory: Set to 'Some nn' with nn in megabytes.  If set to 'Some 0'
108    * then we use same amount of RAM as installed in the physical machine.
109    *)
110   memory = None;
111
112   (* Virtual CPUs: Set to 'Some nn' where nn is the number of virtual CPUs.
113    * If set to 'Some 0' then we use the same as physical CPUs in the
114    * physical machine.
115    *)
116   vcpus = None;
117
118   (* MAC address: Set to 'Some "aa:bb:cc:dd:ee:ff"' where the string is
119    * the MAC address of the emulated network card.  Set to 'Some ""' to
120    * choose a random MAC address.
121    *)
122   mac_address = None;
123 }
124 (* END OF CUSTOM virt-p2v SCRIPT SECTION.                               *)
125 (*----------------------------------------------------------------------*)
126
127 (* General helper functions. *)
128
129 let sort_uniq ?(cmp = compare) xs =     (* sort and uniq a list *)
130   let xs = List.sort ~cmp xs in
131   let rec loop = function
132     | [] -> [] | [x] -> [x]
133     | x1 :: x2 :: xs when x1 = x2 -> loop (x1 :: xs)
134     | x :: xs -> x :: loop xs
135   in
136   loop xs
137
138 let input_all_lines chan =
139   let lines = ref [] in
140   try
141     while true do lines := input_line chan :: !lines done; []
142   with
143     End_of_file -> List.rev !lines
144
145 let dev_of_partition = function
146   | Part (dev, partnum) -> sprintf "/dev/%s%s" dev partnum
147   | LV (vg, lv) -> sprintf "/dev/%s/%s" vg lv
148
149 let string_of_architecture = function
150   | I386 -> "i386"
151   | X86_64 -> "x86_64"
152   | IA64 -> "ia64"
153   | PPC -> "ppc"
154   | PPC64 -> "ppc64"
155   | SPARC -> "sparc"
156   | SPARC64 -> "sparc64"
157   | OtherArch arch -> arch
158   | UnknownArch -> ""
159
160 type dialog_status = Yes of string list | No | Help | Back | Error
161
162 type ask_result = Next of state | Prev | Ask_again
163
164 type nature = LinuxSwap
165             | LinuxRoot of architecture * linux_distro
166             | WindowsRoot               (* Windows C: *)
167             | LinuxBoot                 (* Linux /boot *)
168             | NotRoot                   (* mountable, but not / or /boot *)
169             | UnknownNature
170 and linux_distro = RHEL of int * int
171                  | Fedora of int
172                  | Debian of int * int
173                  | OtherLinux
174
175 let rec string_of_nature = function
176   | LinuxSwap -> "Linux swap"
177   | LinuxRoot (architecture, distro) ->
178       string_of_linux_distro distro ^ " " ^ string_of_architecture architecture
179   | WindowsRoot -> "Windows root"
180   | LinuxBoot -> "Linux /boot"
181   | NotRoot -> "Mountable non-root"
182   | UnknownNature -> "Unknown"
183 and string_of_linux_distro = function
184   | RHEL (a,b) -> sprintf "RHEL %d.%d" a b
185   | Fedora v -> sprintf "Fedora %d" v
186   | Debian (a,b) -> sprintf "Debian %d.%d" a b
187   | OtherLinux -> "Linux"
188
189 (* Dialog functions.
190  *
191  * Each function takes some common parameters (eg. ~title) and some
192  * dialog-specific parameters.
193  *
194  * Returns the exit status (Yes lines | No | Help | Back | Error).
195  *)
196 let msgbox, yesno, inputbox, radiolist, checklist =
197   (* Internal function to actually run the "dialog" shell command. *)
198   let run_dialog cparams params =
199     let params = cparams @ params in
200     eprintf "dialog %s\n%!"
201       (String.concat " " (List.map (sprintf "%S") params));
202
203     (* 'dialog' writes its output/result to stderr, so we need to take
204      * special steps to capture that - in other words, manual pipe/fork.
205      *)
206     let rfd, wfd = pipe () in
207     match fork () with
208     | 0 ->                              (* child, runs dialog *)
209         close rfd;
210         dup2 wfd stderr;                (* capture stderr to pipe *)
211         execvp "dialog" (Array.of_list ("dialog" :: params))
212     | pid ->                            (* parent *)
213         close wfd;
214         let chan = in_channel_of_descr rfd in
215         let result = input_all_lines chan in
216         close rfd;
217         eprintf "dialog result: %S\n%!" (String.concat "\n" result);
218         match snd (wait ()) with
219         | WEXITED 0 -> Yes result       (* something selected / entered *)
220         | WEXITED 1 -> No               (* cancel / no button *)
221         | WEXITED 2 -> Help             (* help pressed *)
222         | WEXITED 3 -> Back             (* back button *)
223         | WEXITED _ -> Error            (* error or Esc *)
224         | WSIGNALED i -> failwith (sprintf "dialog: killed by signal %d" i)
225         | WSTOPPED i -> failwith (sprintf "dialog: stopped by signal %d" i)
226   in
227
228   (* Handle the common parameters.  Note Continuation Passing Style. *)
229   let with_common cont ?(cancel=false) ?(backbutton=true) title =
230     let params = ["--title"; title] in
231     let params = if not cancel then "--nocancel" :: params else params in
232     let params =
233       if backbutton then "--extra-button" :: "--extra-label" :: "Back" :: params
234       else params in
235     cont params
236   in
237
238   (* Message box and yes/no box. *)
239   let rec msgbox =
240     with_common (
241       fun cparams text height width ->
242         run_dialog cparams
243           [ "--msgbox"; text; string_of_int height; string_of_int width ]
244     )
245   and yesno =
246     with_common (
247       fun cparams text height width ->
248         run_dialog cparams
249           [ "--yesno"; text; string_of_int height; string_of_int width ]
250     )
251
252   (* Simple input box. *)
253   and inputbox =
254     with_common (
255       fun cparams text height width default ->
256         run_dialog cparams
257           [ "--inputbox"; text; string_of_int height; string_of_int width;
258             default ]
259     )
260
261   (* Radio list and check list. *)
262   and radiolist =
263     with_common (
264       fun cparams text height width listheight items ->
265         let items = List.map (
266           function
267           | tag, item, true -> [ tag; item; "on" ]
268           | tag, item, false -> [ tag; item; "off" ]
269         ) items in
270         let items = List.concat items in
271         let items = "--single-quoted" ::
272           "--radiolist" :: text ::
273           string_of_int height :: string_of_int width ::
274           string_of_int listheight :: items in
275         run_dialog cparams items
276     )
277   and checklist =
278     with_common (
279       fun cparams text height width listheight items ->
280         let items = List.map (
281           function
282           | tag, item, true -> [ tag; item; "on" ]
283           | tag, item, false -> [ tag; item; "off" ]
284         ) items in
285         let items = List.concat items in
286         let items = "--separate-output" ::
287           "--checklist" :: text ::
288           string_of_int height :: string_of_int width ::
289           string_of_int listheight :: items in
290         run_dialog cparams items
291     )
292   in
293   msgbox, yesno, inputbox, radiolist, checklist
294
295 (* Print failure dialog and exit. *)
296 let fail_dialog text =
297   let text = "\n" ^ text ^ "\n\nIf you want to report this error, there is a shell on [ALT] [F2], log in as root with no password.\n\nPlease provide the contents of /tmp/virt-p2v.log and output of the 'dmesg' command." in
298   ignore (msgbox "Error" text 17 50);
299   exit 1
300
301 (* Shell-safe quoting function.  In fact there's one in stdlib so use it. *)
302 let quote = Filename.quote
303
304 (* Run a shell command and check it returns 0. *)
305 let sh cmd =
306   eprintf "sh: %s\n%!" cmd;
307   if Sys.command cmd <> 0 then fail_dialog (sprintf "Command failed:\n\n%s" cmd)
308
309 let shfailok cmd =
310   eprintf "shfailok: %s\n%!" cmd;
311   ignore (Sys.command cmd)
312
313 let shwithstatus cmd =
314   eprintf "shwithstatus: %s\n%!" cmd;
315   Sys.command cmd
316
317 (* Same as `cmd` in shell.  Any error message will be in the logfile. *)
318 let shget cmd =
319   eprintf "shget: %s\n%!" cmd;
320   let chan = open_process_in cmd in
321   let lines = input_all_lines chan in
322   match close_process_in chan with
323   | WEXITED 0 -> Some lines             (* command succeeded *)
324   | WEXITED _ -> None                   (* command failed *)
325   | WSIGNALED i -> failwith (sprintf "shget: command killed by signal %d" i)
326   | WSTOPPED i -> failwith (sprintf "shget: command stopped by signal %d" i)
327
328 (* Start an interactive shell.  Need to juggle file descriptors a bit
329  * because bash write PS1 to stderr (currently directed to the logfile).
330  *)
331 let shell () =
332   match fork () with
333   | 0 ->                                (* child, runs bash *)
334       close stderr;
335       dup2 stdout stderr;
336       (* Sys.command runs 'sh -c' which blows away PS1, so set it late. *)
337       ignore (
338         Sys.command "PS1='\\u@\\h:\\w\\$ ' /bin/bash --norc --noprofile -i"
339       )
340   | _ ->                                (* parent, waits *)
341       eprintf "waiting for subshell to exit\n%!";
342       ignore (wait ())
343
344 (* Some true if is dir/file, Some false if not, None if not found. *)
345 let is_dir path =
346   try Some ((stat path).st_kind = S_DIR)
347   with Unix_error (ENOENT, "stat", _) -> None
348 let is_file path =
349   try Some ((stat path).st_kind = S_REG)
350   with Unix_error (ENOENT, "stat", _) -> None
351
352 (* Useful regular expression. *)
353 let whitespace = Pcre.regexp "[ \t]+"
354
355 (* Generate a predictable safe name containing only letters, numbers
356  * and underscores.  If passed a string with no letters or numbers,
357  * generates "_1", "_2", etc.
358  *)
359 let safe_name =
360   let next_anon =
361     let i = ref 0 in
362     fun () -> incr i; "_" ^ string_of_int !i
363   in
364   fun name ->
365     let is_safe = function 'a'..'z'|'A'..'Z'|'0'..'9' -> true | _ -> false in
366     let name = String.copy name in
367     let have_safe = ref false in
368     for i = 0 to String.length name - 1 do
369       if not (is_safe name.[i]) then name.[i] <- '_' else have_safe := true
370     done;
371     if !have_safe then name else next_anon ()
372
373 type block_device = string * int64      (* "hda" & size in bytes *)
374
375 (* Parse the output of 'lvs' to get list of LV names, sizes,
376  * corresponding PVs, etc.  Returns a list of (lvname, PVs, lvsize).
377  *)
378 let get_lvs =
379   let devname = Pcre.regexp "^/dev/(.+)\\(.+\\)$" in
380
381   function () ->
382     match
383     shget "lvs --noheadings -o vg_name,lv_name,devices,lv_size"
384     with
385     | None -> []
386     | Some lines ->
387         let lines = List.map (Pcre.split ~rex:whitespace) lines in
388         List.map (
389           function
390           | [vg; lv; pvs; lvsize]
391           | [_; vg; lv; pvs; lvsize] ->
392               let pvs = String.nsplit pvs "," in
393               let pvs = List.filter_map (
394                 fun pv ->
395                   try
396                     let subs = Pcre.exec ~rex:devname pv in
397                     Some (Pcre.get_substring subs 1)
398                   with
399                     Not_found ->
400                       eprintf "lvs: unexpected device name: %s\n%!" pv;
401                       None
402               ) pvs in
403               LV (vg, lv), pvs, lvsize
404           | line ->
405               failwith ("lvs: unexpected output: " ^ String.concat "," line)
406         ) lines
407
408 (* Get the partitions on a block device.
409  * eg. "sda" -> [Part ("sda","1"); Part ("sda", "2")]
410  *)
411 let get_partitions dev =
412   let rex = Pcre.regexp ("^" ^ dev ^ "(.+)$") in
413   let devdir = "/sys/block/" ^ dev in
414   let parts = Sys.readdir devdir in
415   let parts = Array.to_list parts in
416   let parts = List.filter (
417     fun name -> Some true = is_dir (devdir ^ "/" ^ name)
418   ) parts in
419   let parts = List.filter_map (
420     fun part ->
421       try
422         let subs = Pcre.exec ~rex part in
423         Some (Part (dev, Pcre.get_substring subs 1))
424       with
425         Not_found -> None
426   ) parts in
427   parts
428
429 (* Generate snapshot device name from device name. *)
430 let snapshot_name dev =
431   "snap" ^ (safe_name dev)
432
433 (* Perform a device-mapper snapshot with ramdisk overlay. *)
434 let snapshot =
435   let next_free_ram_disk =
436     let i = ref 0 in
437     fun () -> incr i; "/dev/ram" ^ string_of_int !i
438   in
439   fun origin_dev snapshot_dev ->
440     let ramdisk = next_free_ram_disk () in
441     let sectors =
442       let cmd = "blockdev --getsz " ^ quote ("/dev/" ^ origin_dev) in
443       let lines = shget cmd in
444       match lines with
445       | Some (sectors::_) -> Int64.of_string sectors
446       | Some [] | None ->
447           fail_dialog (sprintf "Snapshot failed - unable to read the size in sectors of block device %s" origin_dev) in
448
449     (* Create the snapshot origin device.  Called, eg. snap_sda1_org *)
450     sh (sprintf "dmsetup create %s_org --table='0 %Ld snapshot-origin /dev/%s'"
451           snapshot_dev sectors origin_dev);
452     (* Create the snapshot. *)
453     sh (sprintf "dmsetup create %s --table='0 %Ld snapshot /dev/mapper/%s_org %s n 64'"
454           snapshot_dev sectors snapshot_dev ramdisk)
455
456 (* Try to perform automatic network configuration, assuming a Fedora or
457  * RHEL-like root filesystem mounted on /mnt/root.
458  *)
459 let auto_network state =
460   (* Fedora gives an error if this file doesn't exist. *)
461   sh "touch /etc/resolv.conf";
462
463 (*
464   (* We can run /mnt/root/etc/init.d/network in a chroot environment,
465    * however this won't work directly because the architecture of the
466    * binaries under /mnt/root (eg. /mnt/root/sbin/ip) might not match
467    * the architecture of the live CD kernel.  In particular, a 32 bit
468    * live CD cannot run 64 bit binaries.  So we also have to bind-mount
469    * the live CD's /bin, /sbin, /lib etc. over the equivalents in
470    * /mnt/root.
471    *)
472   let bind dir =
473     if is_dir dir = Some true then
474       sh ("mount -o bind " ^ quote dir ^ " " ^ quote ("/mnt/root" ^ dir))
475   in
476   let unbind dir =
477     if is_dir dir = Some true then sh ("umount -l " ^ quote ("/mnt/root" ^ dir))
478   in
479   let dirs = [
480     "/bin"; "/sbin"; "/lib"; "/lib64";
481     "/usr/bin"; "/usr/sbin"; "/usr/lib"; "/usr/lib64";
482     "/proc"; "/sys"
483   ] in
484   List.iter bind dirs;
485   let status = shwithstatus "chroot /mnt/root /etc/init.d/network start" in
486   List.iter unbind dirs;
487 *)
488
489   (* Simpler way to do the above.
490    * NB. Lazy unmount is required because dhclient keeps its current
491    * directory open on /etc/sysconfig/network-scripts/
492    *)
493   sh "mount -o bind /mnt/root/etc /etc";
494   let status = shwithstatus "/etc/init.d/network start" in
495   sh "umount -l /etc";
496
497   (* Try to ping the remote host to see if this worked. *)
498   shfailok ("ping -c 3 " ^ Option.map_default quote "" state.remote_host);
499
500   if state.greeting then (
501     printf "\n\nDid automatic network configuration work?\n";
502     printf "Hint: If not sure, there is a shell on console [ALT] [F2]\n";
503     printf "    (y/n) %!";
504     let line = read_line () in
505     String.length line > 0 && (line.[0] = 'y' || line.[0] = 'Y')
506   )
507   else
508     (* Non-interactive: return the status of /etc/init.d/network start. *)
509     status = 0
510
511 (* Map local device names to remote devices names.  At the moment we
512  * just change sd* to hd* (as device names appear under fullvirt).  In
513  * future, lots of complex possibilities.
514  *)
515 let remote_of_origin_dev =
516   let devsd = Pcre.regexp "^sd([[:alpha:]]+[[:digit:]]*)$" in
517   let devsd_subst = Pcre.subst "hd$1" in
518   fun dev ->
519     Pcre.replace ~rex:devsd ~itempl:devsd_subst dev
520
521 (* Rewrite /mnt/root/etc/fstab. *)
522 let rewrite_fstab state devices_to_send =
523   let filename = "/mnt/root/etc/fstab" in
524   if is_file filename = Some true then (
525     sh ("cp " ^ quote filename ^ " " ^ quote (filename ^ ".p2vsaved"));
526
527     let chan = open_in filename in
528     let lines = input_all_lines chan in
529     close_in chan;
530     let lines = List.map (Pcre.split ~rex:whitespace) lines in
531     let lines = List.map (
532       function
533       | dev :: rest when String.starts_with dev "/dev/" ->
534           let dev = String.sub dev 5 (String.length dev - 5) in
535           let dev = remote_of_origin_dev dev in
536           let dev = "/dev/" ^ dev in
537           dev :: rest
538       | line -> line
539     ) lines in
540
541     let chan = open_out filename in
542     List.iter (
543       function
544       | [dev; mountpoint; fstype; options; freq; passno] ->
545           fprintf chan "%-23s %-23s %-7s %-15s %s %s\n"
546             dev mountpoint fstype options freq passno
547       | line ->
548           output_string chan (String.concat " " line)
549     ) lines;
550     close_out chan
551   )
552
553 (* Main entry point. *)
554 let rec main ttyname =
555   (* Running from an init script.  We don't have much of a
556    * login environment, so set one up.
557    *)
558   putenv "PATH"
559     (String.concat ":"
560        ["/usr/sbin"; "/sbin"; "/usr/local/bin"; "/usr/kerberos/bin";
561         "/usr/bin"; "/bin"]);
562   putenv "HOME" "/root";
563   putenv "LOGNAME" "root";
564
565   (* We can safely write in /tmp (it's a synthetic live CD directory). *)
566   chdir "/tmp";
567
568   (* Set up logging to /tmp/virt-p2v.log. *)
569   let fd = openfile "virt-p2v.log" [ O_WRONLY; O_APPEND; O_CREAT ] 0o644 in
570   dup2 fd stderr;
571   close fd;
572
573   (* Log the start up time. *)
574   eprintf "\n\n**************************************************\n\n";
575   let tm = localtime (time ()) in
576   eprintf "virt-p2v-ng starting up at %04d-%02d-%02d %02d:%02d:%02d\n\n%!"
577     (tm.tm_year+1900) (tm.tm_mon+1) tm.tm_mday tm.tm_hour tm.tm_min tm.tm_sec;
578
579   (* Connect stdin/stdout to the tty. *)
580   (match ttyname with
581    | None -> ()
582    | Some ttyname ->
583        let fd = openfile ("/dev/" ^ ttyname) [ O_RDWR ] 0 in
584        dup2 fd stdin;
585        dup2 fd stdout;
586        close fd);
587   printf "virt-p2v.ml starting up ...\n%!";
588
589   (* Check that the environment is a sane-looking live CD.  If not, bail. *)
590   if is_dir "/mnt/root" <> Some true then
591     fail_dialog
592       "You should only run this script from the live CD or a USB key.";
593
594   printf "virt-p2v.ml detecting hard drives (this may take some time) ...\n%!";
595
596   (* Search for all non-removable block devices.  Do this early and bail
597    * if we can't find anything.  This is a list of strings, like "hda".
598    *)
599   let all_block_devices : block_device list =
600     let rex = Pcre.regexp "^[hs]d" in
601     let devices = Array.to_list (Sys.readdir "/sys/block") in
602     let devices = List.sort devices in
603     let devices = List.filter (fun d -> Pcre.pmatch ~rex d) devices in
604     eprintf "all_block_devices: block devices: %s\n%!"
605       (String.concat "; " devices);
606     (* Run blockdev --getsize64 on each, and reject any where this fails
607      * (probably removable devices).
608      *)
609     let devices = List.filter_map (
610       fun d ->
611         let cmd = "blockdev --getsize64 " ^ quote ("/dev/" ^ d) in
612         let lines = shget cmd in
613         match lines with
614         | Some (blksize::_) -> Some (d, Int64.of_string blksize)
615         | Some [] | None -> None
616     ) devices in
617     eprintf "all_block_devices: non-removable block devices: %s\n%!"
618       (String.concat "; "
619          (List.map (fun (d, b) -> sprintf "%s [%Ld]" d b) devices));
620     if devices = [] then
621       fail_dialog "No non-removable block devices (hard disks, etc.) could be found on this machine.";
622     devices in
623
624   (* Search for partitions and LVs (anything that could contain a
625    * filesystem directly).  We refer to these generically as
626    * "partitions".
627    *)
628   let all_partitions : partition list =
629     (* LVs & PVs. *)
630     let lvs, pvs =
631       let lvs = get_lvs () in
632       let pvs = List.map (fun (_, pvs, _) -> pvs) lvs in
633       let pvs = List.concat pvs in
634       let pvs = sort_uniq pvs in
635       eprintf "all_partitions: PVs: %s\n%!" (String.concat "; " pvs);
636       let lvs = List.map (fun (lvname, _, _) -> lvname) lvs in
637       eprintf "all_partitions: LVs: %s\n%!"
638         (String.concat "; " (List.map dev_of_partition lvs));
639       lvs, pvs in
640
641     (* Partitions (eg. "sda1", "sda2"). *)
642     let parts =
643       let parts = List.map fst all_block_devices in
644       let parts = List.map get_partitions parts in
645       let parts = List.concat parts in
646       eprintf "all_partitions: all partitions: %s\n%!"
647         (String.concat "; " (List.map dev_of_partition parts));
648
649       (* Remove any partitions which are PVs. *)
650       let parts = List.filter (
651         function
652         | Part (dev, partnum) -> not (List.mem (dev ^ partnum) pvs)
653         | LV _ -> assert false
654       ) parts in
655       parts in
656     eprintf "all_partitions: partitions after removing PVs: %s\n%!"
657       (String.concat "; " (List.map dev_of_partition parts));
658
659     (* Concatenate LVs & Parts *)
660     lvs @ parts in
661
662   (* Try to determine the nature of each partition.
663    * Root? Swap? Architecture? etc.
664    *)
665   let all_partitions : (partition * nature) list =
666     (* Output of 'file' command for Linux swap file. *)
667     let swap = Pcre.regexp "Linux.*swap.*file" in
668     (* Contents of /etc/redhat-release. *)
669     let rhel = Pcre.regexp "(?:Red Hat Enterprise Linux|CentOS|Scientific Linux).*release (\\d+)(?:\\.(\\d+))?" in
670     let fedora = Pcre.regexp "Fedora.*release (\\d+)" in
671     (* Contents of /etc/debian_version. *)
672     let debian = Pcre.regexp "^(\\d+)\\.(\\d+)" in
673     (* Output of 'file' on certain executables. *)
674     let i386 = Pcre.regexp ", Intel 80386," in
675     let x86_64 = Pcre.regexp ", x86-64," in
676     let itanic = Pcre.regexp ", IA-64," in
677
678     (* Examine the filesystem mounted on 'mnt' to determine the
679      * operating system, and, if Linux, the distro.
680      *)
681     let detect_os mnt =
682       if is_dir (mnt ^ "/Windows") = Some true &&
683         is_file (mnt ^ "/autoexec.bat") = Some true then
684           WindowsRoot
685       else if is_dir (mnt ^ "/etc") = Some true &&
686         is_dir (mnt ^ "/sbin") = Some true &&
687         is_dir (mnt ^ "/var") = Some true then (
688           if is_file (mnt ^ "/etc/redhat-release") = Some true then (
689             let chan = open_in (mnt ^ "/etc/redhat-release") in
690             let lines = input_all_lines chan in
691             close_in chan;
692
693             match lines with
694             | [] -> (* empty /etc/redhat-release ...? *)
695                 LinuxRoot (UnknownArch, OtherLinux)
696             | line::_ -> (* try to detect OS from /etc/redhat-release *)
697                 try
698                   let subs = Pcre.exec ~rex:rhel line in
699                   let major = int_of_string (Pcre.get_substring subs 1) in
700                   let minor =
701                     try int_of_string (Pcre.get_substring subs 2)
702                     with Not_found -> 0 in
703                   LinuxRoot (UnknownArch, RHEL (major, minor))
704                 with
705                   Not_found | Failure "int_of_string" ->
706                     try
707                       let subs = Pcre.exec ~rex:fedora line in
708                       let version = int_of_string (Pcre.get_substring subs 1) in
709                       LinuxRoot (UnknownArch, Fedora version)
710                     with
711                       Not_found | Failure "int_of_string" ->
712                         LinuxRoot (UnknownArch, OtherLinux)
713           )
714           else if is_file (mnt ^ "/etc/debian_version") = Some true then (
715             let chan = open_in (mnt ^ "/etc/debian_version") in
716             let lines = input_all_lines chan in
717             close_in chan;
718
719             match lines with
720             | [] -> (* empty /etc/debian_version ...? *)
721                 LinuxRoot (UnknownArch, OtherLinux)
722             | line::_ -> (* try to detect version from /etc/debian_version *)
723                 try
724                   let subs = Pcre.exec ~rex:debian line in
725                   let major = int_of_string (Pcre.get_substring subs 1) in
726                   let minor = int_of_string (Pcre.get_substring subs 2) in
727                   LinuxRoot (UnknownArch, Debian (major, minor))
728                 with
729                   Not_found | Failure "int_of_string" ->
730                     LinuxRoot (UnknownArch, OtherLinux)
731           )
732           else
733             LinuxRoot (UnknownArch, OtherLinux)
734         ) else if is_dir (mnt ^ "/grub") = Some true &&
735           is_file (mnt ^ "/grub/stage1") = Some true then (
736             LinuxBoot
737         ) else
738           NotRoot (* mountable, but not a root filesystem *)
739     in
740
741     (* Examine the Linux root filesystem mounted on 'mnt' to
742      * determine the architecture. We do this by looking at some
743      * well-known binaries that we expect to be there.
744      *)
745     let detect_architecture mnt =
746       let cmd = "file -bL " ^ quote (mnt ^ "/sbin/init") in
747       match shget cmd with
748       | Some (str::_) when Pcre.pmatch ~rex:i386 str -> I386
749       | Some (str::_) when Pcre.pmatch ~rex:x86_64 str -> X86_64
750       | Some (str::_) when Pcre.pmatch ~rex:itanic str -> IA64
751       | _ -> UnknownArch
752     in
753
754     List.map (
755       fun part ->
756         let dev = dev_of_partition part in (* Get /dev device. *)
757
758         let nature =
759           (* Use 'file' command to detect if it is swap. *)
760           let cmd = "file -sbL " ^ quote dev in
761           match shget cmd with
762           | Some (str::_) when Pcre.pmatch ~rex:swap str -> LinuxSwap
763           | _ ->
764               (* Blindly try to mount the device. *)
765               let cmd = "mount -o ro " ^ quote dev ^ " /mnt/root" in
766               match shwithstatus cmd with
767               | 0 ->
768                   let os = detect_os "/mnt/root" in
769                   let nature =
770                     match os with
771                     | LinuxRoot (UnknownArch, distro) ->
772                         let architecture = detect_architecture "/mnt/root" in
773                         LinuxRoot (architecture, distro)
774                     | os -> os in
775                   sh "umount /mnt/root";
776                   nature
777
778               | _ -> UnknownNature (* not mountable *)
779
780         in
781
782         eprintf "partition detection: %s is %s\n%!"
783           dev (string_of_nature nature);
784
785         (part, nature)
786     ) all_partitions
787   in
788
789   printf "virt-p2v.ml finished detecting hard drives\n%!";
790
791   (* Dialogs. *)
792   let ask_greeting state =
793     ignore (msgbox "virt-p2v" "\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.\n\nExtra information is logged in /tmp/virt-p2v.log but this file disappears when the machine reboots." 18 50);
794     Next state
795   in
796
797   let ask_hostname state =
798     match
799     inputbox "Remote host" "Remote host" 10 50
800       (Option.default "" state.remote_host)
801     with
802     | Yes [] -> Ask_again
803     | Yes (hostname::_) -> Next { state with remote_host = Some hostname }
804     | No | Help | Error -> Ask_again
805     | Back -> Prev
806   in
807
808   let ask_port state =
809     match
810     inputbox "Remote port" "Remote port" 10 50
811       (Option.default "22" state.remote_port)
812     with
813     | Yes ([]|""::_) -> Next { state with remote_port = Some "22" }
814     | Yes (port::_) -> Next { state with remote_port = Some port }
815     | No | Help | Error -> Ask_again
816     | Back -> Prev
817   in
818
819   let ask_directory state =
820     let default_dir = "/var/lib/xen/images" in
821     match
822     inputbox "Remote directory" "Remote directory" 10 50
823       (Option.default default_dir state.remote_directory)
824     with
825     | Yes ([]|""::_) -> Next { state with remote_directory = Some default_dir }
826     | Yes (dir::_) -> Next { state with remote_directory = Some dir }
827     | No | Help | Error -> Ask_again
828     | Back -> Prev
829   in
830
831   let ask_network state =
832     match
833     radiolist "Network configuration" "Network configuration" 10 50 2 [
834       "auto", "Automatic configuration", state.network = Some Auto;
835       "sh", "Configure from the shell", state.network = Some Shell;
836     ]
837     with
838     | Yes ("auto"::_) -> Next { state with network = Some Auto }
839     | Yes ("sh"::_) -> Next { state with network = Some Shell }
840     | Yes _ | No | Help | Error -> Ask_again
841     | Back -> Prev
842   in
843
844   let ask_devices state =
845     let selected_devices = Option.default [] state.devices_to_send in
846     let devices = List.map (
847       fun (dev, blksize) ->
848         (dev,
849          sprintf "/dev/%s (%.3f GB)" dev
850            ((Int64.to_float blksize) /. (1024.*.1024.*.1024.)),
851          List.mem dev selected_devices)
852     ) all_block_devices in
853     match
854     checklist "Devices" "Pick devices to send" 15 50 8 devices
855     with
856     | Yes [] | No | Help | Error -> Ask_again
857     | Yes devices -> Next { state with devices_to_send = Some devices }
858     | Back -> Prev
859   in
860
861   let ask_root state =
862     let parts = List.mapi (
863       fun i (part, nature) ->
864         let descr =
865           match nature with
866           | LinuxSwap -> " (Linux swap)"
867           | LinuxRoot (_, RHEL (a,b)) -> sprintf " (RHEL %d.%d root)" a b
868           | LinuxRoot (_, Fedora v) -> sprintf " (Fedora %d root)" v
869           | LinuxRoot (_, Debian (a,b)) -> sprintf " (Debian %d.%d root)" a b
870           | LinuxRoot (_, OtherLinux) -> sprintf " (Linux root)"
871           | WindowsRoot -> " (Windows C:)"
872           | LinuxBoot -> " (Linux /boot)"
873           | NotRoot -> " (filesystem)"
874           | UnknownNature -> "" in
875         (string_of_int i,
876          dev_of_partition part ^ descr,
877          Some part = state.root_filesystem)
878     ) all_partitions in
879     match
880     radiolist "Root device"
881       "Pick partition containing the root (/) filesystem" 18 70 9
882       parts
883     with
884     | Yes (i::_) ->
885         let (part, _) = List.nth all_partitions (int_of_string i) in
886         Next { state with root_filesystem = Some part }
887     | Yes [] | No | Help | Error -> Ask_again
888     | Back -> Prev
889   in
890
891   let ask_hypervisor state =
892     match
893     radiolist "Hypervisor"
894       "Choose hypervisor / virtualization system"
895       11 50 4 [
896         "xen", "Xen", state.hypervisor = Some Xen;
897         "qemu", "QEMU", state.hypervisor = Some QEMU;
898         "kvm", "KVM", state.hypervisor = Some KVM;
899         "other", "Other", state.hypervisor = None
900       ]
901     with
902     | Yes ("xen"::_) -> Next { state with hypervisor = Some Xen }
903     | Yes ("qemu"::_) -> Next { state with hypervisor = Some QEMU }
904     | Yes ("kvm"::_) -> Next { state with hypervisor = Some KVM }
905     | Yes _ -> Next { state with hypervisor = None }
906     | No | Help | Error -> Ask_again
907     | Back -> Prev
908   in
909
910   let ask_architecture state =
911     match
912     radiolist "Architecture" "Machine architecture" 16 50 8 [
913       "i386", "i386 and up (32 bit)", state.architecture = Some I386;
914       "x86_64", "x86-64 (64 bit)", state.architecture = Some X86_64;
915       "ia64", "Itanium IA64", state.architecture = Some IA64;
916       "ppc", "PowerPC (32 bit)", state.architecture = Some PPC;
917       "ppc64", "PowerPC (64 bit)", state.architecture = Some PPC64;
918       "sparc", "SPARC (32 bit)", state.architecture = Some SPARC;
919       "sparc64", "SPARC (64 bit)", state.architecture = Some SPARC64;
920       "auto", "Auto-detect",
921         state.architecture = None || state.architecture = Some UnknownArch;
922     ]
923     with
924     | Yes ("i386" :: _) -> Next { state with architecture = Some I386 }
925     | Yes ("x86_64" :: _) -> Next { state with architecture = Some X86_64 }
926     | Yes ("ia64" :: _) -> Next { state with architecture = Some IA64 }
927     | Yes ("ppc" :: _) -> Next { state with architecture = Some PPC }
928     | Yes ("ppc64" :: _) -> Next { state with architecture = Some PPC64 }
929     | Yes ("sparc" :: _) -> Next { state with architecture = Some SPARC }
930     | Yes ("sparc64" :: _) -> Next { state with architecture = Some SPARC64 }
931     | Yes _ -> Next { state with architecture = Some UnknownArch }
932     | No | Help | Error -> Ask_again
933     | Back -> Prev
934   in
935
936   let ask_memory state =
937     match
938     inputbox "Memory" "Memory (MB). Leave blank to use same as physical server."
939       10 50
940       (Option.map_default string_of_int "" state.memory)
941     with
942     | Yes (""::_ | []) -> Next { state with memory = Some 0 }
943     | Yes (mem::_) ->
944         let mem = try int_of_string mem with Failure "int_of_string" -> -1 in
945         if mem < 0 || (mem > 0 && mem < 64) then Ask_again
946         else Next { state with memory = Some mem }
947     | No | Help | Error -> Ask_again
948     | Back -> Prev
949   in
950
951   let ask_vcpus state =
952     match
953     inputbox "VCPUs" "Virtual CPUs. Leave blank to use same as physical server."
954       10 50
955       (Option.map_default string_of_int "" state.vcpus)
956     with
957     | Yes (""::_ | []) -> Next { state with vcpus = Some 0 }
958     | Yes (vcpus::_) ->
959         let vcpus =
960           try int_of_string vcpus with Failure "int_of_string" -> -1 in
961         if vcpus < 0 then Ask_again
962         else Next { state with vcpus = Some vcpus }
963     | No | Help | Error -> Ask_again
964     | Back -> Prev
965   in
966
967   let ask_mac_address state =
968     match
969     inputbox "MAC address"
970       "Network MAC address. Leave blank to use a random address." 10 50
971       (Option.default "" state.mac_address)
972     with
973     | Yes (""::_ | []) -> Next { state with mac_address = Some "" }
974     | Yes (mac :: _) -> Next { state with mac_address = Some mac }
975     | No | Help | Error -> Ask_again
976     | Back -> Prev
977   in
978
979   let ask_verify state =
980     match
981     yesno "Verify and proceed"
982       (sprintf "\nPlease verify the settings below and click [OK] to proceed, or the [Back] button to return to a previous step.
983
984 Host:port:    %s : %s
985 Directory:    %s
986 Network:      %s
987 Send devices: %s
988 Root (/) dev: %s
989 Hypervisor:   %s
990 Architecture: %s
991 Memory:       %s
992 VCPUs:        %s
993 MAC address:  %s"
994          (Option.default "" state.remote_host)
995          (Option.default "" state.remote_port)
996          (Option.default "" state.remote_directory)
997          (match state.network with
998           | Some Auto -> "Auto-configure" | Some Shell -> "Shell"
999           | None -> "")
1000          (String.concat "," (Option.default [] state.devices_to_send))
1001          (Option.map_default dev_of_partition "" state.root_filesystem)
1002          (match state.hypervisor with
1003           | Some Xen -> "Xen" | Some QEMU -> "QEMU" | Some KVM -> "KVM"
1004           | None -> "Other / not set")
1005          (match state.architecture with
1006           | Some UnknownArch -> "Auto-detect"
1007           | Some arch -> string_of_architecture arch | None -> "")
1008          (match state.memory with
1009           | Some 0 -> "Same as physical"
1010           | Some mem -> string_of_int mem ^ " MB" | None -> "")
1011          (match state.vcpus with
1012           | Some 0 -> "Same as physical"
1013           | Some vcpus -> string_of_int vcpus | None -> "")
1014          (match state.mac_address with
1015           | Some "" -> "Random" | Some mac -> mac | None -> "")
1016       )
1017       21 50
1018     with
1019     | Yes _ -> Next state
1020     | Back -> Prev
1021     | No | Help | Error -> Ask_again
1022   in
1023
1024   (* This is the list of dialogs, in order.  The user can go forwards or
1025    * backwards through them.  The second parameter in each pair is
1026    * false if we need to skip this dialog (info already supplied in
1027    * 'defaults' above).
1028    *)
1029   let dlgs = [|
1030     ask_greeting,                       (* Initial greeting. *)
1031       defaults.greeting;
1032     ask_hostname,                       (* Hostname. *)
1033       defaults.remote_host = None;
1034     ask_port,                           (* Port number. *)
1035       defaults.remote_port = None;
1036     ask_directory,                      (* Remote directory. *)
1037       defaults.remote_directory = None;
1038     ask_network,                        (* Network configuration. *)
1039       defaults.network = None;
1040     ask_devices,                        (* Block devices to send. *)
1041       defaults.devices_to_send = None;
1042     ask_root,                           (* Root filesystem. *)
1043       defaults.root_filesystem = None;
1044     ask_hypervisor,                     (* Hypervisor. *)
1045       defaults.hypervisor = None;
1046     ask_architecture,                   (* Architecture. *)
1047       defaults.architecture = None;
1048     ask_memory,                         (* Memory. *)
1049       defaults.memory = None;
1050     ask_vcpus,                          (* VCPUs. *)
1051       defaults.vcpus = None;
1052     ask_mac_address,                    (* MAC address. *)
1053       defaults.mac_address = None;
1054     ask_verify,                         (* Verify settings. *)
1055       defaults.greeting
1056   |] in
1057
1058   (* Loop through the dialogs until we reach the end. *)
1059   let rec loop posn state =
1060     eprintf "dialog loop: posn = %d\n%!" posn;
1061     if posn >= Array.length dlgs then state (* Finished all dialogs. *)
1062     else (
1063       let dlg, no_skip = dlgs.(posn) in
1064       let skip = not no_skip in
1065       if skip then
1066         (* Skip this dialog and move straight to the next one. *)
1067         loop (posn+1) state
1068       else (
1069         (* Run dialog. *)
1070         match dlg state with
1071         | Next new_state -> loop (posn+1) new_state (* Forwards. *)
1072         | Prev -> loop (posn-1) state       (* Backwards / back button. *)
1073         | Ask_again -> loop posn state      (* Repeat the question. *)
1074       )
1075     )
1076   in
1077   let state = loop 0 defaults in
1078
1079   eprintf "finished dialog loop\n%!";
1080
1081   (* Switch LVM config. *)
1082   sh "vgchange -a n";
1083   putenv "LVM_SYSTEM_DIR" "/etc/lvm.new"; (* see lvm(8) *)
1084   sh "rm -f /etc/lvm/cache/.cache";
1085   sh "rm -f /etc/lvm.new/cache/.cache";
1086
1087   (* Snapshot the block devices to send. *)
1088   let devices_to_send = Option.get state.devices_to_send in
1089   let devices_to_send =
1090     List.map (
1091       fun origin_dev ->
1092         let snapshot_dev = snapshot_name origin_dev in
1093         snapshot origin_dev snapshot_dev;
1094         (origin_dev, snapshot_dev)
1095     ) devices_to_send in
1096
1097   (* Run kpartx on the snapshots. *)
1098   List.iter (
1099     fun (origin, snapshot) ->
1100       shfailok ("kpartx -a " ^ quote ("/dev/mapper/" ^ snapshot))
1101   ) devices_to_send;
1102
1103   (* Rescan for LVs. *)
1104   sh "vgscan";
1105   sh "vgchange -a y";
1106
1107   (* Mount the root filesystem under /mnt/root. *)
1108   let root_filesystem = Option.get state.root_filesystem in
1109   (match root_filesystem with
1110    | Part (dev, partnum) ->
1111        let dev = dev ^ partnum in
1112        let snapshot_dev = snapshot_name dev in
1113        sh ("mount " ^ quote ("/dev/mapper/" ^ snapshot_dev) ^ " /mnt/root")
1114
1115    | LV (vg, lv) ->
1116        (* The LV will be backed by a snapshot device, so just mount
1117         * directly.
1118         *)
1119        sh ("mount " ^ quote ("/dev/" ^ vg ^ "/" ^ lv) ^ " /mnt/root")
1120   );
1121
1122   (* See if we can do network configuration. *)
1123   let network = Option.get state.network in
1124   (match network with
1125    | Shell ->
1126        printf "Network configuration.\n\n";
1127        printf "Please configure the network from this shell.\n\n";
1128        printf "When you have finished, exit the shell with ^D or exit.\n\n%!";
1129        shell ()
1130
1131    | Auto ->
1132        printf
1133          "Trying network auto-configuration from root filesystem ...\n\n%!";
1134        if not (auto_network state) then (
1135          printf "\nAuto-configuration failed.  Starting a shell.\n\n";
1136          printf "Please configure the network from this shell.\n\n";
1137          printf "When you have finished, exit the shell with ^D or exit.\n\n";
1138          shell ()
1139        )
1140   );
1141
1142   (* Work out what devices will be called at the remote end. *)
1143   let devices_to_send = List.map (
1144     fun (origin_dev, snapshot_dev) ->
1145       let remote_dev = remote_of_origin_dev origin_dev in
1146       (origin_dev, snapshot_dev, remote_dev)
1147   ) devices_to_send in
1148
1149   (* Modify files on the root filesystem. *)
1150   rewrite_fstab state devices_to_send;
1151   (* XXX Other files to rewrite? *)
1152
1153   (* XXX Autodetect architecture of root filesystem by looking for /bin/ls. *)
1154   let system_architecture = X86_64 in
1155
1156   (* XXX Autodetect system memory. *)
1157   let system_memory = 256 in
1158
1159   (* XXX Autodetect system # pCPUs. *)
1160   let system_nr_cpus = 1 in
1161
1162   (* Unmount the root filesystem and sync disks. *)
1163   sh "umount /mnt/root";
1164   sh "sync";                            (* Ugh, should be in stdlib. *)
1165
1166   (* Disable screen blanking on console. *)
1167   sh "setterm -blank 0";
1168
1169   let remote_host = Option.get state.remote_host in
1170   let remote_port = Option.get state.remote_port in
1171   let remote_directory = Option.get state.remote_directory in
1172
1173   (* Functions to connect and disconnect from the remote system. *)
1174   let do_connect remote_name _ =
1175     let cmd = sprintf "ssh -C -p %s %s \"cat > %s/%s\""
1176       (quote remote_port) (quote remote_host)
1177       (quote remote_directory) (quote remote_name) in
1178     eprintf "connect: %s\n%!" cmd;
1179     let chan = open_process_out cmd in
1180     descr_of_out_channel chan, chan
1181   in
1182   let do_disconnect (_, chan) =
1183     match close_process_out chan with
1184     | WEXITED 0 -> ()           (* OK *)
1185     | WEXITED i -> failwith (sprintf "ssh: exited with error code %d" i)
1186     | WSIGNALED i -> failwith (sprintf "ssh: killed by signal %d" i)
1187     | WSTOPPED i -> failwith (sprintf "ssh: stopped by signal %d" i)
1188   in
1189
1190   (* XXX This is using the hostname derived from network configuration
1191    * above.  We might want to ask the user to choose.
1192    *)
1193   let hostname = safe_name (gethostname ()) in
1194   let basename =
1195     let date = sprintf "%04d%02d%02d%02d%02d"
1196       (tm.tm_year+1900) (tm.tm_mon+1) tm.tm_mday tm.tm_hour tm.tm_min in
1197     "p2v-" ^ hostname ^ "-" ^ date in
1198
1199   (* Work out what the image filenames will be at the remote end. *)
1200   let devices_to_send = List.map (
1201     fun (origin_dev, snapshot_dev, remote_dev) ->
1202       let remote_name = basename ^ "-" ^ remote_dev ^ ".img" in
1203       (origin_dev, snapshot_dev, remote_dev, remote_name)
1204   ) devices_to_send in
1205
1206   (* Write a configuration file.  Not sure if this is any better than
1207    * just 'sprintf-ing' bits of XML text together, but at least we will
1208    * always get well-formed XML.
1209    *
1210    * XXX For some of the stuff here we really should do a
1211    * virConnectGetCapabilities call to the remote host first.
1212    *
1213    * XXX There is a case for using virt-install to generate this XML.
1214    * When we start to incorporate libvirt access & storage API this
1215    * needs to be rethought.
1216    *)
1217   let conf_filename = basename ^ ".conf" in
1218
1219   let architecture =
1220     match state.architecture with
1221     | Some UnknownArch | None -> system_architecture
1222     | Some arch -> arch in
1223   let memory =
1224     match state.memory with
1225     | Some 0 | None -> system_memory
1226     | Some memory -> memory in
1227   let vcpus =
1228     match state.vcpus with
1229     | Some 0 | None -> system_nr_cpus
1230     | Some n -> n in
1231   let mac_address =
1232     match state.mac_address with
1233     | Some "" | None ->
1234         let random =
1235           List.map (sprintf "%02x") (
1236             List.map (fun _ -> Random.int 256) [0;0;0]
1237           ) in
1238         String.concat ":" ("00"::"16"::"3e"::random)
1239     | Some mac -> mac in
1240
1241   let xml =
1242     (* Shortcut to make "<name>value</name>". *)
1243     let leaf name value = Xml.Element (name, [], [Xml.PCData value]) in
1244     (* ... and the _other_ sort of leaf (god I hate XML). *)
1245     let tleaf name attribs = Xml.Element (name, attribs, []) in
1246
1247     (* Standard stuff for every domain. *)
1248     let name = leaf "name" hostname in
1249     let memory = leaf "memory" (string_of_int (memory * 1024)) in
1250     let vcpu = leaf "vcpu" (string_of_int vcpus) in
1251
1252     (* Top-level stuff which differs for each HV type (isn't this supposed
1253      * to be portable ...)
1254      *)
1255     let extras =
1256       match state.hypervisor with
1257       | Some Xen ->
1258           [Xml.Element ("os", [],
1259                         [leaf "type" "hvm";
1260                          leaf "loader" "/usr/lib/xen/boot/hvmloader";
1261                          tleaf "boot" ["dev", "hd"]]);
1262            Xml.Element ("features", [],
1263                         [tleaf "pae" [];
1264                          tleaf "acpi" [];
1265                          tleaf "apic" []]);
1266            tleaf "clock" ["sync", "localtime"]]
1267       | Some KVM ->
1268           [Xml.Element ("os", [], [leaf "type" "hvm"]);
1269            tleaf "clock" ["sync", "localtime"]]
1270       | Some QEMU ->
1271           [Xml.Element ("os", [],
1272                         [Xml.Element ("type",
1273                                       ["arch",
1274                                        string_of_architecture architecture;
1275                                        "machine","pc"],
1276                                       [Xml.PCData "hvm"]);
1277                          tleaf "boot" ["dev", "hd"]])]
1278       | None ->
1279           [] in
1280
1281     (* <devices> section. *)
1282     let devices =
1283       let emulator =
1284         match state.hypervisor with
1285         | Some Xen ->
1286             [leaf "emulator" "/usr/lib64/xen/bin/qemu-dm"] (* XXX lib64? *)
1287         | Some QEMU ->
1288             [leaf "emulator" "/usr/bin/qemu"]
1289         | Some KVM ->
1290             [leaf "emulator" "/usr/bin/qemu-kvm"]
1291         | None ->
1292             [] in
1293       let interface =
1294         Xml.Element ("interface", ["type", "user"],
1295                      [tleaf "mac" ["address", mac_address]]) in
1296       (* XXX should have an option for Xen bridging:
1297         Xml.Element (
1298         "interface", ["type","bridge"],
1299         [tleaf "source" ["bridge","xenbr0"];
1300         tleaf "mac" ["address",mac_address];
1301         tleaf "script" ["path","vif-bridge"]])*)
1302       let graphics = tleaf "graphics" ["type", "vnc"] in
1303
1304       let disks = List.map (
1305         fun (_, _, remote_dev, remote_name) ->
1306           Xml.Element (
1307             "disk", ["type", "file";
1308                      "device", "disk"],
1309             [tleaf "source" ["file", remote_directory ^ "/" ^ remote_name];
1310              tleaf "target" ["dev", remote_dev]]
1311           )
1312       ) devices_to_send in
1313
1314       Xml.Element (
1315         "devices", [],
1316         emulator @ interface :: graphics :: disks
1317       ) in
1318
1319     (* Put it all together in <domain type='foo'>. *)
1320     Xml.Element (
1321       "domain",
1322       (match state.hypervisor with
1323        | Some Xen -> ["type", "xen"]
1324        | Some QEMU -> ["type", "qemu"]
1325        | Some KVM -> ["type", "kvm"]
1326        | None -> []),
1327       name :: memory :: vcpu :: extras @ [devices]
1328     ) in
1329
1330   let xml = Xml.to_string_fmt xml in
1331   let xml_len = String.length xml in
1332   eprintf "length of configuration file is %d bytes\n%!" xml_len;
1333
1334   let (sock,_) as conn = do_connect conf_filename (Int64.of_int xml_len) in
1335   (* In OCaml this actually loops calling write(2) *)
1336   ignore (write sock xml 0 xml_len);
1337   do_disconnect conn;
1338
1339   (* Send the device snapshots to the remote host. *)
1340   (* XXX This code should be made more robust against both network
1341    * errors and local I/O errors.  Also should allow the user several
1342    * attempts to connect, or let them go back to the dialog stage.
1343    *)
1344   List.iter (
1345     fun (origin_dev, snapshot_dev, remote_dev, remote_name) ->
1346       eprintf "sending %s as %s\n%!" origin_dev remote_name;
1347
1348       let size =
1349         try List.assoc origin_dev all_block_devices
1350         with Not_found -> assert false (* internal error *) in
1351
1352       printf "Sending /dev/%s (%.3f GB) to remote machine\n%!" origin_dev
1353         ((Int64.to_float size) /. (1024.*.1024.*.1024.));
1354
1355       (* Open the snapshot device. *)
1356       let fd = openfile ("/dev/mapper/" ^ snapshot_dev) [O_RDONLY] 0 in
1357
1358       (* Now connect. *)
1359       let (sock,_) as conn = do_connect remote_name size in
1360
1361       (* Copy the data. *)
1362       let bufsize = 1024 * 1024 in
1363       let buffer = String.create bufsize in
1364       let start = gettimeofday () in
1365
1366       let rec copy bytes_sent last_printed_at =
1367         let n = read fd buffer 0 bufsize in
1368         if n > 0 then (
1369           ignore (write sock buffer 0 n);
1370
1371           let bytes_sent = Int64.add bytes_sent (Int64.of_int n) in
1372           let last_printed_at =
1373             let now = gettimeofday () in
1374             (* Print progress once per second. *)
1375             if now -. last_printed_at > 1. then (
1376               let elapsed = Int64.to_float bytes_sent /. Int64.to_float size in
1377               let secs_elapsed = now -. start in
1378               printf "%.0f%%" (100. *. elapsed);
1379               (* After 60 seconds has elapsed, start printing estimates. *)
1380               if secs_elapsed >= 60. then (
1381                 let remaining = 1. -. elapsed in
1382                 let secs_remaining = (remaining /. elapsed) *. secs_elapsed in
1383                 if secs_remaining > 120. then
1384                   printf " (about %.0f minutes remaining)          "
1385                     (secs_remaining /. 60.)
1386                 else
1387                   printf " (about %.0f seconds remaining)          "
1388                     secs_remaining
1389               );
1390               printf "\r%!";
1391               now
1392             )
1393             else last_printed_at in
1394
1395           copy bytes_sent last_printed_at
1396         )
1397       in
1398       copy 0L start;
1399
1400       (* Disconnect. *)
1401       do_disconnect conn
1402   ) devices_to_send;
1403
1404   (* Clean up and reboot. *)
1405   ignore (
1406     msgbox "virt-p2v completed"
1407       (sprintf "\nThe physical to virtual migration is complete.\n\nPlease verify the disk image(s) and configuration file on the remote host, and then start up the virtual machine by doing:\n\ncd %s\nvirsh define %s\n\nWhen you press [OK] this machine will reboot."
1408          remote_directory conf_filename)
1409       17 50
1410   );
1411
1412   shfailok "eject";
1413   shfailok "reboot";
1414   exit 0
1415
1416 let usage () =
1417   eprintf "usage: virt-p2v [ttyname]\n%!";
1418   exit 2
1419
1420 (* Make sure that exceptions from 'main' get printed out on stdout
1421  * as well as stderr, since stderr is probably redirected to the
1422  * logfile, and so not visible to the user.
1423  *)
1424 let handle_exn f arg =
1425   try f arg
1426   with exn -> print_endline (Printexc.to_string exn); raise exn
1427
1428 (* If the ISO image has an attachment then it could be a new version
1429  * of virt-p2v.ml (this script).  Get the attachment and run it
1430  * instead.  Useful mainly for testing, in conjunction with the
1431  * 'make update' target in the virt-p2v Makefile.
1432  *)
1433 let magic = "ISOATTACHMENT002"
1434 let magiclen = String.length magic  (* = 16 bytes *)
1435 let trailerlen = magiclen + 8 + 8   (* magic + file start + true size *)
1436
1437 let int64_of_string str =
1438   let i = ref 0L in
1439   let add offs shift =
1440     i :=
1441       Int64.logor
1442         (Int64.shift_left (Int64.of_int (Char.code str.[offs])) shift) !i
1443   in
1444   add 0 56;  add 1 48;  add 2 40;  add 3 32;
1445   add 4 24;  add 5 16;  add 6 8;   add 7 0;
1446   !i
1447
1448 let update ttyname =
1449   let cdrom = "/dev/cdrom" in
1450   let output = "/tmp/virt-p2v.ml" in
1451
1452   try
1453     let fd = openfile cdrom [O_RDONLY] 0 in
1454     ignore (LargeFile.lseek fd (Int64.of_int ~-trailerlen) SEEK_END);
1455     let buf = String.create magiclen in
1456     if read fd buf 0 magiclen <> magiclen || buf <> magic then (
1457       close fd;
1458       raise Exit
1459     );
1460
1461     (* Read the size. *)
1462     let buf = String.create 8 in
1463     if read fd buf 0 8 <> 8 then
1464       failwith "cannot read attachment offset";
1465     let offset = int64_of_string buf in
1466     let buf = String.create 8 in
1467     if read fd buf 0 8 <> 8 then
1468       failwith "cannot read attachment size";
1469     let size = Int64.to_int (int64_of_string buf) in
1470
1471     (* Seek to beginning of the attachment. *)
1472     ignore (LargeFile.lseek fd offset SEEK_SET);
1473
1474     (* Copy out the attachment. *)
1475     let fd2 = openfile output [O_WRONLY; O_CREAT; O_TRUNC] 0o755 in
1476     let bufsize = 4 * 1024 in
1477     let buffer = String.create bufsize in
1478     let rec copy remaining =
1479       if remaining > 0 then (
1480         let n = min remaining bufsize in
1481         let n = read fd buffer 0 n in
1482         if n = 0 then failwith "corrupted or partial attachment";
1483         ignore (write fd2 buffer 0 n);
1484         copy (remaining - n)
1485       )
1486     in
1487     copy size;
1488     close fd2;
1489
1490     close fd;
1491
1492     (* Run updated virt-p2v script. *)
1493     execv output [| output; ttyname |]
1494   with
1495     Unix_error _ | Exit ->
1496       (* Some error, or no attachment, so keep running this script. *)
1497       handle_exn main (Some ttyname)
1498
1499 (* Test harness for the Makefile.  The Makefile invokes this script as
1500  * 'virt-p2v.ml --test' just to check it compiles.  When it is running
1501  * from the actual live CD, there is a single parameter which is the
1502  * tty name (so usually 'virt-p2v.ml tty1').
1503  *)
1504 let () =
1505   match Array.to_list Sys.argv with
1506   | [ _; "--test" ] -> ()               (* Makefile test - do nothing. *)
1507   | [ _; "--update"; ttyname ] ->       (* Test for update and run. *)
1508       update ttyname
1509   | [ _; ("--help"|"-help"|"-?"|"-h") ] -> usage ();
1510   | [ _; ttyname ] ->                   (* Run main with ttyname. *)
1511       handle_exn main (Some ttyname)
1512   | [ _ ] ->                            (* Interactive - no ttyname. *)
1513       handle_exn main None
1514   | _ -> usage ()
1515
1516 (* This file must end with a newline *)