New tool: virt-sparsify to make disk images sparse.
[libguestfs.git] / sparsify / sparsify.ml
diff --git a/sparsify/sparsify.ml b/sparsify/sparsify.ml
new file mode 100644 (file)
index 0000000..89a2c13
--- /dev/null
@@ -0,0 +1,298 @@
+(* 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