--- /dev/null
+(* virt-sparsify
+ * Copyright (C) 2011 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *)
+
+open Unix
+open Printf
+
+module G = Guestfs
+
+open Utils
+
+let () = Random.self_init ()
+
+(* Command line argument parsing. *)
+let prog = Filename.basename Sys.executable_name
+
+let indisk, outdisk, convert, format, ignores, machine_readable, quiet,
+ verbose, trace =
+ let display_version () =
+ let g = new G.guestfs () in
+ let version = g#version () in
+ printf "virt-sparsify %Ld.%Ld.%Ld%s\n"
+ version.G.major version.G.minor version.G.release version.G.extra;
+ exit 0
+ in
+
+ let add xs s = xs := s :: !xs in
+
+ let convert = ref "" in
+ let format = ref "" in
+ let ignores = ref [] in
+ let machine_readable = ref false in
+ let quiet = ref false in
+ let verbose = ref false in
+ let trace = ref false in
+
+ let argspec = Arg.align [
+ "--convert", Arg.Set_string convert, "format Format of output disk (default: same as input)";
+ "--format", Arg.Set_string format, "format Format of input disk";
+ "--ignore", Arg.String (add ignores), "fs Ignore filesystem";
+ "--machine-readable", Arg.Set machine_readable, " Make output machine readable";
+ "-q", Arg.Set quiet, " Quiet output";
+ "--quiet", Arg.Set quiet, " -\"-";
+ "-v", Arg.Set verbose, " Enable debugging messages";
+ "--verbose", Arg.Set verbose, " -\"-";
+ "-V", Arg.Unit display_version, " Display version and exit";
+ "--version", Arg.Unit display_version, " -\"-";
+ "-x", Arg.Set trace, " Enable tracing of libguestfs calls";
+ ] in
+ let disks = ref [] in
+ let anon_fun s = disks := s :: !disks in
+ let usage_msg =
+ sprintf "\
+%s: sparsify a virtual machine disk
+
+ virt-sparsify [--options] indisk outdisk
+
+A short summary of the options is given below. For detailed help please
+read the man page virt-sparsify(1).
+"
+ prog in
+ Arg.parse argspec anon_fun usage_msg;
+
+ (* Dereference the rest of the args. *)
+ let convert = match !convert with "" -> None | str -> Some str in
+ let format = match !format with "" -> None | str -> Some str in
+ let ignores = List.rev !ignores in
+ let machine_readable = !machine_readable in
+ let quiet = !quiet in
+ let verbose = !verbose in
+ let trace = !trace in
+
+ (* No arguments and machine-readable mode? Print out some facts
+ * about what this binary supports.
+ *)
+ if !disks = [] && machine_readable then (
+ printf "virt-sparsify\n";
+ let g = new G.guestfs () in
+ g#add_drive_opts "/dev/null";
+ g#launch ();
+ if feature_available g [| "ntfsprogs"; "ntfs3g" |] then
+ printf "ntfs\n";
+ if feature_available g [| "btrfs" |] then
+ printf "btrfs\n";
+ exit 0
+ );
+
+ (* Verify we got exactly 2 disks. *)
+ let indisk, outdisk =
+ match List.rev !disks with
+ | [indisk; outdisk] -> indisk, outdisk
+ | _ ->
+ error "usage is: %s [--options] indisk outdisk" prog in
+
+ (* The input disk must be an absolute path, so we can store the name
+ * in the overlay disk.
+ *)
+ let indisk =
+ if not (Filename.is_relative indisk) then
+ indisk
+ else
+ Sys.getcwd () // indisk in
+
+ (* Check indisk filename doesn't contain a comma (limitation of qemu-img). *)
+ let contains_comma =
+ try ignore (String.index indisk ','); true
+ with Not_found -> false in
+ if contains_comma then
+ error "input filename '%s' contains a comma; qemu-img command line syntax prevents us from using such an image" indisk;
+
+ indisk, outdisk, convert, format, ignores, machine_readable, quiet,
+ verbose, trace
+
+let () =
+ if not quiet then
+ printf "Create overlay file to protect source disk ...\n%!"
+
+(* Create the temporary overlay file. *)
+let overlaydisk =
+ let tmp = Filename.temp_file "sparsify" ".qcow2" in
+
+ (* Unlink on exit. *)
+ at_exit (fun () -> try unlink tmp with _ -> ());
+
+ (* Create it with the indisk as the backing file. *)
+ let cmd =
+ sprintf "qemu-img create -f qcow2 -o backing_file=%s%s %s > /dev/null"
+ (Filename.quote indisk)
+ (match format with
+ | None -> ""
+ | Some fmt -> sprintf ",backing_fmt=%s" (Filename.quote fmt))
+ (Filename.quote tmp) in
+ if verbose then
+ printf "%s\n%!" cmd;
+ if Sys.command cmd <> 0 then
+ error "external command failed: %s" cmd;
+
+ tmp
+
+let () =
+ if not quiet then
+ printf "Examine source disk ...\n%!"
+
+(* Connect to libguestfs. *)
+let g =
+ let g = new G.guestfs () in
+ if trace then g#set_trace true;
+ if verbose then g#set_verbose true;
+
+ (* Note that the temporary overlay disk is always qcow2 format. *)
+ g#add_drive_opts ~format:"qcow2" ~readonly:false overlaydisk;
+
+ if not quiet then Progress.set_up_progress_bar ~machine_readable g;
+ g#launch ();
+
+ g
+
+(* Get the size in bytes of the input disk. *)
+let insize = g#blockdev_getsize64 "/dev/sda"
+
+(* Write zeroes for non-ignored filesystems that we are able to mount. *)
+let () =
+ let filesystems = g#list_filesystems () in
+ let filesystems = List.map fst filesystems in
+ let filesystems = List.sort compare filesystems in
+ List.iter (
+ fun fs ->
+ if not (List.mem fs ignores) then (
+
+ let mounted =
+ try g#mount_options "" fs "/"; true
+ with _ -> false in
+
+ if mounted then (
+ if not quiet then
+ printf "Fill free space in %s with zero ...\n%!" fs;
+
+ (* Choose a random filename, just letters and numbers, in
+ * 8.3 format. This ought to be compatible with any
+ * filesystem and not clash with existing files.
+ *)
+ let filename = "/" ^ string_random8 () ^ ".tmp" in
+
+ (* This command is expected to fail. *)
+ (try g#dd "/dev/zero" filename with _ -> ());
+
+ (* Make sure the last part of the file is written to disk. *)
+ g#sync ();
+
+ g#rm filename
+ );
+
+ g#umount_all ()
+ )
+ ) filesystems
+
+(* Fill unused space in volume groups. *)
+let () =
+ let vgs = g#vgs () in
+ let vgs = Array.to_list vgs in
+ let vgs = List.sort compare vgs in
+ List.iter (
+ fun vg ->
+ if not (List.mem vg ignores) then (
+ let lvname = string_random8 () in
+ let lvdev = "/dev/" ^ vg ^ "/" ^ lvname in
+
+ let created =
+ try g#lvcreate lvname vg 32; true
+ with _ -> false in
+
+ if created then (
+ if not quiet then
+ printf "Fill free space in volgroup %s with zero ...\n%!" vg;
+
+ (* XXX Don't have lvcreate -l 100%FREE. Fake it. *)
+ g#lvresize_free lvdev 100;
+
+ (* This command is expected to fail. *)
+ (try g#dd "/dev/zero" lvdev with _ -> ());
+
+ g#sync ();
+ g#lvremove lvdev
+ )
+ )
+ ) vgs
+
+(* Don't need libguestfs now. *)
+let () =
+ g#close ()
+
+(* What should the output format be? If the user specified an
+ * input format, use that, else detect it from the source image.
+ *)
+let output_format =
+ match convert with
+ | Some fmt -> fmt (* user specified output conversion *)
+ | None ->
+ match format with
+ | Some fmt -> fmt (* user specified input format, use that *)
+ | None ->
+ (* Don't know, so we must autodetect. *)
+ let cmd = sprintf "file -bsL %s" (Filename.quote indisk) in
+ let chan = open_process_in cmd in
+ let line = input_line chan in
+ let stat = close_process_in chan in
+ (match stat with
+ | WEXITED 0 -> ()
+ | WEXITED _ ->
+ error "external command failed: %s" cmd
+ | WSIGNALED i ->
+ error "external command '%s' killed by signal %d" cmd i
+ | WSTOPPED i ->
+ error "external command '%s' stopped by signal %d" cmd i
+ );
+ if string_prefix line "QEMU QCOW Image (v2)" then
+ "qcow2"
+ else
+ "raw" (* XXX guess *)
+
+(* Now run qemu-img convert which copies the overlay to the
+ * destination and automatically does sparsification.
+ *)
+let () =
+ if not quiet then
+ printf "Copy to destination and make sparse ...\n%!";
+
+ let cmd =
+ sprintf "qemu-img convert -f qcow2 -O %s %s %s"
+ (Filename.quote output_format)
+ (Filename.quote overlaydisk) (Filename.quote outdisk) in
+ if verbose then
+ printf "%s\n%!" cmd;
+ if Sys.command cmd <> 0 then
+ error "external command failed: %s" cmd
+
+(* Finished. *)
+let () =
+ if not quiet then (
+ print_newline ();
+ wrap "Sparsify operation completed with no errors. Before deleting the old disk, carefully check that the target disk boots and works correctly.\n";
+ );
+
+ exit 0