1 #!/usr/bin/ocamlrun /usr/bin/ocaml
8 (* virt-p2v.ml is a script which performs a physical to
9 * virtual conversion of local disks.
11 * Copyright (C) 2007-2008 Red Hat Inc.
12 * Written by Richard W.M. Jones <rjones@redhat.com>
14 * This program is free software; you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation; either version 2 of the License, or
17 * (at your option) any later version.
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
24 * You should have received a copy of the GNU General Public License
25 * along with this program; if not, write to the Free Software
26 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
34 type state = { greeting : bool;
35 remote_host : string option; remote_port : string option;
36 remote_transport : transport option;
37 remote_directory : string option;
38 network : network option;
39 devices_to_send : string list option;
40 root_filesystem : partition option }
41 and transport = SSH | TCP
42 and network = Auto | Shell
43 and partition = Part of string * string (* eg. "hda", "1" *)
44 | LV of string * string (* eg. "VolGroup00", "LogVol00" *)
46 (*----------------------------------------------------------------------*)
47 (* TO MAKE A CUSTOM virt-p2v SCRIPT, adjust the defaults in this section.
49 * If left as they are, then this will create a generic virt-p2v script
50 * which asks the user for each question. If you set the defaults here
51 * then you will get a custom virt-p2v which is partially or even fully
52 * automated and won't ask the user any questions.
54 * Note that 'None' means 'no default' (ie. ask the user) whereas
55 * 'Some foo' means use 'foo' as the answer.
58 (* If greeting is true, wait for keypress after boot and during
59 * final verification. Set to 'false' for less interactions.
63 (* Transport: Set to 'Some SSH' or 'Some TCP' to assume SSH or TCP
66 remote_transport = None;
68 (* Remote host and port. Set to 'Some "host"' and 'Some "port"',
74 (* Remote directory (only for SSH transport). Set to 'Some "path"'
75 * to set up a directory path, else ask the user.
77 remote_directory = None;
79 (* List of devices to send. Set to 'Some ["sda"; "sdb"]' for
80 * example to select /dev/sda and /dev/sdb.
82 devices_to_send = None;
84 (* The root filesystem containing /etc/fstab. Set to
85 * 'Some (Part ("sda", "3"))' or 'Some (LV ("VolGroup00", "LogVol00"))'
86 * for example, else ask user.
88 root_filesystem = None;
90 (* Network configuration: Set to 'Some Auto' (try to set it up
91 * automatically, or 'Some Shell' (give the user a shell).
96 (* END OF CUSTOM virt-p2v SCRIPT SECTION. *)
97 (*----------------------------------------------------------------------*)
99 (* General helper functions. *)
101 let sort_uniq ?(cmp = compare) xs = (* sort and uniq a list *)
102 let xs = List.sort ~cmp xs in
103 let rec loop = function
104 | [] -> [] | [x] -> [x]
105 | x1 :: x2 :: xs when x1 = x2 -> loop (x1 :: xs)
106 | x :: xs -> x :: loop xs
110 let rec string_of_state state =
112 "greeting: %b remote: %s:%s%s%s network: %s devices: [%s] root: %s"
114 (Option.default "" state.remote_host)
115 (Option.default "" state.remote_port)
116 (match state.remote_transport with
117 | None -> "" | Some SSH -> " (ssh)" | Some TCP -> " (tcp)")
118 (match state.remote_directory with
119 | None -> "" | Some dir -> " " ^ dir)
120 (match state.network with
121 | None -> "none" | Some Auto -> "auto" | Some Shell -> "shell")
122 (String.concat "; " (Option.default [] state.devices_to_send))
123 (Option.map_default dev_of_partition "" state.root_filesystem)
125 and dev_of_partition = function
126 | Part (dev, partnum) -> sprintf "/dev/%s%s" dev partnum
127 | LV (vg, lv) -> sprintf "/dev/%s/%s" vg lv
129 type dialog_status = Yes of string list | No | Help | Back | Error
131 type ask_result = Next of state | Prev | Ask_again
133 let input_all_lines chan =
134 let lines = ref [] in
136 while true do lines := input_line chan :: !lines done; []
138 End_of_file -> List.rev !lines
140 (* Same as `cmd` in shell. Any error message will be in the logfile. *)
142 let chan = open_process_in cmd in
143 let lines = input_all_lines chan in
144 match close_process_in chan with
145 | WEXITED 0 -> Some lines (* command succeeded *)
146 | WEXITED _ -> None (* command failed *)
147 | WSIGNALED i -> failwith (sprintf "shget: command killed by signal %d" i)
148 | WSTOPPED i -> failwith (sprintf "shget: command stopped by signal %d" i)
150 let is_dir path = (stat path).st_kind = S_DIR
152 type block_device = string * int64 (* "hda" & size in bytes *)
154 (* Parse the output of 'lvs' to get list of LV names, sizes,
155 * corresponding PVs, etc. Returns a list of (lvname, PVs, lvsize).
158 let whitespace = Pcre.regexp "[ \t]+" in
159 let comma = Pcre.regexp "," in
160 let devname = Pcre.regexp "^/dev/(.+)\\(.+\\)$" in
164 shget "lvs --noheadings -o vg_name,lv_name,devices,lv_size"
168 let lines = List.map (Pcre.split ~rex:whitespace) lines in
171 | [vg; lv; pvs; lvsize]
172 | [_; vg; lv; pvs; lvsize] ->
173 let pvs = Pcre.split ~rex:comma pvs in
177 let subs = Pcre.exec ~rex:devname pv in
178 Pcre.get_substring subs 1
180 Not_found -> failwith ("lvs: unexpected device name: " ^ pv)
182 LV (vg, lv), pvs, lvsize
184 failwith ("lvs: unexpected output: " ^ String.concat "," line)
187 (* Get the partitions on a block device.
188 * eg. "sda" -> [Part ("sda","1"); Part ("sda", "2")]
190 let get_partitions dev =
191 let rex = Pcre.regexp ("^" ^ dev ^ "(.+)$") in
192 let devdir = "/sys/block/" ^ dev in
193 let parts = Sys.readdir devdir in
194 let parts = Array.to_list parts in
195 let parts = List.filter (fun name -> is_dir (devdir ^ "/" ^ name)) parts in
196 let parts = List.filter_map (
199 let subs = Pcre.exec ~rex part in
200 Some (Part (dev, Pcre.get_substring subs 1))
208 * Each function takes some common parameters (eg. ~title) and some
209 * dialog-specific parameters.
211 * Returns the exit status (Yes lines | No | Help | Back | Error).
213 let msgbox, inputbox, radiolist, checklist =
214 (* Internal function to actually run the "dialog" shell command. *)
215 let run_dialog cparams params =
216 let params = cparams @ params in
217 eprintf "dialog [%s]\n%!"
218 (String.concat "; " (List.map (sprintf "%S") params));
220 (* 'dialog' writes its output/result to stderr, so we need to take
221 * special steps to capture that - in other words, manual pipe/fork.
223 let rfd, wfd = pipe () in
225 | 0 -> (* child, runs dialog *)
227 dup2 wfd stderr; (* capture stderr to pipe *)
228 execvp "dialog" (Array.of_list ("dialog" :: params))
229 | pid -> (* parent *)
231 let chan = in_channel_of_descr rfd in
232 let result = input_all_lines chan in
234 eprintf "dialog result: %S\n%!" (String.concat "\n" result);
235 match snd (wait ()) with
236 | WEXITED 0 -> Yes result (* something selected / entered *)
237 | WEXITED 1 -> No (* cancel / no button *)
238 | WEXITED 2 -> Help (* help pressed *)
239 | WEXITED 3 -> Back (* back button *)
240 | WEXITED _ -> Error (* error or Esc *)
241 | WSIGNALED i -> failwith (sprintf "dialog: killed by signal %d" i)
242 | WSTOPPED i -> failwith (sprintf "dialog: stopped by signal %d" i)
245 (* Handle the common parameters. Note Continuation Passing Style. *)
246 let with_common cont ?(cancel=false) ?(backbutton=true) title =
247 let params = ["--title"; title] in
248 let params = if not cancel then "--nocancel" :: params else params in
250 if backbutton then "--extra-button" :: "--extra-label" :: "Back" :: params
258 fun cparams text height width ->
260 [ "--msgbox"; text; string_of_int height; string_of_int width ]
264 (* Simple input box. *)
267 fun cparams text height width default ->
269 [ "--inputbox"; text; string_of_int height; string_of_int width;
274 (* Radio list and check list. *)
277 fun cparams text height width listheight items ->
278 let items = List.map (
280 | tag, item, true -> [ tag; item; "on" ]
281 | tag, item, false -> [ tag; item; "off" ]
283 let items = List.concat items in
284 let items = "--single-quoted" ::
285 "--radiolist" :: text ::
286 string_of_int height :: string_of_int width ::
287 string_of_int listheight :: items in
288 run_dialog cparams items
294 fun cparams text height width listheight items ->
295 let items = List.map (
297 | tag, item, true -> [ tag; item; "on" ]
298 | tag, item, false -> [ tag; item; "off" ]
300 let items = List.concat items in
301 let items = "--separate-output" ::
302 "--checklist" :: text ::
303 string_of_int height :: string_of_int width ::
304 string_of_int listheight :: items in
305 run_dialog cparams items
308 msgbox, inputbox, radiolist, checklist
310 (* Print failure dialog and exit. *)
311 let fail_dialog text =
312 let text = 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
313 ignore (msgbox "Error" text 17 50);
316 (* Main entry point. *)
317 let rec main ttyname =
318 (* Running from an init script. We don't have much of a
319 * login environment, so set one up.
323 ["/usr/sbin"; "/sbin"; "/usr/local/bin"; "/usr/kerberos/bin";
324 "/usr/bin"; "/bin"]);
325 putenv "HOME" "/root";
326 putenv "LOGNAME" "root";
328 (* We can safely write in /tmp (it's a synthetic live CD directory). *)
331 (* Set up logging to /tmp/virt-p2v.log. *)
332 let fd = openfile "virt-p2v.log" [ O_WRONLY; O_APPEND; O_CREAT ] 0o644 in
336 (* Log the start up time. *)
337 eprintf "\n\n**************************************************\n\n";
338 let tm = localtime (time ()) in
339 eprintf "virt-p2v-ng starting up at %04d-%02d-%02d %02d:%02d:%02d\n\n%!"
340 (tm.tm_year+1900) (tm.tm_mon+1) tm.tm_mday tm.tm_hour tm.tm_min tm.tm_sec;
342 (* Connect stdin/stdout to the tty. *)
346 let fd = openfile ("/dev/" ^ ttyname) [ O_RDWR ] 0 in
351 (* Search for all non-removable block devices. Do this early and bail
352 * if we can't find anything. This is a list of strings, like "hda".
354 let all_block_devices : block_device list =
355 let rex = Pcre.regexp "^[hs]d" in
356 let devices = Array.to_list (Sys.readdir "/sys/block") in
357 let devices = List.sort devices in
358 let devices = List.filter (fun d -> Pcre.pmatch ~rex d) devices in
359 eprintf "all_block_devices: block devices: %s\n%!"
360 (String.concat "; " devices);
361 (* Run blockdev --getsize64 on each, and reject any where this fails
362 * (probably removable devices).
364 let devices = List.filter_map (
366 let cmd = "blockdev --getsize64 /dev/" ^ Filename.quote d in
367 let lines = shget cmd in
369 | Some (blksize::_) -> Some (d, Int64.of_string blksize)
370 | Some [] | None -> None
372 eprintf "all_block_devices: non-removable block devices: %s\n%!"
374 (List.map (fun (d, b) -> sprintf "%s [%Ld]" d b) devices));
376 fail_dialog "No non-removable block devices (hard disks, etc.) could be found on this machine.";
379 (* Search for partitions and LVs (anything that could contain a
380 * filesystem directly). We refer to these generically as
383 let all_partitions : partition list =
386 let lvs = get_lvs () in
387 let pvs = List.map (fun (_, pvs, _) -> pvs) lvs in
388 let pvs = List.concat pvs in
389 let pvs = sort_uniq pvs in
390 eprintf "all_partitions: PVs: %s\n%!" (String.concat "; " pvs);
391 let lvs = List.map (fun (lvname, _, _) -> lvname) lvs in
392 eprintf "all_partitions: LVs: %s\n%!"
393 (String.concat "; " (List.map dev_of_partition lvs));
396 (* Partitions (eg. "sda1", "sda2"). *)
398 let parts = List.map fst all_block_devices in
399 let parts = List.map get_partitions parts in
400 let parts = List.concat parts in
401 eprintf "all_partitions: all partitions: %s\n%!"
402 (String.concat "; " (List.map dev_of_partition parts));
404 (* Remove any partitions which are PVs. *)
405 let parts = List.filter (
407 | Part (dev, partnum) -> not (List.mem (dev ^ partnum) pvs)
408 | LV _ -> assert false
411 eprintf "all_partitions: partitions after removing PVs: %s\n%!"
412 (String.concat "; " (List.map dev_of_partition parts));
414 (* Concatenate LVs & Parts *)
418 let ask_greeting state =
419 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);
423 let ask_transport state =
425 radiolist "Connection type" ~backbutton:false
426 "Connection type" 10 50 2 [
427 "ssh", "SSH (secure shell - recommended)",
428 state.remote_transport = Some SSH;
430 state.remote_transport = Some TCP
433 | Yes ("ssh"::_) -> Next { state with remote_transport = Some SSH }
434 | Yes ("tcp"::_) -> Next { state with remote_transport = Some TCP }
435 | Yes _ | No | Help | Error -> Ask_again
439 let ask_hostname state =
441 inputbox "Remote host" "Remote host" 10 50
442 (Option.default "" state.remote_host)
444 | Yes [] -> Ask_again
445 | Yes (hostname::_) -> Next { state with remote_host = Some hostname }
446 | No | Help | Error -> Ask_again
452 inputbox "Remote port" "Remote port" 10 50
453 (Option.default "" state.remote_port)
456 if state.remote_transport = Some TCP then
457 Next { state with remote_port = Some "16211" }
459 Next { state with remote_port = Some "22" }
460 | Yes (port::_) -> Next { state with remote_port = Some port }
461 | No | Help | Error -> Ask_again
465 let ask_directory state =
467 inputbox "Remote directory" "Remote directory" 10 50
468 (Option.default "" state.remote_directory)
471 Next { state with remote_directory = Some "/var/lib/xen/images" }
472 | Yes (dir::_) -> Next { state with remote_directory = Some dir }
473 | No | Help | Error -> Ask_again
477 let ask_network state =
479 radiolist "Network configuration" "Network configuration" 10 50 2 [
480 "auto", "Automatic configuration", state.network = Some Auto;
481 "sh", "Configure from the shell", state.network = Some Shell;
484 | Yes ("auto"::_) -> Next { state with network = Some Auto }
485 | Yes ("sh"::_) -> Next { state with network = Some Shell }
486 | Yes _ | No | Help | Error -> Ask_again
490 let ask_devices state =
491 let selected_devices = Option.default [] state.devices_to_send in
492 let devices = List.map (
493 fun (dev, blksize) ->
495 sprintf "/dev/%s (%.3f GB)" dev
496 ((Int64.to_float blksize) /. (1024.*.1024.*.1024.)),
497 List.mem dev selected_devices)
498 ) all_block_devices in
500 checklist "Devices" "Pick devices to send" 15 50 8 devices
502 | Yes [] | No | Help | Error -> Ask_again
503 | Yes devices -> Next { state with devices_to_send = Some devices }
508 let parts = List.mapi (
510 (string_of_int i, dev_of_partition part,
511 Some part = state.root_filesystem)
514 radiolist "Root device"
515 "Pick partition containing the root (/) filesystem" 15 50 6
519 let part = List.nth all_partitions (int_of_string i) in
520 Next { state with root_filesystem = Some part }
521 | Yes [] | No | Help | Error -> Ask_again
525 (* This is the list of dialogs, in order. The user can go forwards or
526 * backwards through them. The second parameter in each pair is
527 * false if we need to skip this dialog (info already supplied in
531 ask_greeting, (* Initial greeting. *)
533 ask_transport, (* Transport (ssh, tcp) *)
534 defaults.remote_transport = None;
535 ask_hostname, (* Hostname. *)
536 defaults.remote_host = None;
537 ask_port, (* Port number. *)
538 defaults.remote_port = None;
539 ask_directory, (* Remote directory. *)
540 defaults.remote_directory = None;
541 ask_network, (* Network configuration. *)
542 defaults.network = None;
543 ask_devices, (* Block devices to send. *)
544 defaults.devices_to_send = None;
545 ask_root, (* Root filesystem. *)
546 defaults.root_filesystem = None;
547 (* ask_verify, (* Verify settings. *)
551 (* Loop through the dialogs until we reach the end. *)
552 let rec loop posn state =
553 eprintf "dialog loop: posn = %d\n%!" posn;
554 if posn >= Array.length dlgs then state (* Finished all dialogs. *)
556 let dlg, no_skip = dlgs.(posn) in
557 let skip = not no_skip in
559 (* Skip this dialog and move straight to the next one. *)
564 | Next new_state -> loop (posn+1) new_state (* Forwards. *)
565 | Prev -> loop (posn-1) state (* Backwards / back button. *)
566 | Ask_again -> loop posn state (* Repeat the question. *)
570 let state = loop 0 defaults in
572 eprintf "finished dialog loop\nstate = %s\n%!" (string_of_state state);
582 eprintf "usage: virt-p2v [ttyname]\n%!";
585 (* Test harness for the Makefile. The Makefile invokes this script as
586 * 'virt-p2v.ml --test' just to check it compiles. When it is running
587 * from the actual live CD, there is a single parameter which is the
588 * tty name (so usually 'virt-p2v.ml tty1').
591 match Array.to_list Sys.argv with
592 | [ _; "--test" ] -> () (* Makefile test - do nothing. *)
593 | [ _; ("--help"|"-help"|"-?"|"-h") ] -> usage ();
594 | [ _; ttyname ] -> main (Some ttyname) (* Run main with ttyname. *)
595 | [ _ ] -> main None (* Interactive - no ttyname. *)