2 * Copyright (C) 2010 Red Hat Inc.
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
28 module C = Libvirt.Connect
29 module Cond = Condition
30 module D = Libvirt.Domain
35 type 'a callback = 'a -> unit
40 | Connect of string option * domain list callback
41 | Disk_usage of source * string * int64 callback
42 | Download_dir_find0 of source * string * string * unit callback
43 | Download_dir_tarball of source * string * download_dir_tarball_format * string * unit callback
44 | Download_file of source * string * string * unit callback
45 | Open_domain of string * inspection_data callback
46 | Open_images of (string * string option) list * inspection_data callback
47 | Read_directory of source * string * direntry list callback
55 and inspection_data = {
56 insp_all_filesystems : (string * string) list;
57 insp_oses : inspection_os list;
64 insp_filesystems : string array;
65 insp_hostname : string;
66 insp_major_version : int;
67 insp_minor_version : int;
68 insp_mountpoints : (string * string) list;
69 insp_package_format : string;
70 insp_package_management : string;
71 insp_product_name : string;
73 insp_windows_systemroot : string option;
74 insp_winreg_DEFAULT : string option;
75 insp_winreg_SAM : string option;
76 insp_winreg_SECURITY : string option;
77 insp_winreg_SOFTWARE : string option;
78 insp_winreg_SYSTEM : string option;
81 and source = OS of inspection_os | Volume of string
89 and download_dir_tarball_format = Tar | TGZ | TXZ
91 let rec string_of_command = function
92 | Exit_thread -> "Exit_thread"
93 | Connect (Some name, _) -> sprintf "Connect %s" name
94 | Connect (None, _) -> "Connect NULL"
95 | Disk_usage (src, remotedir, _) ->
96 sprintf "Disk_usage (%s, %s)" (string_of_source src) remotedir
97 | Download_dir_find0 (src, remotedir, localfile, _) ->
98 sprintf "Download_dir_find0 (%s, %s, %s)"
99 (string_of_source src) remotedir localfile
100 | Download_dir_tarball (src, remotedir, format, localfile, _) ->
101 sprintf "Download_dir_tarball (%s, %s, %s, %s)"
102 (string_of_source src) remotedir
103 (string_of_download_dir_tarball_format format) localfile
104 | Download_file (src, remotefile, localfile, _) ->
105 sprintf "Download_file (%s, %s, %s)"
106 (string_of_source src) remotefile localfile
107 | Open_domain (name, _) -> sprintf "Open_domain %s" name
108 | Open_images (images, _) ->
109 sprintf "Open_images %s" (string_of_images images)
110 | Read_directory (src, dir, _) ->
111 sprintf "Read_directory (%s, %s)" (string_of_source src) dir
113 and string_of_images images =
118 | fn, Some format -> sprintf "%s (%s)" fn format)
121 and string_of_source = function
122 | OS { insp_root = root } ->
125 sprintf "Volume %s" dev
127 and string_of_download_dir_tarball_format = function
132 let no_callback _ = ()
134 let failure_hook = ref (fun _ -> ())
135 let busy_hook = ref (fun _ -> ())
136 let idle_hook = ref (fun _ -> ())
137 let status_hook = ref (fun _ -> ())
138 let progress_hook = ref (fun _ -> ())
140 let set_failure_hook cb = failure_hook := cb
141 let set_busy_hook cb = busy_hook := cb
142 let set_idle_hook cb = idle_hook := cb
143 let set_status_hook cb = status_hook := cb
144 let set_progress_hook cb = progress_hook := cb
146 (* Execute a function, while holding a mutex. If the function
147 * fails, ensure we release the mutex before rethrowing the
152 let r = try Left (f ()) with exn -> Right exn in
156 | Right exn -> raise exn
158 (* The queue of commands, and a lock and condition to protect it. *)
160 let q_discard = ref false
161 let q_lock = M.create ()
162 let q_cond = Cond.create ()
164 (* Send a command message to the slave thread. *)
165 let send_to_slave ?fail cmd =
166 debug "sending message %s to slave thread ..." (string_of_command cmd);
169 Q.push (fail, cmd) q;
173 let discard_command_queue () =
177 (* Discard the currently running command. *)
181 let connect ?fail uri cb = send_to_slave ?fail (Connect (uri, cb))
182 let disk_usage ?fail src remotedir cb =
183 send_to_slave ?fail (Disk_usage (src, remotedir, cb))
184 let download_dir_find0 ?fail src remotedir localfile cb =
185 send_to_slave ?fail (Download_dir_find0 (src, remotedir, localfile, cb))
186 let download_dir_tarball ?fail src remotedir format localfile cb =
188 (Download_dir_tarball (src, remotedir, format, localfile, cb))
189 let download_file ?fail src remotefile localfile cb =
190 send_to_slave ?fail (Download_file (src, remotefile, localfile, cb))
191 let open_domain ?fail name cb = send_to_slave ?fail (Open_domain (name, cb))
192 let open_images ?fail images cb = send_to_slave ?fail (Open_images (images, cb))
193 let read_directory ?fail src path cb =
194 send_to_slave ?fail (Read_directory (src, path, cb))
196 (*----- Slave thread starts here -----*)
198 (* Set this to true to exit the thread. *)
201 (* Handles. These are not protected by locks because only the slave
202 * thread has access to them.
207 (* Run the callback unless someone set the q_discard flag while
208 * we were running the command.
210 let callback_if_not_discarded (cb : 'a callback) (arg : 'a) =
211 let discard = with_lock q_lock (fun () -> !q_discard) in
213 GtkThread.async cb arg
215 (* Call 'f ()' with source mounted read-only. Ensure that everything
216 * is unmounted even if an exception is thrown.
218 let with_mount_ro g src (f : unit -> 'a) : 'a =
219 Std.finally (fun () -> g#umount_all ()) (
221 (* Do the mount - could be OS or single volume. *)
223 | Volume dev -> g#mount_ro dev "/";
224 | OS { insp_mountpoints = mps } ->
225 (* Sort the mountpoint keys by length, shortest first. *)
226 let cmp (a,_) (b,_) = compare (String.length a) (String.length b) in
227 let mps = List.sort ~cmp mps in
228 (* Mount the filesystems. *)
230 fun (mp, dev) -> g#mount_ro dev mp
236 (* Update the status bar. *)
238 let f str = GtkThread.async !status_hook str in
242 debug "top of slave loop";
244 (* Get the next command. *)
248 while Q.is_empty q do Cond.wait q_cond q_lock done;
253 debug "slave processing command %s ..." (string_of_command cmd);
256 GtkThread.async !busy_hook ();
259 (* If the user provided an override ?fail parameter to the
260 * original call, call that, else call the global hook.
263 | Some cb -> GtkThread.async cb exn
264 | None -> GtkThread.async !failure_hook exn
267 (* If there are no more commands in the queue, run the idle hook. *)
268 let empty = with_lock q_lock (fun () -> Q.is_empty q) in
269 if empty then GtkThread.async !idle_hook ();
271 if !quit then Thread.exit ();
274 and execute_command = function
279 | Connect (name, cb) ->
281 match name with None -> "default hypervisor" | Some uri -> uri in
282 status "Connecting to %s ..." printable_name;
285 conn := Some (C.connect_readonly ?name ());
287 let conn = get_conn () in
288 let doms = D.get_domains conn [D.ListAll] in
289 let doms = List.map (
291 { dom_id = D.get_id d;
292 dom_name = D.get_name d;
293 dom_state = (D.get_info d).D.state }
295 let cmp { dom_name = n1 } { dom_name = n2 } = compare n1 n2 in
296 let doms = List.sort ~cmp doms in
298 status "Connected to %s" printable_name;
299 callback_if_not_discarded cb doms
301 | Disk_usage (src, remotedir, cb) ->
302 status "Calculating disk usage of %s ..." remotedir;
306 with_mount_ro g src (
311 status "Finished calculating disk usage of %s" remotedir;
312 callback_if_not_discarded cb r
314 | Download_dir_find0 (src, remotedir, localfile, cb) ->
315 status "Downloading %s filenames to %s ..." remotedir localfile;
318 with_mount_ro g src (
320 g#find0 remotedir localfile
323 status "Finished downloading %s" localfile;
324 callback_if_not_discarded cb ()
326 | Download_dir_tarball (src, remotedir, format, localfile, cb) ->
327 status "Downloading %s to %s ..." remotedir localfile;
330 let f = match format with
335 with_mount_ro g src (
337 f remotedir localfile
340 status "Finished downloading %s" localfile;
341 callback_if_not_discarded cb ()
343 | Download_file (src, remotefile, localfile, cb) ->
344 status "Downloading %s to %s ..." remotefile localfile;
347 with_mount_ro g src (
349 g#download remotefile localfile
352 status "Finished downloading %s" localfile;
353 callback_if_not_discarded cb ()
355 | Open_domain (name, cb) ->
356 status "Opening %s ..." name;
358 let conn = get_conn () in
359 let dom = D.lookup_by_name conn name in
360 let xml = D.get_xml_desc dom in
361 let images = get_disk_images_from_xml xml in
362 open_disk_images images cb
364 | Open_images (images, cb) ->
365 status "Opening disk images ...";
367 open_disk_images images cb
369 | Read_directory (src, dir, cb) ->
370 status "Reading directory %s ..." dir;
373 let names, stats, links =
374 with_mount_ro g src (
376 let names = g#ls dir in (* sorted and without . and .. *)
377 let names = Array.to_list names in
378 let stats = lstatlist_wrapper g dir names in
379 let links = readlink_wrapper g dir names stats in
383 let n = List.length names in
384 n = List.length stats && n = List.length links
386 let entries = List.combine (List.combine names stats) links in
387 let entries = List.map (
388 fun ((name, stat), link) ->
389 { dent_name = name; dent_stat = stat; dent_link = link }
392 status "Finished reading directory %s" dir;
393 callback_if_not_discarded cb entries
395 (* Expect to be connected, and return the current libvirt connection. *)
399 | None -> failwith "not connected to libvirt"
404 | None -> failwith "no domain or disk image is open"
406 (* Close all libvirt and libguestfs handles. *)
408 (match !conn with Some conn -> C.close conn | None -> ());
413 (match !g with Some g -> g#close () | None -> ());
416 and get_disk_images_from_xml xml =
417 let xml = Xml.parse_string xml in
419 (* Return the device nodes. *)
422 | Xml.Element ("domain", _, children) ->
426 | Xml.Element ("devices", _, devices) -> Some devices
431 failwith "get_xml_desc didn't return <domain/>" in
433 (* Look for <source attr_name=attr_val/> and return attr_val. *)
434 let rec source_of attr_name = function
436 | Xml.Element ("source", attrs, _) :: rest ->
437 (try Some (List.assoc attr_name attrs)
438 with Not_found -> source_of attr_name rest)
439 | _ :: rest -> source_of attr_name rest
442 (* Look for <driver type=attr_val/> and return attr_val. *)
443 let rec format_of = function
445 | Xml.Element ("driver", attrs, _) :: rest ->
446 (try Some (List.assoc "type" attrs)
447 with Not_found -> format_of rest)
448 | _ :: rest -> format_of rest
451 (* Look for <disk> nodes and return the sources (block devices) of those. *)
455 | Xml.Element ("disk", attrs, disks) ->
458 let typ = List.assoc "type" attrs in
459 if typ = "file" then source_of "file" disks
460 else if typ = "block" then source_of "dev" disks
467 let format = format_of disks in
468 Some (filename, format)
474 (* The common code for Open_domain and Open_images which opens the
475 * libguestfs handle, adds the disks, and launches the appliance.
477 and open_disk_images images cb =
478 debug "opening disk image %s" (string_of_images images);
481 let g' = new G.guestfs () in
485 g#set_trace (trace ());
487 (* Uncomment the next line to pass the verbose flag from the command
488 * line through to libguestfs. This is not generally necessary since
489 * we are not so interested in debugging libguestfs problems at this
490 * level, and the user can always set LIBGUESTFS_DEBUG=1 if they need
493 (* g#set_verbose (verbose ());*)
495 (* Attach progress bar callback. *)
496 g#set_progress_callback (
497 fun proc_nr serial position total ->
498 debug "progress callback proc_nr=%d serial=%d posn=%Ld total=%Ld"
499 proc_nr serial position total;
500 GtkThread.async !progress_hook (position, total)
506 g#add_drive_opts ~readonly:true filename
507 | filename, Some format ->
508 g#add_drive_opts ~readonly:true ~format filename
513 status "Listing filesystems ...";
515 (* Get list of filesystems. *)
516 let fses = g#list_filesystems () in
518 status "Looking for operating systems ...";
520 (* Perform inspection. This can fail, ignore errors. *)
522 try Array.to_list (g#inspect_os ())
525 debug "inspection failed (error ignored): %s" msg;
528 let oses = List.map (
530 let typ = g#inspect_get_type root in
531 let windows_systemroot =
532 if typ <> "windows" then None
534 try Some (g#inspect_get_windows_systemroot root)
535 with G.Error _ -> None
538 (* Create most of the OS object that we're going to return. We
539 * have to pass this to with_mount_ro below which is why we need
540 * to partially create it here.
544 insp_arch = g#inspect_get_arch root;
545 insp_distro = g#inspect_get_distro root;
546 insp_filesystems = g#inspect_get_filesystems root;
547 insp_hostname = g#inspect_get_hostname root;
548 insp_major_version = g#inspect_get_major_version root;
549 insp_minor_version = g#inspect_get_minor_version root;
550 insp_mountpoints = g#inspect_get_mountpoints root;
551 insp_package_format = g#inspect_get_package_format root;
552 insp_package_management = g#inspect_get_package_management root;
553 insp_product_name = g#inspect_get_product_name root;
555 insp_windows_systemroot = windows_systemroot;
556 insp_winreg_DEFAULT = None; (* incomplete, see below *)
557 insp_winreg_SAM = None;
558 insp_winreg_SECURITY = None;
559 insp_winreg_SOFTWARE = None;
560 insp_winreg_SYSTEM = None;
563 (* We need to mount the root in order to look for Registry hives. *)
564 let winreg_DEFAULT, winreg_SAM, winreg_SECURITY, winreg_SOFTWARE,
566 match windows_systemroot with
567 | None -> None, None, None, None, None
569 with_mount_ro g (OS os) (
571 let check_for_hive filename =
573 sprintf "%s/system32/config/%s" sysroot filename in
574 try Some (g#case_sensitive_path path)
575 with G.Error _ -> None
577 check_for_hive "default",
578 check_for_hive "sam",
579 check_for_hive "security",
580 check_for_hive "software",
581 check_for_hive "system"
584 (* Fill in the remaining struct fields. *)
586 insp_winreg_DEFAULT = winreg_DEFAULT;
587 insp_winreg_SAM = winreg_SAM;
588 insp_winreg_SECURITY = winreg_SECURITY;
589 insp_winreg_SOFTWARE = winreg_SOFTWARE;
590 insp_winreg_SYSTEM = winreg_SYSTEM
596 insp_all_filesystems = fses;
600 status "Finished opening disk";
601 callback_if_not_discarded cb data
603 (* guestfs_lstatlist has a "hidden" limit of the protocol message size.
604 * Call this function, but split the list of names into chunks.
606 and lstatlist_wrapper g dir = function
609 let names', names = List.take 1000 names, List.drop 1000 names in
610 let xs = g#lstatlist dir (Array.of_list names') in
611 let xs = Array.to_list xs in
612 xs @ lstatlist_wrapper g dir names
614 (* For each entry which is a symlink, read the destination of the
615 * symlink. This is non-trivial because on Windows we cannot use
616 * readlink but need to instead parse the reparse data from NTFS.
618 and readlink_wrapper g dir names stats =
619 (* Is the directory on an NTFS filesystem? *)
620 let dev = get_mounted_device g dir in
621 if g#vfs_type dev <> "ntfs" then (
622 (* Not NTFS, use the fast g#readlinklist method. *)
623 let rec readlinklist_wrapper g dir = function
626 let names', names = List.take 1000 names, List.drop 1000 names in
627 let xs = g#readlinklist dir (Array.of_list names') in
628 let xs = Array.to_list xs in
629 xs @ readlinklist_wrapper g dir names
631 readlinklist_wrapper g dir names
634 (* NTFS: look up each symlink individually. *)
637 if not (is_symlink stat.G.mode) then ""
639 let path = if dir = "/" then dir ^ name else dir ^ "/" ^ name in
641 let _, display = get_ntfs_reparse_data g path in
644 debug "get_ntfs_reparse_data: %s: failed: %s"
645 path (Printexc.to_string exn);
647 ) (List.combine names stats)
651 * https://bugzilla.redhat.com/show_bug.cgi?id=663407
652 * http://git.annexia.org/?p=libguestfs.git;a=commit;h=3a3836b933b80c4f9f2c767fda4f8b459f998db2
653 * http://www.tuxera.com/community/ntfs-3g-advanced/junction-points-and-symbolic-links/
654 * http://www.tuxera.com/community/ntfs-3g-advanced/extended-attributes/
655 * http://www.codeproject.com/KB/winsdk/junctionpoints.aspx
657 and get_ntfs_reparse_data g path =
658 let data = g#lgetxattr path "system.ntfs_reparse_data" in
660 bitmatch Bitstring.bitstring_of_string data with
661 (* IO_REPARSE_TAG_MOUNT_POINT *)
662 | { 0xa0000003_l : 32 : littleendian;
663 _ : 16 : littleendian; (* data length - ignore it *)
664 _ : 16 : littleendian; (* reserved *)
665 link_offset : 16 : littleendian;
666 link_len : 16 : littleendian;
667 display_offset : 16 : littleendian;
668 display_len : 16 : littleendian;
669 link : link_len * 8 :
670 string, offset (8 * (link_offset + 0x10));
671 display : display_len * 8 :
672 string, offset (8 * (display_offset + 0x10)) } ->
673 (* These strings should always be valid UTF16LE, but the caller
674 * is prepared to catch any exception if this fails.
676 let link = windows_string_to_utf8 link in
677 let display = windows_string_to_utf8 display in
679 | { 0xa0000003_l : 32 : littleendian } ->
681 sprintf "%s: could not parse IO_REPARSE_TAG_MOUNT_POINT data" path
684 (* IO_REPARSE_TAG_SYMLINK *)
685 | { 0xa000000c_l : 32 : littleendian;
686 _ : 16 : littleendian; (* data length - ignore it *)
687 _ : 16 : littleendian; (* reserved *)
688 link_offset : 16 : littleendian;
689 link_len : 16 : littleendian;
690 display_offset : 16 : littleendian;
691 display_len : 16 : littleendian;
692 link : link_len * 8 :
693 string, offset (8 * (link_offset + 0x14));
694 display : display_len * 8 :
695 string, offset (8 * (display_offset + 0x14)) } ->
696 let link = windows_string_to_utf8 link in
697 let display = windows_string_to_utf8 display in
699 | { 0xa000000c_l : 32 : littleendian } ->
701 sprintf "%s: could not parse IO_REPARSE_TAG_SYMLINK data" path
704 | { i : 32 : littleendian } ->
706 sprintf "%s: reparse data of type 0x%lx is not supported" path i
709 invalid_arg (sprintf "%s: reparse data is too short" path) in
713 (* Given a path which is located somewhere on a mountpoint, return the
714 * device name. This works by using g#mountpoints and then looking for
715 * the mount path with the longest match.
717 and get_mounted_device g path =
718 let mps = g#mountpoints () in
721 if String.starts_with path mp then dev, String.length mp else dev, 0
723 let cmp (_,n1) (_,n2) = compare n2 n1 in
724 let mps = List.sort ~cmp mps in
727 invalid_arg (sprintf "%s: not mounted" path)
729 invalid_arg (sprintf "%s: not found on any filesystem" path)
730 | (dev,_) :: _ -> dev
732 (* Start up one slave thread. *)
733 let slave_thread = Thread.create loop ()
735 (* Note the following function is called from the main thread. *)
737 discard_command_queue ();
738 ignore (send_to_slave Exit_thread);
739 Thread.join slave_thread