[Libguestfs] [PATCH v5 NOT TO BE APPLIED 1/2] New tool: virt-v2v.

Richard W.M. Jones rjones at redhat.com
Wed Apr 30 16:42:40 UTC 2014


This is a rewrite of the original virt-v2v tool.  The original was
written by Matt Booth et al in Perl between 2009 and 2013.
---
 .gitignore                       |   6 +
 Makefile.am                      |   6 +-
 configure.ac                     |   5 +-
 fish/guestfish.pod               |   1 +
 po/POTFILES                      |   2 +
 po/POTFILES-ml                   |  11 +
 src/guestfs.pod                  |   5 +
 v2v/Makefile.am                  | 163 ++++++++++
 v2v/README                       |  24 ++
 v2v/cmdline.ml                   | 197 ++++++++++++
 v2v/convert_linux_common.ml      | 236 +++++++++++++++
 v2v/convert_linux_common.mli     |  45 +++
 v2v/convert_linux_enterprise.ml  | 637 +++++++++++++++++++++++++++++++++++++++
 v2v/convert_linux_enterprise.mli |  19 ++
 v2v/convert_linux_grub.ml        | 330 ++++++++++++++++++++
 v2v/convert_linux_grub.mli       |  43 +++
 v2v/convert_windows.ml           |  22 ++
 v2v/convert_windows.mli          |  19 ++
 v2v/link.sh.in                   |  22 ++
 v2v/source_libvirt.ml            | 118 ++++++++
 v2v/source_libvirt.mli           |  27 ++
 v2v/target_local.ml              |  86 ++++++
 v2v/target_local.mli             |  21 ++
 v2v/types.ml                     |  84 ++++++
 v2v/types.mli                    |  77 +++++
 v2v/utils-c.c                    |  43 +++
 v2v/utils.ml                     |  44 +++
 v2v/v2v.ml                       | 353 ++++++++++++++++++++++
 v2v/virt-v2v.pod                 | 301 ++++++++++++++++++
 v2v/xml-c.c                      | 240 +++++++++++++++
 v2v/xml.ml                       |  50 +++
 v2v/xml.mli                      |  57 ++++
 32 files changed, 3291 insertions(+), 3 deletions(-)
 create mode 100644 v2v/Makefile.am
 create mode 100644 v2v/README
 create mode 100644 v2v/cmdline.ml
 create mode 100644 v2v/convert_linux_common.ml
 create mode 100644 v2v/convert_linux_common.mli
 create mode 100644 v2v/convert_linux_enterprise.ml
 create mode 100644 v2v/convert_linux_enterprise.mli
 create mode 100644 v2v/convert_linux_grub.ml
 create mode 100644 v2v/convert_linux_grub.mli
 create mode 100644 v2v/convert_windows.ml
 create mode 100644 v2v/convert_windows.mli
 create mode 100644 v2v/link.sh.in
 create mode 100644 v2v/source_libvirt.ml
 create mode 100644 v2v/source_libvirt.mli
 create mode 100644 v2v/target_local.ml
 create mode 100644 v2v/target_local.mli
 create mode 100644 v2v/types.ml
 create mode 100644 v2v/types.mli
 create mode 100644 v2v/utils-c.c
 create mode 100644 v2v/utils.ml
 create mode 100644 v2v/v2v.ml
 create mode 100644 v2v/virt-v2v.pod
 create mode 100644 v2v/xml-c.c
 create mode 100644 v2v/xml.ml
 create mode 100644 v2v/xml.mli

diff --git a/.gitignore b/.gitignore
index 4d47d23..25e9358 100644
--- a/.gitignore
+++ b/.gitignore
@@ -252,6 +252,7 @@ Makefile.in
 /html/virt-tar.1.html
 /html/virt-tar-in.1.html
 /html/virt-tar-out.1.html
+/html/virt-v2v.1.html
 /html/virt-win-reg.1.html
 /inspector/actual-*.xml
 /inspector/stamp-virt-inspector.pod
@@ -523,3 +524,8 @@ Makefile.in
 /test-tool/libguestfs-test-tool-helper
 /test-tool/stamp-libguestfs-test-tool.pod
 /tools/virt-*.1
+/v2v/.depend
+/v2v/link.sh
+/v2v/stamp-virt-v2v.pod
+/v2v/virt-v2v
+/v2v/virt-v2v.1
diff --git a/Makefile.am b/Makefile.am
index b135d65..3102e0b 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -132,7 +132,8 @@ SUBDIRS += \
 	builder builder/website \
 	resize \
 	sparsify \
-	sysprep
+	sysprep \
+	v2v
 endif
 
 # Perl tools.
@@ -257,6 +258,7 @@ HTMLFILES = \
 	html/virt-tar.1.html \
 	html/virt-tar-in.1.html \
 	html/virt-tar-out.1.html \
+	html/virt-v2v.1.html \
 	html/virt-win-reg.1.html
 
 HTMLSUPPORTFILES = \
@@ -319,7 +321,7 @@ all-local:
 	grep -v -E '^python/utils.c$$' | \
 	LC_ALL=C sort > po/POTFILES
 	cd $(srcdir); \
-	find builder customize mllib resize sparsify sysprep -name '*.ml' | \
+	find builder customize mllib resize sparsify sysprep v2v -name '*.ml' | \
 	LC_ALL=C sort > po/POTFILES-ml
 
 # Manual pages in top level directory.
diff --git a/configure.ac b/configure.ac
index b80644c..a8cd195 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1599,6 +1599,8 @@ AC_CONFIG_FILES([run],
                 [chmod +x,-w run])
 AC_CONFIG_FILES([sparsify/link.sh],
                 [chmod +x,-w sparsify/link.sh])
+AC_CONFIG_FILES([v2v/link.sh],
+                [chmod +x,-w v2v/link.sh])
 
 AC_CONFIG_FILES([Makefile
                  align/Makefile
@@ -1702,7 +1704,8 @@ AC_CONFIG_FILES([Makefile
                  tests/tmpdirs/Makefile
                  tests/xfs/Makefile
                  tests/xml/Makefile
-                 tools/Makefile])
+                 tools/Makefile
+                 v2v/Makefile])
 AC_OUTPUT
 
 dnl Produce summary.
diff --git a/fish/guestfish.pod b/fish/guestfish.pod
index 25279fb..5cf6ebc 100644
--- a/fish/guestfish.pod
+++ b/fish/guestfish.pod
@@ -1624,6 +1624,7 @@ L<virt-sysprep(1)>,
 L<virt-tar(1)>,
 L<virt-tar-in(1)>,
 L<virt-tar-out(1)>,
+L<virt-v2v(1)>,
 L<virt-win-reg(1)>,
 L<libguestfs-tools.conf(5)>,
 L<display(1)>,
diff --git a/po/POTFILES b/po/POTFILES
index 0fac8fe..b481157 100644
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -318,3 +318,5 @@ src/test-utils.c
 src/tmpdirs.c
 src/utils.c
 test-tool/test-tool.c
+v2v/utils-c.c
+v2v/xml-c.c
diff --git a/po/POTFILES-ml b/po/POTFILES-ml
index 8993136..b47e7db 100644
--- a/po/POTFILES-ml
+++ b/po/POTFILES-ml
@@ -81,3 +81,14 @@ sysprep/sysprep_operation_udev_persistent_net.ml
 sysprep/sysprep_operation_user_account.ml
 sysprep/sysprep_operation_utmp.ml
 sysprep/sysprep_operation_yum_uuid.ml
+v2v/cmdline.ml
+v2v/convert_linux_common.ml
+v2v/convert_linux_enterprise.ml
+v2v/convert_linux_grub.ml
+v2v/convert_windows.ml
+v2v/source_libvirt.ml
+v2v/target_local.ml
+v2v/types.ml
+v2v/utils.ml
+v2v/v2v.ml
+v2v/xml.ml
diff --git a/src/guestfs.pod b/src/guestfs.pod
index 0f54625..f634442 100644
--- a/src/guestfs.pod
+++ b/src/guestfs.pod
@@ -4396,6 +4396,10 @@ created by another.
 
 Command line tools written in Perl (L<virt-win-reg(1)> and many others).
 
+=item C<v2v>
+
+L<virt-v2v(1)> command and documentation.
+
 =item C<csharp>
 
 =item C<erlang>
@@ -4749,6 +4753,7 @@ L<virt-sysprep(1)>,
 L<virt-tar(1)>,
 L<virt-tar-in(1)>,
 L<virt-tar-out(1)>,
+L<virt-v2v(1)>,
 L<virt-win-reg(1)>,
 L<guestfs-faq(1)>,
 L<guestfs-performance(1)>,
diff --git a/v2v/Makefile.am b/v2v/Makefile.am
new file mode 100644
index 0000000..3b50a89
--- /dev/null
+++ b/v2v/Makefile.am
@@ -0,0 +1,163 @@
+# libguestfs virt-v2v tool
+# Copyright (C) 2009-2014 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.
+
+include $(top_srcdir)/subdir-rules.mk
+
+EXTRA_DIST = \
+	$(SOURCES_MLI) $(SOURCES_ML) $(SOURCES_C) \
+	virt-v2v.pod
+
+CLEANFILES = *~ *.cmi *.cmo *.cmx *.cmxa *.o virt-v2v
+
+SOURCES_MLI = \
+	convert_linux_common.mli \
+	convert_linux_enterprise.mli \
+	convert_linux_grub.mli \
+	convert_windows.mli \
+	source_libvirt.mli \
+	target_local.mli \
+	types.mli \
+	xml.mli
+
+SOURCES_ML = \
+	types.ml \
+	utils.ml \
+	xml.ml \
+	cmdline.ml \
+	source_libvirt.ml \
+	convert_linux_common.ml \
+	convert_linux_grub.ml \
+	convert_linux_enterprise.ml \
+	convert_windows.ml \
+	target_local.ml \
+	v2v.ml
+
+SOURCES_C = \
+	$(top_builddir)/fish/progress.c \
+	$(top_builddir)/mllib/tty-c.c \
+	$(top_builddir)/mllib/progress-c.c \
+	utils-c.c \
+	xml-c.c
+
+if HAVE_OCAML
+
+bin_PROGRAMS = virt-v2v
+
+virt_v2v_SOURCES = $(SOURCES_C)
+virt_v2v_CFLAGS = \
+	-I. \
+	-I$(top_builddir) \
+	-I$(shell $(OCAMLC) -where) \
+	-I$(top_srcdir)/src \
+	-I$(top_srcdir)/fish \
+	$(LIBXML2_CFLAGS)
+
+BOBJECTS = \
+	$(top_builddir)/mllib/common_gettext.cmo \
+	$(top_builddir)/mllib/common_utils.cmo \
+	$(top_builddir)/mllib/tTY.cmo \
+	$(top_builddir)/mllib/progress.cmo \
+	$(top_builddir)/mllib/config.cmo \
+	$(SOURCES_ML:.ml=.cmo)
+XOBJECTS = $(BOBJECTS:.cmo=.cmx)
+
+# -I $(top_builddir)/src/.libs is a hack which forces corresponding -L
+# option to be passed to gcc, so we don't try linking against an
+# installed copy of libguestfs.
+OCAMLPACKAGES = \
+	-package str,unix \
+	-I $(top_builddir)/src/.libs \
+	-I ../gnulib/lib/.libs \
+	-I $(top_builddir)/ocaml \
+	-I $(top_builddir)/mllib
+if HAVE_OCAML_PKG_GETTEXT
+OCAMLPACKAGES += -package gettext-stub
+endif
+
+OCAMLFLAGS = -g -warn-error CDEFLMPSUVYZX
+
+if !HAVE_OCAMLOPT
+OBJECTS = $(BOBJECTS)
+BEST    = c
+OCAMLLINKFLAGS = mlguestfs.cma -custom
+else
+OBJECTS = $(XOBJECTS)
+BEST    = opt
+OCAMLLINKFLAGS = mlguestfs.cmxa
+endif
+
+virt_v2v_DEPENDENCIES = $(OBJECTS)
+virt_v2v_LINK = \
+	./link.sh \
+	  $(OCAMLFIND) $(BEST) $(OCAMLFLAGS) $(OCAMLPACKAGES) $(OCAMLLINKFLAGS) \
+	  $(OBJECTS) -o $@
+
+.mli.cmi:
+	$(OCAMLFIND) ocamlc $(OCAMLFLAGS) $(OCAMLPACKAGES) -c $< -o $@
+.ml.cmo:
+	$(OCAMLFIND) ocamlc $(OCAMLFLAGS) $(OCAMLPACKAGES) -c $< -o $@
+.ml.cmx:
+	$(OCAMLFIND) ocamlopt $(OCAMLFLAGS) $(OCAMLPACKAGES) -c $< -o $@
+
+# Manual pages and HTML files for the website.
+
+man_MANS = virt-v2v.1
+
+noinst_DATA = $(top_builddir)/html/virt-v2v.1.html
+
+virt-v2v.1 $(top_builddir)/html/virt-v2v.1.html: stamp-virt-v2v.pod
+
+stamp-virt-v2v.pod: virt-v2v.pod
+	$(PODWRAPPER) \
+	  --man virt-v2v.1 \
+	  --html $(top_builddir)/html/virt-v2v.1.html \
+	  --license GPLv2+ \
+	  $<
+	touch $@
+
+CLEANFILES += stamp-virt-v2v.pod
+
+# Tests.
+
+TESTS_ENVIRONMENT = $(top_builddir)/run --test
+
+if ENABLE_APPLIANCE
+TESTS =
+endif ENABLE_APPLIANCE
+
+check-valgrind:
+	$(MAKE) VG="$(top_builddir)/run @VG@" check
+
+# Dependencies.
+depend: .depend
+
+.depend: $(wildcard $(abs_srcdir)/*.mli) $(wildcard $(abs_srcdir)/*.ml)
+	rm -f $@ $@-t
+	$(OCAMLFIND) ocamldep -I ../ocaml -I $(abs_srcdir) -I $(abs_top_builddir)/mllib $^ | \
+	  $(SED) 's/ *$$//' | \
+	  $(SED) -e :a -e '/ *\\$$/N; s/ *\\\n */ /; ta' | \
+	  $(SED) -e 's,$(abs_srcdir)/,$(builddir)/,g' | \
+	  sort > $@-t
+	mv $@-t $@
+
+-include .depend
+
+endif
+
+DISTCLEANFILES = .depend
+
+.PHONY: depend docs
diff --git a/v2v/README b/v2v/README
new file mode 100644
index 0000000..c4b3b0a
--- /dev/null
+++ b/v2v/README
@@ -0,0 +1,24 @@
+Missing features compared to Perl version:
+
+ - virt-p2v
+ - user-custom in virt-v2v.conf to install custom packages (virt-customize?)
+ - Windows support
+ - Fix configure_kernel on SUSE (see Mike Latimer's email)
+ - RHEV-M metadata
+ - testing
+
+Notes on the support matrix for upstream virt-v2v:
+
+- All these in 32- and 64-bit variants.
+
+- All these as Xen HV and PV variants, and ESX guests.
+
+- RHEL 3, 4, 5, 6, 7.
+- RHEL 4.5, 4.8, 5.2, 5.4 - tested particularly because virtio was added
+  between these releases.
+
+- Windows XP, 2000, 2003, 2008.
+
+- SUSE: OpenSUSE 13.1, SLES11 SP3, SLES12.
+
+- VirtualBox: Not tested.
diff --git a/v2v/cmdline.ml b/v2v/cmdline.ml
new file mode 100644
index 0000000..966fe42
--- /dev/null
+++ b/v2v/cmdline.ml
@@ -0,0 +1,197 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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.
+ *)
+
+(* Command line argument parsing. *)
+
+open Printf
+
+open Common_gettext.Gettext
+open Common_utils
+
+open Types
+open Utils
+
+let parse_cmdline () =
+  let display_version () =
+    printf "virt-v2v %s\n" Config.package_version;
+    exit 0
+  in
+
+  let debug_gc = ref false in
+  let input_conn = ref "" in
+  let output_conn = ref "" in
+  let output_format = ref "" in
+  let output_name = ref "" in
+  let output_storage = ref "" in
+  let machine_readable = ref false in
+  let quiet = ref false in
+  let verbose = ref false in
+  let trace = ref false in
+
+  let input_mode = ref `Libvirt in
+  let set_input_mode = function
+    | "libvirt" -> input_mode := `Libvirt
+    | "libvirtxml" -> input_mode := `LibvirtXML
+    | s ->
+      error (f_"unknown -i option: %s") s
+  in
+
+  let output_mode = ref `Libvirt in
+  let set_output_mode = function
+    | "libvirt" -> output_mode := `Libvirt
+    | "local" -> output_mode := `Local
+    | "ovirt" | "rhev" -> output_mode := `RHEV
+    | s ->
+      error (f_"unknown -o option: %s") s
+  in
+
+  let output_alloc = ref `Sparse in
+  let set_output_alloc = function
+    | "sparse" -> output_alloc := `Sparse
+    | "preallocated" -> output_alloc := `Preallocated
+    | s ->
+      error (f_"unknown -oa option: %s") s
+  in
+
+  let root_choice = ref `Ask in
+  let set_root_choice = function
+    | "ask" -> root_choice := `Ask
+    | "single" -> root_choice := `Single
+    | "first" -> root_choice := `First
+    | dev when string_prefix dev "/dev/" -> root_choice := `Dev dev
+    | s ->
+      error (f_"unknown --root option: %s") s
+  in
+
+  let ditto = " -\"-" in
+  let argspec = Arg.align [
+    "--debug-gc",Arg.Set debug_gc,          " " ^ s_"Debug GC and memory allocations";
+    "-i",        Arg.String set_input_mode, "libvirtxml|libvirt " ^ s_"Set input mode (default: libvirt)";
+    "-ic",       Arg.Set_string input_conn, "uri " ^ s_"Libvirt URI";
+    "--long-options", Arg.Unit display_long_options, " " ^ s_"List long options";
+    "--machine-readable", Arg.Set machine_readable, " " ^ s_"Make output machine readable";
+    "-o",        Arg.String set_output_mode, "libvirt|local|rhev " ^ s_"Set output mode (default: libvirt)";
+    "-oa",       Arg.String set_output_alloc, "sparse|preallocated " ^ s_"Set output allocation mode";
+    "-oc",       Arg.Set_string output_conn, "uri " ^ s_"Libvirt URI";
+    "-of",       Arg.Set_string output_format, "raw|qcow2 " ^ s_"Set output format";
+    "-on",       Arg.Set_string output_name, "name " ^ s_"Rename guest when converting";
+    "-os",       Arg.Set_string output_storage, "storage " ^ s_"Set output storage location";
+    "-q",        Arg.Set quiet,             " " ^ s_"Quiet output";
+    "--quiet",   Arg.Set quiet,             ditto;
+    "--root",    Arg.String set_root_choice,"ask|... " ^ s_"How to choose root filesystem";
+    "-v",        Arg.Set verbose,           " " ^ s_"Enable debugging messages";
+    "--verbose", Arg.Set verbose,           ditto;
+    "-V",        Arg.Unit display_version,  " " ^ s_"Display version and exit";
+    "--version", Arg.Unit display_version,  ditto;
+    "-x",        Arg.Set trace,             " " ^ s_"Enable tracing of libguestfs calls";
+  ] in
+  long_options := argspec;
+  let args = ref [] in
+  let anon_fun s = args := s :: !args in
+  let usage_msg =
+    sprintf (f_"\
+%s: convert a guest to use KVM
+
+ virt-v2v -ic esx://esx.example.com/ -os imported esx_guest
+
+ virt-v2v -ic esx://esx.example.com/ \
+   -o rhev -os rhev.nfs:/export_domain --network rhevm esx_guest
+
+ virt-v2v -i libvirtxml -o local -os /tmp guest-domain.xml
+
+There is a companion front-end called \"virt-p2v\" which comes as an
+ISO or CD image that can be booted on physical machines.
+
+A short summary of the options is given below.  For detailed help please
+read the man page virt-v2v(1).
+")
+      prog in
+  Arg.parse argspec anon_fun usage_msg;
+
+  (* Dereference the arguments. *)
+  let args = List.rev !args in
+  let debug_gc = !debug_gc in
+  let input_conn = match !input_conn with "" -> None | s -> Some s in
+  let input_mode = !input_mode in
+  let machine_readable = !machine_readable in
+  let output_alloc = !output_alloc in
+  let output_conn = match !output_conn with "" -> None | s -> Some s in
+  let output_format = match !output_format with "" -> None | s -> Some s in
+  let output_mode = !output_mode in
+  let output_name = match !output_name with "" -> None | s -> Some s in
+  let output_storage = !output_storage in
+  let quiet = !quiet in
+  let root_choice = !root_choice 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 args = [] && machine_readable then (
+    printf "virt-v2v\n";
+    printf "libguestfs-rewrite\n";
+    exit 0
+  );
+
+  (* Parsing of the argument(s) depends on the input mode. *)
+  let input =
+    match input_mode with
+    | `Libvirt ->
+      (* -i libvirt: Expecting a single argument which is the name
+       * of the libvirt guest.
+       *)
+      let guest =
+        match args with
+        | [guest] -> guest
+        | _ ->
+          error (f_"expecting a libvirt guest name on the command line") in
+      InputLibvirt (input_conn, guest)
+    | `LibvirtXML ->
+      (* -i libvirtxml: Expecting a filename (XML file). *)
+      let filename =
+        match args with
+        | [filename] -> filename
+        | _ ->
+          error (f_"expecting a libvirt XML file name on the command line") in
+      InputLibvirtXML filename in
+
+  (* Parse the output mode. *)
+  let output =
+    match output_mode with
+    | `Libvirt ->
+      if output_storage <> "" then
+        error (f_"-o libvirt: do not use the -os option");
+      OutputLibvirt output_conn
+    | `Local ->
+      if output_storage = "" then
+        error (f_"-o local: output directory was not specified, use '-os /dir'");
+      let dir_exists =
+        try Sys.is_directory output_storage with Sys_error _ -> false in
+      if not dir_exists then
+        error (f_"-os %s: output directory does not exist or is not a directory")
+          output_storage;
+      OutputLocal output_storage
+    | `RHEV ->
+      if output_storage = "" then
+        error (f_"-o local: output storage was not specified, use '-os'");
+      OutputRHEV output_storage in
+
+  input, output,
+  debug_gc, output_alloc, output_format, output_name,
+  quiet, root_choice, trace, verbose
diff --git a/v2v/convert_linux_common.ml b/v2v/convert_linux_common.ml
new file mode 100644
index 0000000..4922e2f
--- /dev/null
+++ b/v2v/convert_linux_common.ml
@@ -0,0 +1,236 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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 Printf
+
+open Common_gettext.Gettext
+open Common_utils
+
+open Types
+open Utils
+
+module StringMap = Map.Make (String)
+let keys map = StringMap.fold (fun k _ ks -> k :: ks) map []
+
+(* Wrappers around aug_init & aug_load which can dump out full Augeas
+ * parsing problems when debugging is enabled.
+ *)
+let rec augeas_init verbose g =
+  g#aug_init "/" 1;
+  if verbose then augeas_debug_errors g
+
+and augeas_reload verbose g =
+  g#aug_load ();
+  if verbose then augeas_debug_errors g
+
+and augeas_debug_errors g =
+  try
+    let errors = g#aug_match "/augeas/files//error" in
+    let errors = Array.to_list errors in
+    let map =
+      List.fold_left (
+        fun map error ->
+          let detail_paths = g#aug_match (error ^ "//*") in
+          let detail_paths = Array.to_list detail_paths in
+          List.fold_left (
+            fun map path ->
+              (* path is "/augeas/files/<filename>/error/<field>".  Put
+               * <filename>, <field> and the value of this Augeas field
+               * into a map.
+               *)
+              let i = string_find path "/error/" in
+              assert (i >= 0);
+              let filename = String.sub path 13 (i-13) in
+              let field = String.sub path (i+7) (String.length path - (i+7)) in
+
+              let detail = g#aug_get path in
+
+              let fmap : string StringMap.t =
+                try StringMap.find filename map
+                with Not_found -> StringMap.empty in
+              let fmap = StringMap.add field detail fmap in
+              StringMap.add filename fmap map
+          ) map detail_paths
+      ) StringMap.empty errors in
+
+    let filenames = keys map in
+    let filenames = List.sort compare filenames in
+
+    List.iter (
+      fun filename ->
+        printf "augeas failed to parse %s:\n" filename;
+        let fmap = StringMap.find filename map in
+        (try
+           let msg = StringMap.find "message" fmap in
+           printf " error \"%s\"" msg
+         with Not_found -> ()
+        );
+        (try
+           let line = StringMap.find "line" fmap
+           and char = StringMap.find "char" fmap in
+           printf " at line %s char %s" line char
+         with Not_found -> ()
+        );
+        (try
+           let lens = StringMap.find "lens" fmap in
+           printf " in lens %s" lens
+         with Not_found -> ()
+        );
+        printf "\n"
+    ) filenames;
+
+    flush stdout
+  with
+    G.Error msg -> eprintf "%s: augeas: %s (ignored)\n" prog msg
+
+let install verbose g inspect packages =
+  assert false
+
+let remove verbose g inspect packages =
+  if packages <> [] then (
+    let root = inspect.i_root in
+    let package_format = g#inspect_get_package_format root in
+    match package_format with
+    | "rpm" ->
+      let cmd = [ "rpm"; "-e" ] @ packages in
+      let cmd = Array.of_list cmd in
+      ignore (g#command cmd);
+
+      (* Reload Augeas in case anything changed. *)
+      augeas_reload verbose g
+
+    | format ->
+      error (f_"don't know how to remove packages using %s: packages: %s")
+        format (String.concat " " packages)
+  )
+
+let file_owned verbose g inspect file =
+  let root = inspect.i_root in
+  let package_format = g#inspect_get_package_format root in
+  match package_format with
+  | "rpm" ->
+      let cmd = [| "rpm"; "-qf"; file |] in
+      (try ignore (g#command cmd); true with G.Error _ -> false)
+
+  | format ->
+    error (f_"don't know how to find package owner using %s") format
+
+type kernel_info = {
+  base_package : string;          (* base package, eg. "kernel-PAE" *)
+  version : string;               (* kernel version *)
+  modules : string list;          (* list of kernel modules *)
+  arch : string;                  (* kernel arch *)
+}
+
+(* There was some crazy SUSE stuff going on in the Perl version
+ * of virt-v2v, which I have dropped from this as I couldn't
+ * understand what on earth it was doing.  - RWMJ
+ *)
+let inspect_linux_kernel verbose (g : Guestfs.guestfs) inspect path =
+  let root = inspect.i_root in
+
+  let base_package =
+    let package_format = g#inspect_get_package_format root in
+    match package_format with
+    | "rpm" ->
+      let cmd = [| "rpm"; "-qf"; "--qf"; "%{NAME}"; path |] in
+      g#command cmd
+    | format ->
+      error (f_"don't know how to inspect kernel using %s") format in
+
+  (* Try to get kernel version by examination of the binary.
+   * See supermin.git/src/kernel.ml
+   *)
+  let version =
+    try
+      let hdrS = g#pread path 4 514L in
+      if hdrS <> "HdrS" then raise Not_found;
+      let s = g#pread path 2 518L in
+      let s = (Char.code s.[1] lsl 8) lor Char.code s.[0] in
+      if s < 0x1ff then raise Not_found;
+      let offset = g#pread path 2 526L in
+      let offset = (Char.code offset.[1] lsl 8) lor Char.code offset.[0] in
+      if offset < 0 then raise Not_found;
+      let buf = g#pread path (offset + 0x200) 132L in
+      let rec loop i =
+        if i < 132 then (
+          if buf.[i] = '\000' || buf.[i] = ' ' ||
+            buf.[i] = '\t' || buf.[i] = '\n' then
+            String.sub buf 0 i
+          else
+            loop (i+1)
+        )
+        else raise Not_found
+      in
+      let v = loop 0 in
+      (* There must be a corresponding modules directory. *)
+      let modpath = sprintf "/lib/modules/%s" v in
+      if not (g#is_dir modpath) then
+        raise Not_found;
+      Some (v, modpath)
+    with Not_found -> None in
+
+  (* Apparently Xen PV kernels don't contain a version number,
+   * so try to guess the version from the filename.
+   *)
+  let version =
+    match version with
+    | Some v -> Some v
+    | None ->
+      let rex = Str.regexp "^/boot/vmlinuz-\\(.*\\)" in
+      if Str.string_match rex path 0 then (
+        let v = Str.matched_group 1 path in
+        let modpath = sprintf "/lib/modules/%s" v in
+        if g#is_dir modpath then Some (v, modpath) else None
+      )
+      else None in
+
+  (* If we sill didn't find a version, give up here. *)
+  match version with
+  | None -> None
+  | Some (version, modpath) ->
+
+    (* List modules. *)
+    let modules = g#find modpath in
+    let modules = Array.to_list modules in
+    let rex = Str.regexp ".*\\.k?o$" in
+    let modules = List.filter (fun m -> Str.string_match rex m 0) modules in
+
+    assert (List.length modules > 0);
+
+    (* Determine the kernel architecture by looking at the architecture
+     * of an arbitrary kernel module.
+     *)
+    let arch =
+      let any_module = modpath ^ List.hd modules in
+      g#file_architecture any_module in
+
+    (* Just return the module names, without path or extension. *)
+    let rex = Str.regexp ".*/\\([^/]+\\)\\.k?o$/" in
+    let modules = filter_map (
+      fun m ->
+        if Str.string_match rex m 0 then
+          Some (Str.matched_group 1 m)
+        else
+          None
+    ) modules in
+
+    Some { base_package = base_package;
+           version = version;
+           modules = modules;
+           arch = arch }
diff --git a/v2v/convert_linux_common.mli b/v2v/convert_linux_common.mli
new file mode 100644
index 0000000..4ab621a
--- /dev/null
+++ b/v2v/convert_linux_common.mli
@@ -0,0 +1,45 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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.
+ *)
+
+(** Common Linux conversion code. *)
+
+val augeas_init : bool -> Guestfs.guestfs -> unit
+val augeas_reload : bool -> Guestfs.guestfs -> unit
+(** Wrappers around [g#aug_init] and [g#aug_load], which (if verbose)
+    provide additional debugging information about parsing problems
+    that augeas found. *)
+
+val install : bool -> Guestfs.guestfs -> Types.inspect -> string list -> unit
+(** Install package(s) from the list in the guest (or ensure they are
+    installed). *)
+
+val remove : bool -> Guestfs.guestfs -> Types.inspect -> string list -> unit
+(** Uninstall package(s). *)
+
+val file_owned : bool -> Guestfs.guestfs -> Types.inspect -> string -> bool
+(** Returns true if the file is owned by an installed package. *)
+
+type kernel_info = {
+  base_package : string;          (* base package, eg. "kernel-PAE" *)
+  version : string;               (* kernel version *)
+  modules : string list;          (* list of kernel modules *)
+  arch : string;                  (* kernel arch *)
+}
+
+val inspect_linux_kernel : bool -> Guestfs.guestfs -> Types.inspect -> string -> kernel_info option
+(** Inspect a Linux kernel (by path) and return various information. *)
diff --git a/v2v/convert_linux_enterprise.ml b/v2v/convert_linux_enterprise.ml
new file mode 100644
index 0000000..9496f1a
--- /dev/null
+++ b/v2v/convert_linux_enterprise.ml
@@ -0,0 +1,637 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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.
+ *)
+
+(* Convert various RPM-based Linux enterprise distros.  This module
+ * handles:
+ *
+ * - RHEL and derivatives like CentOS and ScientificLinux
+ * - SUSE
+ * - OpenSUSE and Fedora (not enterprisey, but similar enough to RHEL/SUSE)
+ *)
+
+open Printf
+
+open Common_gettext.Gettext
+open Common_utils
+
+open Utils
+open Types
+
+let rec convert ?(keep_serial_console = true) verbose (g : Guestfs.guestfs)
+    ({ i_root = root; i_apps = apps }
+        as inspect) source =
+  let typ = g#inspect_get_type root
+  and distro = g#inspect_get_distro root
+  and arch = g#inspect_get_arch root
+  and major_version = g#inspect_get_major_version root
+  and minor_version = g#inspect_get_minor_version root
+  and package_format = g#inspect_get_package_format root
+  and package_management = g#inspect_get_package_management root in
+
+  assert (typ = "linux");
+
+  let is_rhel_family =
+    (distro = "rhel" || distro = "centos"
+            || distro = "scientificlinux" || distro = "redhat-based")
+
+  and is_suse_family =
+    (distro = "sles" || distro = "suse-based" || distro = "opensuse") in
+
+  let rec clean_rpmdb () =
+    (* Clean RPM database. *)
+    assert (package_format = "rpm");
+    let dbfiles = g#glob_expand "/var/lib/rpm/__db.00?" in
+    let dbfiles = Array.to_list dbfiles in
+    List.iter g#rm_f dbfiles
+
+  and autorelabel () =
+    (* Only do autorelabel if load_policy binary exists.  Actually
+     * loading the policy is problematic.
+     *)
+    if g#is_file ~followsymlinks:true "/usr/sbin/load_policy" then
+      g#touch "/.autorelabel";
+
+  and get_grub () =
+    (* Detect if grub2 or grub1 is installed by trying to create
+     * an object of each sort.
+     *)
+    try Convert_linux_grub.grub2 verbose g inspect
+    with Failure grub2_error ->
+      try Convert_linux_grub.grub1 verbose g inspect
+      with Failure grub1_error ->
+        error (f_"no grub configuration found in this guest.
+Grub2 error was: %s
+Grub1/grub-legacy error was: %s")
+          grub2_error grub1_error
+
+  and unconfigure_xen () =
+    (* Remove kmod-xenpv-* (RHEL 3). *)
+    let xenmods =
+      filter_map (
+        fun { G.app2_name = name } ->
+          if name = "kmod-xenpv" || string_prefix name "kmod-xenpv-" then
+            Some name
+          else
+            None
+      ) apps in
+    Convert_linux_common.remove verbose g inspect xenmods;
+
+    (* Undo related nastiness if kmod-xenpv was installed. *)
+    if xenmods <> [] then (
+      (* kmod-xenpv modules may have been manually copied to other kernels.
+       * Hunt them down and destroy them.
+       *)
+      let dirs = g#find "/lib/modules" in
+      let dirs = Array.to_list dirs in
+      let dirs = List.filter (fun s -> string_find s "/xenpv" >= 0) dirs in
+      let dirs = List.map ((^) "/lib/modules/") dirs in
+      let dirs = List.filter g#is_dir dirs in
+
+      (* Check it's not owned by an installed application. *)
+      let dirs = List.filter (
+        fun d -> not (Convert_linux_common.file_owned verbose g inspect d)
+      ) dirs in
+
+      (* Remove any unowned xenpv directories. *)
+      List.iter g#rm_rf dirs;
+
+      (* rc.local may contain an insmod or modprobe of the xen-vbd driver,
+       * added by an installation script. 
+       *)
+      (try
+         let lines = g#read_lines "/etc/rc.local" in
+         let lines = Array.to_list lines in
+         let rex = Str.regexp ".*\\b\\(insmod|modprobe\\)\b.*\\bxen-vbd.*" in
+         let lines = List.map (
+           fun s ->
+             if Str.string_match rex s 0 then
+               "#" ^ s
+             else
+               s
+         ) lines in
+         let file = String.concat "\n" lines ^ "\n" in
+         g#write "/etc/rc.local" file
+       with
+         G.Error msg -> eprintf "%s: /etc/rc.local: %s (ignored)\n" prog msg
+      );
+    );
+
+    if is_suse_family then (
+      (* Remove xen modules from INITRD_MODULES and DOMU_INITRD_MODULES. *)
+      let variables = ["INITRD_MODULES"; "DOMU_INITRD_MODULES"] in
+      let xen_modules = ["xennet"; "xen-vnif"; "xenblk"; "xen-vbd"] in
+      let modified = ref false in
+      List.iter (
+        fun var ->
+          List.iter (
+            fun xen_mod ->
+              let expr =
+                sprintf "/file/etc/sysconfig/kernel/%s/value[. = '%s']"
+                  var xen_mod in
+              let entries = g#aug_match expr in
+              let entries = Array.to_list entries in
+              if entries <> [] then (
+                List.iter (fun e -> ignore (g#aug_rm e)) entries;
+                modified := true
+              )
+          ) xen_modules
+      ) variables;
+      if !modified then g#aug_save ()
+    );
+
+  and unconfigure_vbox () =
+    (* Uninstall VirtualBox Guest Additions. *)
+    let package_name = "virtualbox-guest-additions" in
+    let has_guest_additions =
+      List.exists (
+        fun { G.app2_name = name } -> name = package_name
+      ) apps in
+    if has_guest_additions then
+      Convert_linux_common.remove verbose g inspect [package_name];
+
+    (* Guest Additions might have been installed from a tarball.  The
+     * above code won't detect this case.  Look for the uninstall tool
+     * and try running it.
+     *
+     * Note that it's important we do this early in the conversion
+     * process, as this uninstallation script naively overwrites
+     * configuration files with versions it cached prior to
+     * installation.
+     *)
+    let vboxconfig = "/var/lib/VBoxGuestAdditions/config" in
+    if g#is_file ~followsymlinks:true vboxconfig then (
+      let lines = g#read_lines vboxconfig in
+      let lines = Array.to_list lines in
+      let rex = Str.regexp "^INSTALL_DIR=\\(.*\\)$" in
+      let lines = filter_map (
+        fun line ->
+          if Str.string_match rex line 0 then (
+            let vboxuninstall = Str.matched_group 1 line ^ "/uninstall.sh" in
+            Some vboxuninstall
+          )
+          else None
+      ) lines in
+      let lines = List.filter (g#is_file ~followsymlinks:true) lines in
+      match lines with
+      | [] -> ()
+      | vboxuninstall :: _ ->
+        try
+          ignore (g#command [| vboxuninstall |]);
+
+          (* Reload Augeas to detect changes made by vbox tools uninst. *)
+          Convert_linux_common.augeas_reload verbose g
+        with
+          G.Error msg ->
+            eprintf (f_"%s: warning: VirtualBox Guest Additions were detected, but uninstallation failed.  The error message was: %s (ignored)")
+              prog msg
+    )
+
+  and unconfigure_vmware () =
+    (* Look for any configured VMware yum repos and disable them. *)
+    let repos =
+      g#aug_match "/files/etc/yum.repos.d/*/*[baseurl =~ regexp('https?://([^/]+\\.)?vmware\\.com/.*')]" in
+    let repos = Array.to_list repos in
+    List.iter (
+      fun repo ->
+        g#aug_set (repo ^ "/enabled") "0";
+        g#aug_save ()
+    ) repos;
+
+    (* Uninstall VMware Tools. *)
+    let remove = ref [] and libraries = ref [] in
+    List.iter (
+      fun { G.app2_name = name } ->
+        if name = "open-vm-tools" then
+          remove := name :: !remove
+        else if string_prefix name "vmware-tools-libraries-" then
+          libraries := name :: !libraries
+        else if string_prefix name "vmware-tools-" then
+          remove := name :: !remove
+    ) apps;
+    let libraries = !libraries in
+
+    (* VMware tools includes 'libraries' packages which provide custom
+     * versions of core functionality. We need to install non-custom
+     * versions of everything provided by these packages before
+     * attempting to uninstall them, or we'll hit dependency
+     * issues.
+     *)
+    if libraries <> [] then (
+      (* We only support removal of libraries on systems which use yum. *)
+      if package_management = "yum" then (
+        List.iter (
+          fun library ->
+            let provides =
+              g#command_lines [| "rpm"; "-q"; "--provides"; library |] in
+            let provides = Array.to_list provides in
+
+            (* The packages provide themselves, filter this out. *)
+            let provides =
+              List.filter (fun s -> string_find s library = -1) provides in
+
+            (* Trim whitespace. *)
+            let rex = Str.regexp "^[ \\t]*\\([^ \\t]+\\)[ \\t]*$" in
+            let provides = List.map (Str.replace_first rex "\\1") provides in
+
+            (* Install the dependencies with yum.  Use yum explicitly
+             * because we don't have package names and local install is
+             * impractical.  - RWMJ: Not convinced the original Perl code
+             * would work, so I'm just installing the dependencies.
+             *)
+            let cmd = [ "yum"; "install"; "-y" ] @ provides in
+            let cmd = Array.of_list cmd in
+            (try
+               ignore (g#command cmd);
+               remove := library :: !remove
+             with G.Error msg ->
+               eprintf "%s: could not install replacement for %s.  Error was: %s.  %s was not removed.\n"
+                 prog library msg library
+            );
+        ) libraries
+      )
+    );
+
+    let remove = !remove in
+    Convert_linux_common.remove verbose g inspect remove;
+
+    (* VMware Tools may have been installed from a tarball, so the
+     * above code won't remove it.  Look for the uninstall tool and run
+     * if present.
+     *)
+    let uninstaller = "/usr/bin/vmware-uninstall-tools.pl" in
+    if g#is_file ~followsymlinks:true uninstaller then (
+      try
+        ignore (g#command [| uninstaller |]);
+
+        (* Reload Augeas to detect changes made by vbox tools uninst. *)
+        Convert_linux_common.augeas_reload verbose g
+      with
+        G.Error msg ->
+          eprintf (f_"%s: warning: VMware tools was detected, but uninstallation failed.  The error message was: %s (ignored)")
+            prog msg
+    )
+
+  and unconfigure_citrix () =
+    let pkgs =
+      List.filter (
+        fun { G.app2_name = name } -> string_prefix name "xe-guest-utilities"
+      ) apps in
+    let pkgs = List.map (fun { G.app2_name = name } -> name) pkgs in
+
+    if pkgs <> [] then (
+      Convert_linux_common.remove verbose g inspect pkgs;
+
+      (* Installing these guest utilities automatically unconfigures
+       * ttys in /etc/inittab if the system uses it. We need to put
+       * them back.
+       *)
+      let rex = Str.regexp "^\\([1-6]\\):\\([2-5]+\\):respawn:\\(.*\\)" in
+      let updated = ref false in
+      let rec loop () =
+        let comments = g#aug_match "/files/etc/inittab/#comment" in
+        let comments = Array.to_list comments in
+        match comments with
+        | [] -> ()
+        | commentp :: _ ->
+          let comment = g#aug_get commentp in
+          if Str.string_match rex comment 0 then (
+            let name = Str.matched_group 1 comment in
+            let runlevels = Str.matched_group 2 comment in
+            let process = Str.matched_group 3 comment in
+
+            if string_find process "getty" >= 0 then (
+              updated := true;
+
+              (* Create a new entry immediately after the comment. *)
+              g#aug_insert commentp name false;
+              g#aug_set ("/files/etc/inittab/" ^ name ^ "/runlevels") runlevels;
+              g#aug_set ("/files/etc/inittab/" ^ name ^ "/action") "respawn";
+              g#aug_set ("/files/etc/inittab/" ^ name ^ "/process") process;
+
+              (* Delete the comment node. *)
+              ignore (g#aug_rm commentp);
+
+              (* As the aug_rm invalidates the output of aug_match, we
+               * now have to restart the whole loop.
+               *)
+              loop ()
+            )
+          )
+      in
+      loop ();
+      if !updated then g#aug_save ();
+    )
+
+  and install_virtio () =
+    (* How you install virtio depends on the guest type.  Note that most
+     * modern guests already support virtio, so we do nothing for them.
+     * In Perl virt-v2v this was done via a configuration database
+     * (virt-v2v.db).  This function returns true if virtio is supported
+     * already or if we managed to install it.
+     *)
+    match distro, major_version, minor_version with
+    (* RHEL 6+ has always supported virtio. *)
+    | ("rhel"|"centos"|"scientificlinux"|"redhat-based"), v, _ when v >= 6 ->
+      true
+    | ("rhel"|"centos"|"scientificlinux"|"redhat-based"), 5, _ ->
+      let kernel = upgrade_package "kernel" (0_l, "2.6.18", "128.el5") in
+      let lvm2 = upgrade_package "lvm2" (0_l, "2.02.40", "6.el5") in
+      let selinux =
+        upgrade_package ~ifinstalled:true
+          "selinux-policy-targeted" (0_l, "2.4.6", "203.el5") in
+      kernel && lvm2 && selinux
+    | ("rhel"|"centos"|"scientificlinux"|"redhat-based"), 4, _ ->
+      upgrade_package "kernel" (0_l, "2.6.9", "89.EL")
+
+    (* All supported Fedora versions support virtio. *)
+    | "fedora", _, _ -> true
+
+    (* SLES 11 supports virtio in the kernel. *)
+    | ("sles"|"suse-based"), v, _ when v >= 11 -> true
+    | ("sles"|"suse-based"), 10, _ ->
+      upgrade_package "kernel" (0_l, "2.6.16.60", "0.85.1")
+
+    (* OpenSUSE. *)
+    | "opensuse", v, _ when v >= 11 -> true
+    | "opensuse", 10, _ ->
+      upgrade_package "kernel" (0_l, "2.6.25.5", "1.1")
+
+    | _ ->
+      eprintf (f_"%s: warning: don't know how to install virtio drivers for %s %d")
+        prog distro major_version;
+      false
+
+  and configure_kernel virtio grub =
+    let kernels = grub#list_kernels () in
+
+    let bootable_kernel =
+      let rec loop =
+        function
+        | [] -> None
+        | path :: paths ->
+          let kernel =
+            Convert_linux_common.inspect_linux_kernel verbose g inspect path in
+          match kernel with
+          | None -> loop paths
+          | Some kernel when is_hv_kernel kernel -> loop paths
+          | Some kernel when virtio && not (supports_virtio kernel) ->
+            loop paths
+          | Some kernel -> Some kernel
+      in
+      loop kernels in
+
+    (* If virtio == true, then a virtio kernel should have been
+     * installed.  If we didn't find one, it indicates a bug in
+     * virt-v2v.
+     *)
+    if virtio && bootable_kernel = None then
+      error (f_"virtio configured, but no virtio kernel found");
+
+    (* No bootable kernel was found.  Install one. *)
+    let bootable_kernel =
+      match bootable_kernel with
+      | Some k -> k
+      | None ->
+        (* Find which kernel is currently used by the guest. *)
+        let current_kernel =
+          let rec loop = function
+            | [] -> "kernel"
+            | path :: paths ->
+              let kernel =
+                Convert_linux_common.inspect_linux_kernel verbose g inspect
+                  path in
+              match kernel with
+              | None -> loop paths
+              | Some kernel -> kernel.Convert_linux_common.base_package
+          in
+          loop kernels in
+
+        (* Replace kernel-xen with a suitable kernel. *)
+        let current_kernel =
+          if string_find current_kernel "kernel-xen" >= 0 then
+            xen_replacement_kernel ()
+          else
+            current_kernel in
+
+        (* Install the kernel.  However we need a way to detect the
+         * version of the kernel that has just been installed.  A quick
+         * way is to compare /lib/modules before and after.
+         *)
+        let files1 = g#ls "/lib/modules" in
+        let files1 = Array.to_list files1 in
+        Convert_linux_common.install verbose g inspect [current_kernel];
+        let files2 = g#ls "/lib/modules" in
+        let files2 = Array.to_list files2 in
+
+        (* Note that g#ls is guaranteed to return the strings in order. *)
+        let rec loop files1 files2 =
+          match files1, files2 with
+          | [], [] ->
+            error (f_"tried to install '%s', but no kernel package was installed") current_kernel
+          | (v1 :: _), [] ->
+            error (f_"tried to install '%s', but there are now fewer directories under /lib/modules!") current_kernel
+          | [], (v2 :: _) -> v2
+          | (v1 :: _), (v2 :: _) when v1 <> v2 -> v2
+          | (_ :: v1s), (_ :: v2s) -> loop v1s v2s
+        in
+        let version = loop files1 files2 in
+
+        { Convert_linux_common.base_package = current_kernel;
+          version = version; modules = []; arch = "" } in
+
+    (* Set /etc/sysconfig/kernel DEFAULTKERNEL to point to the new
+     * kernel package name.
+     *)
+    if g#is_file ~followsymlinks:true "/etc/sysconfig/kernel" then (
+      let base_package = bootable_kernel.Convert_linux_common.base_package in
+      let paths =
+        g#aug_match "/files/etc/sysconfig/kernel/DEFAULTKERNEL/value" in
+      let paths = Array.to_list paths in
+      List.iter (fun path -> g#aug_set path base_package) paths;
+      g#aug_save ()
+    );
+
+    (* Return the installed kernel version. *)
+    bootable_kernel.Convert_linux_common.version
+
+  and supports_virtio { Convert_linux_common.modules = modules } =
+    List.mem "virtio_blk" modules && List.mem "virtio_net" modules
+
+  (* Is it a hypervisor-specific kernel? *)
+  and is_hv_kernel { Convert_linux_common.modules = modules } =
+    List.mem "xennet" modules           (* Xen PV kernel. *)
+
+  (* Find a suitable replacement for kernel-xen. *)
+  and xen_replacement_kernel () =
+    if is_rhel_family then (
+      match major_version, arch with
+      | 5, ("i386"|"i486"|"i586"|"i686") -> "kernel-PAE"
+      | 5, _ -> "kernel"
+      | 4, ("i386"|"i486"|"i586"|"i686") ->
+        (* If guest has >= 10GB of RAM, give it a hugemem kernel. *)
+        if source.s_memory >= 10L *^ 1024L *^ 1024L *^ 1024L then
+          "kernel-hugemem"
+        (* SMP kernel for guests with > 1 vCPU. *)
+        else if source.s_vcpu > 1 then
+          "kernel-smp"
+        else
+          "kernel"
+      | 4, _ ->
+        if source.s_vcpu > 8 then "kernel-largesmp"
+        else if source.s_vcpu > 1 then "kernel-smp"
+        else "kernel"
+      | _, _ -> "kernel"
+    )
+    else if is_suse_family then (
+      match distro, major_version, arch with
+      | "opensuse", _, _ -> "kernel-default"
+      | _, v, ("i386"|"i486"|"i586"|"i686") when v >= 11 ->
+        if source.s_memory >= 10L *^ 1024L *^ 1024L *^ 1024L then
+          "kernel-pae"
+        else
+          "kernel"
+      | _, v, _ when v >= 11 -> "kernel-default"
+      | _, 10, ("i386"|"i486"|"i586"|"i686") ->
+        if source.s_memory >= 10L *^ 1024L *^ 1024L *^ 1024L then
+          "kernel-bigsmp"
+        else if source.s_vcpu > 1 then
+          "kernel-smp"
+        else
+          "kernel-default"
+      | _, 10, _ ->
+        if source.s_vcpu > 1 then
+          "kernel-smp"
+        else
+          "kernel-default"
+      | _ -> "kernel-default"
+    )
+    else
+      "kernel" (* conservative default *)
+
+  (* We configure a console on ttyS0. Make sure existing console
+   * references use it.  N.B. Note that the RHEL 6 xen guest kernel
+   * presents a console device called /dev/hvc0, whereas previous xen
+   * guest kernels presented /dev/xvc0. The regular kernel running
+   * under KVM also presents a virtio console device called /dev/hvc0,
+   * so ideally we would just leave it alone. However, RHEL 6 libvirt
+   * doesn't yet support this device so we can't attach to it. We
+   * therefore use /dev/ttyS0 for RHEL 6 anyway.
+   *)
+  and configure_console () =
+    (* Look for gettys using xvc0 or hvc0.  RHEL 6 doesn't use inittab
+     * but this still works.
+     *)
+    let paths = g#aug_match "/files/etc/inittab/*/process" in
+    let paths = Array.to_list paths in
+    let rex = Str.regexp "\\(.*\\)\\b\\([xh]vc0\\)\\b\\(.*\\)" in
+    List.iter (
+      fun path ->
+        let proc = g#aug_get path in
+        if Str.string_match rex proc 0 then (
+          let proc = Str.global_replace rex "\\1ttyS0\\3" proc in
+          g#aug_set path proc
+        );
+    ) paths;
+
+    let paths = g#aug_match "/files/etc/securetty/*" in
+    let paths = Array.to_list paths in
+    List.iter (
+      fun path ->
+        let tty = g#aug_get path in
+        if tty = "xvc0" || tty = "hvc0" then
+          g#aug_set path "ttyS0"
+    ) paths;
+
+    g#aug_save ()
+
+  (* If the target doesn't support a serial console, we want to remove
+   * all references to it instead.
+   *)
+  and remove_console () =
+    (* Look for gettys using xvc0 or hvc0.  RHEL 6 doesn't use inittab
+     * but this still works.
+     *)
+    let paths = g#aug_match "/files/etc/inittab/*/process" in
+    let paths = Array.to_list paths in
+    let rex = Str.regexp ".*\\b\\([xh]vc0|ttyS0\\)\\b.*" in
+    List.iter (
+      fun path ->
+        let proc = g#aug_get path in
+        if Str.string_match rex proc 0 then
+          ignore (g#aug_rm (path ^ "/.."))
+    ) paths;
+
+    let paths = g#aug_match "/files/etc/securetty/*" in
+    let paths = Array.to_list paths in
+    List.iter (
+      fun path ->
+        let tty = g#aug_get path in
+        if tty = "xvc0" || tty = "hvc0" then
+          ignore (g#aug_rm path)
+    ) paths;
+
+    g#aug_save ()
+
+  (* Upgrade 'pkg' to >= minversion.  Returns true if that was possible. *)
+  and upgrade_package ?(ifinstalled = false) name minversion =
+
+
+
+
+
+    (* XXX *)
+    true
+
+
+  in
+
+  clean_rpmdb ();
+  autorelabel ();
+  Convert_linux_common.augeas_init verbose g;
+  let grub = get_grub () in
+
+  unconfigure_xen ();
+  unconfigure_vbox ();
+  unconfigure_vmware ();
+  unconfigure_citrix ();
+
+  let virtio = install_virtio () in
+  let kernel_version = configure_kernel virtio grub in (*XXX*) ignore kernel_version;
+  if keep_serial_console then (
+    configure_console ();
+    grub#configure_console ()
+  ) else (
+    remove_console ();
+    grub#remove_console ()
+  );
+
+
+
+
+
+
+
+
+
+  let guestcaps = {
+    gcaps_block_bus = if virtio then "virtio" else "ide";
+    gcaps_net_bus = if virtio then "virtio" else "e1000";
+  (* XXX display *)
+  } in
+
+  guestcaps
diff --git a/v2v/convert_linux_enterprise.mli b/v2v/convert_linux_enterprise.mli
new file mode 100644
index 0000000..f55ce15
--- /dev/null
+++ b/v2v/convert_linux_enterprise.mli
@@ -0,0 +1,19 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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.
+ *)
+
+val convert : ?keep_serial_console:bool -> bool -> Guestfs.guestfs -> Types.inspect -> Types.source -> Types.guestcaps
diff --git a/v2v/convert_linux_grub.ml b/v2v/convert_linux_grub.ml
new file mode 100644
index 0000000..59dd4f2
--- /dev/null
+++ b/v2v/convert_linux_grub.ml
@@ -0,0 +1,330 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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.
+ *)
+
+module G = Guestfs
+
+open Printf
+
+open Common_gettext.Gettext
+
+open Utils
+open Types
+
+(* Helper function for SUSE: remove (hdX,X) prefix from a path. *)
+let remove_hd_prefix =
+  let rex = Str.regexp "^(hd.*)\\(.*\\)" in
+  Str.replace_first rex "\\1"
+
+(* Helper function to check if guest is EFI. *)
+let check_efi g =
+  if Array.length (g#glob_expand "/boot/efi/EFI/*/grub.cfg") < 1 then
+    raise Not_found;
+
+  (* Check the first partition of each device looking for an EFI
+   * boot partition. We can't be sure which device is the boot
+   * device, so we just check them all.
+   *)
+  let devs = g#list_devices () in
+  let devs = Array.to_list devs in
+  List.find (
+    fun dev ->
+      try g#part_get_gpt_type dev 1 = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
+      with G.Error _ -> false
+  ) devs
+
+(* Virtual grub superclass. *)
+class virtual grub verbose (g : Guestfs.guestfs) inspect config_file =
+object
+  method virtual list_kernels : unit -> string list
+
+  method virtual configure_console : unit -> unit
+  method virtual remove_console : unit -> unit
+
+  method private get_default_image () =
+    let cmd =
+      if g#exists "/sbin/grubby" then
+        [| "grubby"; "--default-kernel" |]
+      else
+        [| "/usr/bin/perl"; "-MBootloader::Tools"; "-e"; "
+             InitLibrary();
+             my $default = Bootloader::Tools::GetDefaultSection();
+             print $default->{image};
+         " |] in
+    match g#command cmd with
+    | "" -> None
+    | k ->
+      let len = String.length k in
+      let k =
+        if len > 0 && k.[len-1] = '\n' then String.sub k 0 (len-1) else k in
+      Some (remove_hd_prefix k)
+end
+
+(* Concrete implementation for grub1. *)
+class grub1 verbose g inspect config_file grub_fs =
+object (self)
+  inherit grub verbose g inspect config_file
+
+  method private grub_fs = grub_fs      (* grub filesystem prefix *)
+
+  method list_kernels () =
+    let paths =
+      let expr = sprintf "/files%s/title/kernel" config_file in
+      let paths = g#aug_match expr in
+      let paths = Array.to_list paths in
+
+      (* Get the default kernel from grub if it's set. *)
+      let default =
+        let expr = sprintf "/files%s/default" config_file in
+        try
+          let idx = g#aug_get expr in
+          let idx = int_of_string idx in
+          (* Grub indices are zero-based, augeas is 1-based. *)
+          let expr = sprintf "/files%s/title[%d]/kernel" config_file (idx+1) in
+          Some expr
+        with Not_found -> None in
+
+      (* If a default kernel was set, put it at the beginning of the paths
+       * list.
+       *)
+      match default with
+      | None -> paths
+      | Some p -> p :: List.filter ((<>) p) paths in
+
+    (* Remove duplicates. *)
+    let paths =
+      let checked = Hashtbl.create 13 in
+      let rec loop = function
+        | [] -> []
+        | p :: ps when Hashtbl.mem checked p -> ps
+        | p :: ps -> Hashtbl.add checked p true; p :: loop ps
+      in
+      loop paths in
+
+    (* Resolve the Augeas paths to kernel filenames. *)
+    let kernels = List.map g#aug_get paths in
+
+    (* Make sure kernel does not begin with (hdX,X). *)
+    let kernels = List.map remove_hd_prefix kernels in
+
+    (* Prepend grub filesystem. *)
+    let kernels = List.map ((^) grub_fs) kernels in
+
+    (* Check the actual file exists. *)
+    let kernels = List.filter (g#is_file ~followsymlinks:true) kernels in
+
+    kernels
+
+  method configure_console () =
+    let rex = Str.regexp "\\(.*\\)\\b\\([xh]vc0\\)\\b\\(.*\\)" in
+    let expr = sprintf "/files%s/title/kernel/console" config_file in
+
+    let paths = g#aug_match expr in
+    let paths = Array.to_list paths in
+    List.iter (
+      fun path ->
+        let console = g#aug_get path in
+        if Str.string_match rex console 0 then (
+          let console = Str.global_replace rex "\\1ttyS0\\3" console in
+          g#aug_set path console
+        )
+    ) paths;
+
+    g#aug_save ()
+
+  method remove_console () =
+    let rex = Str.regexp "\\(.*\\)\\b\\([xh]vc0\\)\\b\\(.*\\)" in
+    let expr = sprintf "/files%s/title/kernel/console" config_file in
+
+    let rec loop = function
+      | [] -> ()
+      | path :: paths ->
+        let console = g#aug_get path in
+        if Str.string_match rex console 0 then (
+          ignore (g#aug_rm path);
+          (* All the paths are invalid, restart the loop. *)
+          let paths = g#aug_match expr in
+          let paths = Array.to_list paths in
+          loop paths
+        )
+        else
+          loop paths
+    in
+    let paths = g#aug_match expr in
+    let paths = Array.to_list paths in
+    loop paths;
+
+    g#aug_save ()
+
+end
+
+(* Create a grub1 object. *)
+let rec grub1 verbose (g : Guestfs.guestfs) inspect =
+  let root = inspect.i_root in
+
+  (* Look for a grub configuration file. *)
+  let config_file =
+    try
+      List.find (
+        fun file -> g#is_file ~followsymlinks:true file
+      ) ["/boot/grub/menu.lst"; "/boot/grub/grub.conf"]
+    with
+      Not_found ->
+        failwith (s_"no grub/grub1/grub-legacy configuration file was found") in
+
+  (* Check for EFI and convert if found. *)
+  (try let dev = check_efi g in grub1_convert_from_efi verbose g dev
+   with Not_found -> ()
+  );
+
+  (* Find the path that has to be prepended to filenames in grub.conf
+   * in order to make them absolute.
+   *)
+  let grub_fs =
+    let mounts = g#inspect_get_mountpoints root in
+    try
+      List.find (
+        fun path -> List.mem_assoc path mounts
+      ) [ "/boot/grub"; "/boot" ]
+    with Not_found -> "" in
+
+  (* Ensure Augeas is reading the grub configuration file, and if not
+   * then add it.
+   *)
+  let () =
+    let incls = g#aug_match "/augeas/load/Grub/incl" in
+    let incls = Array.to_list incls in
+    let incls_contains_conf =
+      List.exists (fun incl -> g#aug_get incl = config_file) incls in
+    if not incls_contains_conf then (
+      g#aug_set "/augeas/load/Grub/incl[last()+1]" config_file;
+      Convert_linux_common.augeas_reload verbose g;
+    ) in
+
+  new grub1 verbose g inspect config_file grub_fs
+
+(* Reinstall grub. *)
+and grub1_convert_from_efi verbose g dev =
+  g#cp "/etc/grub.conf" "/boot/grub/grub.conf";
+  g#ln_sf "/boot/grub/grub.conf" "/etc/grub.conf";
+
+  (* Reload Augeas to pick up new location of grub.conf. *)
+  Convert_linux_common.augeas_reload verbose g;
+
+  ignore (g#command [| "grub-install"; dev |])
+
+(* Concrete implementation for grub2. *)
+class grub2 verbose g inspect config_file =
+object (self)
+  inherit grub verbose g inspect config_file
+
+  method list_kernels () =
+    let files =
+      (match self#get_default_image () with
+      | None -> []
+      | Some k -> [k]) @
+        (* This is how the grub2 config generator enumerates kernels. *)
+        Array.to_list (g#glob_expand "/boot/kernel-*") @
+        Array.to_list (g#glob_expand "/boot/vmlinuz-*") @
+        Array.to_list (g#glob_expand "/vmlinuz-*") in
+    let rex = Str.regexp ".*\\.\\(dpkg-.*|rpmsave|rpmnew\\)$" in
+    let files = List.filter (
+      fun file -> not (Str.string_match rex file 0)
+    ) files in
+    files
+
+  method private update_console ~remove =
+    let rex = Str.regexp "\\(.*\\)\\bconsole=[xh]vc0\\b\\(.*\\)" in
+
+    let grub_cmdline_expr =
+      if g#exists "/etc/sysconfig/grub" then
+        "/files/etc/sysconfig/grub/GRUB_CMDLINE_LINUX"
+      else
+        "/files/etc/default/grub/GRUB_CMDLINE_LINUX_DEFAULT" in
+
+    (try
+       let grub_cmdline = g#aug_get grub_cmdline_expr in
+       let grub_cmdline =
+         if Str.string_match rex grub_cmdline 0 then (
+           if remove then
+             Str.global_replace rex "\\1\\3" grub_cmdline
+           else
+             Str.global_replace rex "\\1console=ttyS0\\3" grub_cmdline
+         )
+         else grub_cmdline in
+       g#aug_set grub_cmdline_expr grub_cmdline;
+       g#aug_save ();
+
+       ignore (g#command [| "grub2-mkconfig"; "-o"; config_file |])
+     with
+       G.Error msg ->
+         eprintf (f_"%s: warning: could not update grub2 console: %s (ignored)\n")
+           prog msg
+    )
+
+  method configure_console () = self#update_console ~remove:false
+  method remove_console () = self#update_console ~remove:true
+end
+
+let rec grub2 verbose (g : Guestfs.guestfs) inspect =
+  (* Look for a grub2 configuration file. *)
+  let config_file = "/boot/grub2/grub.cfg" in
+  if not (g#is_file ~followsymlinks:true config_file) then (
+    let msg =
+      sprintf (f_"no grub2 configuration file was found (expecting %s)")
+        config_file in
+    failwith msg
+  );
+
+  (* Check for EFI and convert if found. *)
+  (try
+     let dev = check_efi g in
+     grub2_convert_from_efi verbose g inspect dev
+   with Not_found -> ()
+  );
+
+  new grub2 verbose g inspect config_file
+
+(* For grub2:
+ * - Turn the EFI partition into a BIOS Boot Partition
+ * - Remove the former EFI partition from fstab
+ * - Install the non-EFI version of grub
+ * - Install grub2 in the BIOS Boot Partition
+ * - Regenerate grub.cfg
+ *)
+and grub2_convert_from_efi verbose g inspect dev =
+  (* EFI systems boot using grub2-efi, and probably don't have the
+   * base grub2 package installed.
+   *)
+  Convert_linux_common.install verbose g inspect ["grub2"];
+
+  (* Relabel the EFI boot partition as a BIOS boot partition. *)
+  g#part_set_gpt_type dev 1 "21686148-6449-6E6F-744E-656564454649";
+
+  (* Delete the fstab entry for the EFI boot partition. *)
+  let nodes = g#aug_match "/files/etc/fstab/*[file = '/boot/efi']" in
+  let nodes = Array.to_list nodes in
+  List.iter (fun node -> ignore (g#aug_rm node)) nodes;
+  g#aug_save ();
+
+  (* Install grub2 in the BIOS boot partition. This overwrites the
+   * previous contents of the EFI boot partition.
+   *)
+  ignore (g#command [| "grub2-install"; dev |]);
+
+  (* Re-generate the grub2 config, and put it in the correct place *)
+  ignore (g#command [| "grub2-mkconfig"; "-o"; "/boot/grub2/grub.cfg" |])
diff --git a/v2v/convert_linux_grub.mli b/v2v/convert_linux_grub.mli
new file mode 100644
index 0000000..324a333
--- /dev/null
+++ b/v2v/convert_linux_grub.mli
@@ -0,0 +1,43 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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.
+ *)
+
+(** Common code handling grub1 (grub-legacy) and grub2 operations. *)
+
+class type virtual grub = object
+  method virtual list_kernels : unit -> string list
+  (** Return a list of kernels from the grub configuration.  The
+      returned list is a list of filenames. *)
+  method virtual configure_console : unit -> unit
+  (** Reconfigure the grub console. *)
+  method virtual remove_console : unit -> unit
+  (** Remove the grub console configuration. *)
+end
+
+val grub1 : bool -> Guestfs.guestfs -> Types.inspect -> grub
+(** Detect if grub1/grub-legacy is used by this guest and return a
+    grub object if so.
+
+    This raises [Failure] if grub1 is not used by this guest or some
+    other problem happens. *)
+
+val grub2 : bool -> Guestfs.guestfs -> Types.inspect -> grub
+(** Detect if grub2 is used by this guest and return a grub object
+    if so.
+
+    This raises [Failure] if grub2 is not used by this guest or some
+    other problem happens. *)
diff --git a/v2v/convert_windows.ml b/v2v/convert_windows.ml
new file mode 100644
index 0000000..e5d1ea8
--- /dev/null
+++ b/v2v/convert_windows.ml
@@ -0,0 +1,22 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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.
+ *)
+
+(* Convert Windows guests. *)
+
+let convert verbose g inspect =
+  assert false
diff --git a/v2v/convert_windows.mli b/v2v/convert_windows.mli
new file mode 100644
index 0000000..d1e60fe
--- /dev/null
+++ b/v2v/convert_windows.mli
@@ -0,0 +1,19 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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.
+ *)
+
+val convert : bool -> Guestfs.guestfs -> Types.inspect -> Types.guestcaps
diff --git a/v2v/link.sh.in b/v2v/link.sh.in
new file mode 100644
index 0000000..9e5684b
--- /dev/null
+++ b/v2v/link.sh.in
@@ -0,0 +1,22 @@
+# libguestfs Makefile.am
+# @configure_input@
+# (C) Copyright 2014 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+# Hack automake to link binary properly.  There is no other way to add
+# the -cclib parameter to the end of the command line.
+
+exec "$@" -linkpkg -cclib '-lutils -lncurses @LIBXML2_LIBS@ -lgnu'
diff --git a/v2v/source_libvirt.ml b/v2v/source_libvirt.ml
new file mode 100644
index 0000000..bdea8d4
--- /dev/null
+++ b/v2v/source_libvirt.ml
@@ -0,0 +1,118 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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 Printf
+
+open Common_gettext.Gettext
+open Common_utils
+
+open Types
+open Utils
+
+let create_xml xml =
+  let doc = Xml.parse_memory xml in
+  let xpathctx = Xml.xpath_new_context doc in
+
+  let xpath_to_string expr default =
+    let obj = Xml.xpath_eval_expression xpathctx expr in
+    if Xml.xpathobj_nr_nodes obj < 1 then default
+    else (
+      let node = Xml.xpathobj_node doc obj 0 in
+      Xml.node_as_string node
+    ) in
+  let xpath_to_int expr default =
+    let obj = Xml.xpath_eval_expression xpathctx expr in
+    if Xml.xpathobj_nr_nodes obj < 1 then default
+    else (
+      let node = Xml.xpathobj_node doc obj 0 in
+      let str = Xml.node_as_string node in
+      try int_of_string str
+      with Failure "int_of_string" ->
+        error (f_"expecting XML expression to return an integer (expression: %s)")
+          expr
+    ) in
+
+  let dom_type = xpath_to_string "/domain/@type" "" in
+  let name = xpath_to_string "/domain/name/text()" "" in
+  let memory = xpath_to_int "/domain/memory/text()" 0 in
+  let memory = Int64.of_int memory *^ 1024L in
+  let vcpu = xpath_to_int "/domain/vcpu/text()" 0 in
+  let arch = xpath_to_string "/domain/os/type/@arch" "" in
+
+  let features =
+    let features = ref [] in
+    let obj = Xml.xpath_eval_expression xpathctx "/domain/features/*" in
+    let nr_nodes = Xml.xpathobj_nr_nodes obj in
+    for i = 0 to nr_nodes-1 do
+      let node = Xml.xpathobj_node doc obj i in
+      features := Xml.node_name node :: !features
+    done;
+    !features in
+
+  (* Non-removable disk devices. *)
+  let disks =
+    let disks = ref [] in
+    let obj =
+      Xml.xpath_eval_expression xpathctx
+        "/domain/devices/disk[@device='disk']" in
+    let nr_nodes = Xml.xpathobj_nr_nodes obj in
+    if nr_nodes < 1 then
+      error (f_"this guest has no non-removable disks");
+    for i = 0 to nr_nodes-1 do
+      let node = Xml.xpathobj_node doc obj i in
+      Xml.xpathctx_set_current_context xpathctx node;
+      let path = xpath_to_string "source/@file | source/@dev" "" in
+      if path <> "" then (
+        let format =
+          let format = xpath_to_string "driver/@type" "" in
+          if format <> "" then Some format else None in
+        disks := (path, format) :: !disks
+      )
+    done;
+    List.rev !disks in
+
+  (* XXX Much more metadata needs to be collected here:
+   * - graphics
+   * - cdroms
+   * - floppies
+   * - network interfaces
+   * See: lib/Sys/VirtConvert/Connection/LibVirt.pm
+   *)
+
+  {
+    s_dom_type = dom_type;
+    s_name = name;
+    s_memory = memory;
+    s_vcpu = vcpu;
+    s_arch = arch;
+    s_features = features;
+    s_disks = disks;
+  }
+
+let create_from_xml file =
+  let xml = read_whole_file file in
+  create_xml xml
+
+let create libvirt_uri guest =
+  let cmd =
+    match libvirt_uri with
+    | None -> sprintf "virsh dumpxml %s" (quote guest)
+    | Some uri -> sprintf "virsh -c %s dumpxml %s" (quote uri) (quote guest) in
+  let lines = external_command ~prog cmd in
+  let xml = String.concat "\n" lines in
+  create_xml xml
diff --git a/v2v/source_libvirt.mli b/v2v/source_libvirt.mli
new file mode 100644
index 0000000..1e3b1e1
--- /dev/null
+++ b/v2v/source_libvirt.mli
@@ -0,0 +1,27 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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.
+ *)
+
+(** [-i libvirt] and [-i libvirtxml] sources. *)
+
+val create : string option -> string -> Types.source
+(** [create libvirt_uri guest] reads the source metadata from the
+    named libvirt guest. *)
+
+val create_from_xml : string -> Types.source
+(** [create_from_xml filename] reads the source metadata from the
+    libvirt XML file. *)
diff --git a/v2v/target_local.ml b/v2v/target_local.ml
new file mode 100644
index 0000000..ed4e5e3
--- /dev/null
+++ b/v2v/target_local.ml
@@ -0,0 +1,86 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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 Printf
+
+open Common_gettext.Gettext
+open Common_utils
+
+open Types
+open Utils
+
+let initialize dir overlays =
+  List.map (
+    fun ov ->
+      let target_file = dir // "disk-" ^ ov.ov_sd in
+      { ov with ov_target_file = target_file; ov_target_file_tmp = target_file }
+  ) overlays
+
+let create_metadata dir source overlays guestcaps =
+  let name = source.s_name in
+  let file = dir // name ^ ".xml" in
+
+  let chan = open_out file in
+  let p fs = fprintf chan fs in
+
+  p "<domain type='%s'>\n" "kvm"; (* Always assume target is kvm? *)
+  p "  <name>%s</name>\n" name;
+  let memory_k = source.s_memory /^ 1024L in
+  p "  <memory unit='KiB'>%Ld</memory>\n" memory_k;
+  p "  <currentMemory unit='KiB'>%Ld</currentMemory>\n" memory_k;
+  p "  <vcpu>%d</vcpu>\n" source.s_vcpu;
+  p "  <os>\n";
+  p "    <type arch='%s'>hvm</type>\n" source.s_arch;
+  p "  </os>\n";
+  p "  <features>\n";
+  List.iter (p "    <%s/>\n") source.s_features;
+  p "  </features>\n";
+
+  p "  <on_poweroff>destroy</on_poweroff>\n";
+  p "  <on_reboot>restart</on_reboot>\n";
+  p "  <on_crash>restart</on_crash>\n";
+  p "  <devices>\n";
+
+  let block_prefix =
+    if guestcaps.gcaps_block_bus = "virtio" then "vd" else "hd" in
+  iteri (
+    fun i ov ->
+      p "    <disk type='file' device='disk'>\n";
+      p "      <driver name='qemu' type='%s' cache='none'/>\n"
+        ov.ov_target_format;
+      p "      <source file='%s'/>\n" (xml_quote_attr ov.ov_target_file);
+      p "      <target dev='%s%s' bus='%s'/>\n"
+        block_prefix (drive_name i) guestcaps.gcaps_block_bus;
+      p "    </disk>\n";
+  ) overlays;
+
+  p "    <input type='tablet' bus='usb'/>\n";
+  p "    <input type='mouse' bus='ps2'/>\n";
+  p "    <console type='pty'/>\n";
+
+  (* XXX Missing here from old virt-v2v:
+     <video/>
+     <graphics/>
+     cdroms and floppies
+     network interfaces
+     See: lib/Sys/VirtConvert/Connection/LibVirtTarget.pm
+  *)
+
+  p "</domain>\n";
+
+  close_out chan
diff --git a/v2v/target_local.mli b/v2v/target_local.mli
new file mode 100644
index 0000000..1833ecb
--- /dev/null
+++ b/v2v/target_local.mli
@@ -0,0 +1,21 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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.
+ *)
+
+val initialize : string -> Types.overlay list -> Types.overlay list
+
+val create_metadata : string -> Types.source -> Types.overlay list -> Types.guestcaps -> unit
diff --git a/v2v/types.ml b/v2v/types.ml
new file mode 100644
index 0000000..0f5ae86
--- /dev/null
+++ b/v2v/types.ml
@@ -0,0 +1,84 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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 Printf
+
+(* Types.  See types.mli for documentation. *)
+
+type input =
+| InputLibvirt of string option * string
+| InputLibvirtXML of string
+
+type output =
+| OutputLibvirt of string option
+| OutputLocal of string
+| OutputRHEV of string
+
+type source = {
+  s_dom_type : string;
+  s_name : string;
+  s_memory : int64;
+  s_vcpu : int;
+  s_arch : string;
+  s_features : string list;
+  s_disks : source_disk list;
+}
+and source_disk = string * string option
+
+type overlay = {
+  ov_overlay : string;
+  ov_target_file : string;
+  ov_target_file_tmp : string;
+  ov_target_format : string;
+  ov_sd : string;
+  ov_virtual_size : int64;
+  ov_preallocation : string option;
+  ov_source_file : string;
+  ov_source_format : string option;
+}
+
+let string_of_overlay ov =
+  sprintf "\
+ov_overlay = %s
+ov_target_file = %s
+ov_target_file_tmp = %s
+ov_target_format = %s
+ov_sd = %s
+ov_virtual_size = %Ld
+ov_preallocation = %s
+ov_source_file = %s
+ov_source_format = %s
+"
+    ov.ov_overlay
+    ov.ov_target_file ov.ov_target_file_tmp
+    ov.ov_target_format
+    ov.ov_sd
+    ov.ov_virtual_size
+    (match ov.ov_preallocation with None -> "None" | Some s -> s)
+    ov.ov_source_file
+    (match ov.ov_source_format with None -> "None" | Some s -> s)
+
+type inspect = {
+  i_root : string;
+  i_apps : Guestfs.application2 list;
+}
+
+type guestcaps = {
+  gcaps_block_bus : string;
+  gcaps_net_bus : string;
+}
diff --git a/v2v/types.mli b/v2v/types.mli
new file mode 100644
index 0000000..e7e72e0
--- /dev/null
+++ b/v2v/types.mli
@@ -0,0 +1,77 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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.
+ *)
+
+(** Types. *)
+
+type input =
+| InputLibvirt of string option * string (* -i libvirt: -ic + guest name *)
+| InputLibvirtXML of string         (* -i libvirtxml: XML file name *)
+(** The input arguments as specified on the command line. *)
+
+type output =
+| OutputLibvirt of string option    (* -o libvirt: -oc *)
+| OutputLocal of string             (* -o local: directory *)
+| OutputRHEV of string              (* -o rhev: output storage *)
+(** The output arguments as specified on the command line. *)
+
+type source = {
+  s_dom_type : string;                  (** Source domain type, eg "kvm" *)
+  s_name : string;                      (** Guest name. *)
+  s_memory : int64;                     (** Memory size (bytes). *)
+  s_vcpu : int;                         (** Number of CPUs. *)
+  s_arch : string;                      (** Architecture. *)
+  s_features : string list;             (** Machine features. *)
+  s_disks : source_disk list;           (** Disk images. *)
+}
+(** The source: metadata, disk images. *)
+
+and source_disk = string * string option
+(** A source file is a qemu URI and a format. *)
+
+type overlay = {
+  ov_overlay : string;       (** Local overlay file (qcow2 format). *)
+  ov_target_file : string;   (** Destination file (real). *)
+  ov_target_file_tmp : string;    (** Destination file (temporary). *)
+  ov_target_format : string; (** Destination format (eg. -of option). *)
+  ov_sd : string;            (** sdX libguestfs name of disk. *)
+  ov_virtual_size : int64;   (** Virtual disk size in bytes. *)
+  ov_preallocation : string option;     (** ?preallocation option. *)
+
+  (* Note: the next two fields are for information only and must not
+   * be opened/copied/etc.
+   *)
+  ov_source_file : string;   (** qemu URI for source file. *)
+  ov_source_format : string option; (** Source file format, if known. *)
+}
+(** Disk overlays and destination disks. *)
+
+val string_of_overlay : overlay -> string
+
+type inspect = {
+  i_root : string;                      (** Root device. *)
+  i_apps : Guestfs.application2 list;   (** Packages installed. *)
+}
+(** Inspection information.  Only the applications list is stored here
+    as that is the only one which is slow/inconvenient to fetch. *)
+
+type guestcaps = {
+  gcaps_block_bus : string;    (** "virtio", "ide", possibly others *)
+  gcaps_net_bus : string;      (** "virtio", "e1000", possibly others *)
+  (* XXX acpi, display *)
+}
+(** Guest capabilities after conversion.  eg. Was virtio found or installed? *)
diff --git a/v2v/utils-c.c b/v2v/utils-c.c
new file mode 100644
index 0000000..f6a5d74
--- /dev/null
+++ b/v2v/utils-c.c
@@ -0,0 +1,43 @@
+/* virt-v2v
+ * Copyright (C) 2009-2014 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.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <caml/alloc.h>
+#include <caml/memory.h>
+#include <caml/mlvalues.h>
+
+#include "guestfs.h"
+#include "guestfs-internal-frontend.h"
+
+value
+v2v_utils_drive_name (value indexv)
+{
+  CAMLparam1 (indexv);
+  CAMLlocal1 (namev);
+  char name[64];
+
+  guestfs___drive_name (Int_val (indexv), name);
+  namev = caml_copy_string (name);
+
+  CAMLreturn (namev);
+}
diff --git a/v2v/utils.ml b/v2v/utils.ml
new file mode 100644
index 0000000..6155f9a
--- /dev/null
+++ b/v2v/utils.ml
@@ -0,0 +1,44 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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.
+ *)
+
+(* Utilities used in virt-v2v only. *)
+
+open Printf
+
+open Common_gettext.Gettext
+open Common_utils
+
+open Types
+
+let prog = Filename.basename Sys.executable_name
+let error ?exit_code fs = error ~prog ?exit_code fs
+
+let quote = Filename.quote
+
+(* Quote XML <element attr='...'> content.  Note you must use single
+ * quotes around the attribute.
+ *)
+let xml_quote_attr str =
+  let str = Common_utils.replace_str str "&" "&" in
+  let str = Common_utils.replace_str str "'" "'" in
+  let str = Common_utils.replace_str str "<" "<" in
+  let str = Common_utils.replace_str str ">" ">" in
+  str
+
+external drive_name : int -> string = "v2v_utils_drive_name"
+
diff --git a/v2v/v2v.ml b/v2v/v2v.ml
new file mode 100644
index 0000000..e21cb79
--- /dev/null
+++ b/v2v/v2v.ml
@@ -0,0 +1,353 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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
+
+open Common_gettext.Gettext
+
+module G = Guestfs
+
+open Common_utils
+open Types
+open Utils
+
+let () = Random.self_init ()
+
+let rec main () =
+  (* Handle the command line. *)
+  let input, output,
+    debug_gc, output_alloc, output_format, output_name,
+    quiet, root_choice, trace, verbose =
+    Cmdline.parse_cmdline () in
+
+  let msg fs = make_message_function ~quiet fs in
+
+  let source =
+    match input with
+    | InputLibvirt (libvirt_uri, guest) ->
+      Source_libvirt.create libvirt_uri guest
+    | InputLibvirtXML filename ->
+      Source_libvirt.create_from_xml filename in
+
+  (* Create a qcow2 v3 overlay to protect the source image(s).  There
+   * is a specific reason to use the newer qcow2 variant: Because the
+   * L2 table can store zero clusters efficiently, and because
+   * discarded blocks are stored as zero clusters, this should allow us
+   * to fstrim/blkdiscard and avoid copying significant parts of the
+   * data over the wire.
+   *)
+  msg (f_"Creating an overlay to protect the source from being modified");
+  let overlays =
+    List.map (
+      fun (qemu_uri, format) ->
+        let overlay = Filename.temp_file "v2vovl" ".qcow2" in
+        unlink_on_exit overlay;
+
+        let options =
+          "compat=1.1,lazy_refcounts=on" ^
+            (match format with None -> ""
+            | Some fmt -> ",backing_fmt=" ^ fmt) in
+        let cmd =
+          sprintf "qemu-img create -q -f qcow2 -b %s -o %s %s"
+            (quote qemu_uri) (quote options) overlay in
+        if Sys.command cmd <> 0 then
+          error (f_"qemu-img command failed, see earlier errors");
+        overlay, qemu_uri, format
+    ) source.s_disks in
+
+  (* Open the guestfs handle. *)
+  msg (f_"Opening the overlay");
+  let g = new G.guestfs () in
+  g#set_trace trace;
+  g#set_verbose verbose;
+  g#set_network true;
+  List.iter (
+    fun (overlay, _, _) ->
+      g#add_drive_opts overlay
+        ~format:"qcow2" ~cachemode:"unsafe" ~discard:"besteffort"
+  ) overlays;
+
+  g#launch ();
+
+  (* Work out where we will write the final output.  Do this early
+   * just so we can display errors to the user before doing too much
+   * work.
+   *)
+  let overlays =
+    initialize_target g output output_alloc output_format overlays in
+
+  (* Inspection - this also mounts up the filesystems. *)
+  msg (f_"Inspecting the overlay");
+  let inspect = inspect_source g root_choice in
+
+  (* Conversion. *)
+  let guestcaps =
+    let root = inspect.i_root in
+
+    (match g#inspect_get_product_name root with
+    | "unknown" ->
+      msg (f_"Converting the guest to run on KVM")
+    | prod ->
+      msg (f_"Converting %s to run on KVM") prod
+    );
+
+    match g#inspect_get_type root with
+    | "linux" ->
+      (match g#inspect_get_distro root with
+      | "fedora"
+      | "rhel" | "centos" | "scientificlinux" | "redhat-based"
+      | "sles" | "suse-based" | "opensuse" ->
+
+        (* RHEV doesn't support serial console so remove any on conversion. *)
+        let keep_serial_console =
+          match output with
+          | OutputRHEV _ -> Some false
+          | OutputLibvirt _ | OutputLocal _ -> None in
+
+        Convert_linux_enterprise.convert ?keep_serial_console
+          verbose g inspect source
+
+      | distro ->
+        error (f_"virt-v2v is unable to convert this guest type (linux/distro=%s)") distro
+      );
+
+    | "windows" -> Convert_windows.convert verbose g inspect
+
+    | typ ->
+      error (f_"virt-v2v is unable to convert this guest type (type=%s)") typ in
+
+  (* Trim the filesystems to reduce transfer size. *)
+  msg (f_"Trimming filesystems to reduce amount of data to copy");
+  let () =
+    let mps = g#mountpoints () in
+    List.iter (
+      fun (_, mp) ->
+        try g#fstrim mp
+        with G.Error msg -> eprintf "%s: %s (ignored)\n" mp msg
+    ) mps in
+
+  msg (f_"Closing the overlay");
+  g#umount_all ();
+  g#shutdown ();
+  g#close ();
+
+  (* Copy the source to the output. *)
+  let delete_target_on_exit = ref true in
+  at_exit (fun () ->
+    if !delete_target_on_exit then (
+      List.iter (
+        fun ov -> try Unix.unlink ov.ov_target_file_tmp with _ -> ()
+      ) overlays
+    )
+  );
+  let nr_overlays = List.length overlays in
+  iteri (
+    fun i ov ->
+      msg (f_"Copying disk %d/%d to %s (%s)")
+        (i+1) nr_overlays ov.ov_target_file ov.ov_target_format;
+      if verbose then printf "%s\n%!" (string_of_overlay ov);
+
+      (* It turns out that libguestfs's disk creation code is
+       * considerably more flexible and easier to use than qemu-img, so
+       * create the disk explicitly using libguestfs then pass the
+       * 'qemu-img convert -n' option so qemu reuses the disk.
+       *)
+      let preallocation = ov.ov_preallocation in
+      let compat =
+        match ov.ov_target_format with "qcow2" -> Some "1.1" | _ -> None in
+      (new G.guestfs ())#disk_create ov.ov_target_file_tmp
+        ov.ov_target_format ov.ov_virtual_size ?preallocation ?compat;
+
+      let cmd =
+        sprintf "qemu-img convert -n -f qcow2 -O %s %s %s"
+          (quote ov.ov_target_format) (quote ov.ov_overlay)
+          (quote ov.ov_target_file_tmp) in
+      if verbose then printf "%s\n%!" cmd;
+      if Sys.command cmd <> 0 then
+        error (f_"qemu-img command failed, see earlier errors");
+  ) overlays;
+
+  (* Create output metadata. *)
+  msg (f_"Creating output metadata");
+  let () =
+    (* Are we going to rename the guest? *)
+    let renamed_source =
+      match output_name with
+      | None -> source
+      | Some name -> { source with s_name = name } in
+    match output with
+    | OutputLibvirt oc -> assert false
+    | OutputLocal dir ->
+      Target_local.create_metadata dir renamed_source overlays guestcaps
+    | OutputRHEV os -> assert false in
+
+  (* If we wrote to a temporary file, rename to the real file. *)
+  List.iter (
+    fun ov ->
+      if ov.ov_target_file_tmp <> ov.ov_target_file then
+        rename ov.ov_target_file_tmp ov.ov_target_file
+  ) overlays;
+
+  delete_target_on_exit := false;
+
+  msg (f_"Finishing off");
+
+  if debug_gc then
+    Gc.compact ()
+
+and initialize_target g output output_alloc output_format overlays =
+  let overlays =
+    mapi (
+      fun i (overlay, qemu_uri, backing_format) ->
+        (* Grab the virtual size of each disk. *)
+        let sd = "sd" ^ drive_name i in
+        let dev = "/dev/" ^ sd in
+        let vsize = g#blockdev_getsize64 dev in
+
+        (* What output format should we use? *)
+        let format =
+          match output_format, backing_format with
+          | Some format, _ -> format    (* -of overrides everything *)
+          | None, Some format -> format (* same as backing format *)
+          | None, None ->
+            error (f_"disk %s (%s) has no defined format, you have to either define the original format in the source metadata, or use the '-of' option to force the output format") sd qemu_uri in
+
+        (* What output preallocation mode should we use? *)
+        let preallocation =
+          match format, output_alloc with
+          | "raw", `Sparse -> Some "sparse"
+          | "raw", `Preallocated -> Some "full"
+          | "qcow2", `Sparse -> Some "off" (* ? *)
+          | "qcow2", `Preallocated -> Some "metadata"
+          | _ -> None (* ignore -oa flag for other formats *) in
+
+        { ov_overlay = overlay;
+          ov_target_file = ""; ov_target_file_tmp = "";
+          ov_target_format = format;
+          ov_sd = sd; ov_virtual_size = vsize; ov_preallocation = preallocation;
+          ov_source_file = qemu_uri; ov_source_format = backing_format; }
+    ) overlays in
+  let overlays =
+    match output with
+    | OutputLibvirt oc -> assert false
+    | OutputLocal dir -> Target_local.initialize dir overlays
+    | OutputRHEV os -> assert false in
+  overlays
+
+and inspect_source g root_choice =
+  let roots = g#inspect_os () in
+  let roots = Array.to_list roots in
+
+  let root =
+    match roots with
+    | [] ->
+      error (f_"no root device found in this operating system image.");
+    | [root] -> root
+    | roots ->
+      match root_choice with
+      | `Ask ->
+        (* List out the roots and ask the user to choose. *)
+        printf "\n***\n";
+        printf (f_"dual- or multi-boot operating system detected. Choose the root filesystem\nthat contains the main operating system from the list below:\n");
+        printf "\n";
+        iteri (
+          fun i root ->
+            let prod = g#inspect_get_product_name root in
+            match prod with
+            | "unknown" -> printf " [%d] %s\n" i root
+            | prod -> printf " [%d] %s (%s)\n" i root prod
+        ) roots;
+        printf "\n";
+        let i = ref 0 in
+        let n = List.length roots in
+        while !i < 1 || !i > n do
+          printf (f_"Enter number between 1 and %d: ") n;
+          (try i := int_of_string (read_line ())
+           with
+           | End_of_file -> error (f_"connection closed")
+           | Failure "int_of_string" -> ()
+          )
+        done;
+        List.nth roots (!i - 1)
+
+      | `Single ->
+        error (f_"multi-boot operating systems are not supported by virt-v2v. Use the --root option to change how virt-v2v handles this.")
+
+      | `First ->
+        List.hd roots
+
+      | `Dev dev ->
+        if List.mem dev roots then dev
+        else
+          error (f_"root device %s not found.  Roots found were: %s")
+            dev (String.concat " " roots) in
+
+  (* Reject this OS if it doesn't look like an installed image. *)
+  let () =
+    let fmt = g#inspect_get_format root in
+    if fmt <> "installed" then
+      error (f_"libguestfs thinks this is not an installed operating system (it might be, for example, an installer disk or live CD).  If this is wrong, it is probably a bug in libguestfs.  root=%s fmt=%s") root fmt in
+
+  (* Mount up the filesystems. *)
+  let mps = g#inspect_get_mountpoints root in
+  let cmp (a,_) (b,_) = compare (String.length a) (String.length b) in
+  let mps = List.sort cmp mps in
+  List.iter (
+    fun (mp, dev) ->
+      try g#mount dev mp
+      with G.Error msg -> eprintf "%s (ignored)\n" msg
+  ) mps;
+
+  (* Get list of applications/packages installed. *)
+  let apps = g#inspect_list_applications2 root in
+  let apps = Array.to_list apps in
+
+  { i_root = root; i_apps = apps; }
+    
+let () =
+  try main ()
+  with
+  | Unix.Unix_error (code, fname, "") -> (* from a syscall *)
+    eprintf (f_"%s: error: %s: %s\n") prog fname (Unix.error_message code);
+    exit 1
+  | Unix.Unix_error (code, fname, param) -> (* from a syscall *)
+    eprintf (f_"%s: error: %s: %s: %s\n") prog fname (Unix.error_message code)
+      param;
+    exit 1
+  | Sys_error msg ->                    (* from a syscall *)
+    eprintf (f_"%s: error: %s\n") prog msg;
+    exit 1
+  | G.Error msg ->                      (* from libguestfs *)
+    eprintf (f_"%s: libguestfs error: %s\n") prog msg;
+    exit 1
+  | Failure msg ->                      (* from failwith/failwithf *)
+    eprintf (f_"%s: failure: %s\n") prog msg;
+    exit 1
+  | Invalid_argument msg ->             (* probably should never happen *)
+    eprintf (f_"%s: internal error: invalid argument: %s\n") prog msg;
+    exit 1
+  | Assert_failure (file, line, char) -> (* should never happen *)
+    eprintf (f_"%s: internal error: assertion failed at %s, line %d, char %d\n") prog file line char;
+    exit 1
+  | Not_found ->                        (* should never happen *)
+    eprintf (f_"%s: internal error: Not_found exception was thrown\n") prog;
+    exit 1
+  | exn ->                              (* something not matched above *)
+    eprintf (f_"%s: exception: %s\n") prog (Printexc.to_string exn);
+    exit 1
diff --git a/v2v/virt-v2v.pod b/v2v/virt-v2v.pod
new file mode 100644
index 0000000..138e73b
--- /dev/null
+++ b/v2v/virt-v2v.pod
@@ -0,0 +1,301 @@
+=head1 NAME
+
+virt-v2v - Convert a guest to use KVM
+
+=head1 SYNOPSIS
+
+ virt-v2v -ic esx://esx.example.com/ -os imported esx_guest
+
+ virt-v2v -ic esx://esx.example.com/ \
+   -o rhev -os rhev.nfs:/export_domain --network rhevm esx_guest
+
+ virt-v2v -i libvirtxml -o local -os /tmp guest-domain.xml
+
+=head1 DESCRIPTION
+
+Virt-v2v converts guests from a foreign hypervisor to run on KVM,
+managed by libvirt or Red Hat Enterprise Virtualisation (RHEV) version
+2.2 or later. It can currently convert Red Hat Enterprise Linux and
+Windows guests running on Xen and VMware ESX.
+
+There is also a companion front-end called "virt-p2v" which comes as an
+ISO or CD image that can be booted on physical machines.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<--help>
+
+Display help.
+
+=item B<--debug-gc>
+
+Debug garbage collection and memory allocation.  This is only useful
+when debugging memory problems in virt-v2v or the OCaml libguestfs
+bindings.
+
+=item B<-i libvirt>
+
+Set the input method to I<libvirt>.  This is the default.
+
+In this mode you have to specify a libvirt guest name on the command
+line.  You may also specify a libvirt connection URI (see I<-ic>).
+
+=item B<-i libvirtxml>
+
+Set the input method to I<libvirtxml>.
+
+In this mode you have to pass a libvirt XML file on the command line.
+This file is read in order to get metadata about the source guest
+(such as its name, amount of memory), and also to locate the input
+disks.
+
+=item B<-ic> libvirtURI
+
+Specify a libvirt connection URI to use when reading the guest.  This
+is only used when S<I<-i libvirt>>.
+
+Only local libvirt connections and ESX connections can be used.
+Remote libvirt connections will not work in general.
+
+=item B<--machine-readable>
+
+This option is used to make the output more machine friendly
+when being parsed by other programs.  See
+L</MACHINE READABLE OUTPUT> below.
+
+=item B<-o libvirt>
+
+Set the output method to I<libvirt>.  This is the default.
+
+In this mode, the converted guest is created as a libvirt guest.  You
+may also specify a libvirt connection URI (see I<-oc>).
+
+=item B<-o local>
+
+Set the output method to I<local>.
+
+In this mode, the converted guest is written to a local directory
+specified by I<-os /dir> (the directory must exist).  The converted
+guest's disks are written as:
+
+ /dir/disk-sda
+ /dir/disk-sdb
+ [etc]
+
+and a libvirt XML file is created containing guest metadata
+(C</dir/name.xml>, where C<name> is the guest name).
+
+=item B<-o rhev>
+
+Set the output method to I<rhev>.
+
+The converted guest is written to a RHEV Export Storage Domain.  The
+I<-os> parameter must also be used to specify the location of the
+Export Storage Domain.  Note this does not actually import the guest
+into RHEV.  You have to do that manually later using the UI.
+
+=item B<-oa sparse>
+
+=item B<-oa preallocated>
+
+Set the output file allocation mode.  The default is C<sparse>.
+
+=item B<-oc> libvirtURI
+
+Specify a libvirt connection to use when writing the converted guest.
+This is only used when S<I<-o libvirt>>.
+
+Only local libvirt connections can be used.  Remote libvirt
+connections will not work.
+
+=item B<-of> format
+
+When converting the guest, convert the disks to the given format.
+
+If not specified, then the input format is used.
+
+=item B<-on> name
+
+Rename the guest when converting it.  If this option is not used then
+the output name is the same as the input name.
+
+=item B<-os> storage
+
+The location of the storage for the converted guest.
+
+For I<-o libvirt>, this is a libvirt pool (see S<C<virsh pool-list>>).
+
+For I<-o local>, this is a directory name.  The directory must exist.
+
+For I<-o rhev>, this is an NFS path of the form
+C<E<lt>hostE<gt>:E<lt>pathE<gt>>, eg:
+
+ rhev-storage.example.com:/rhev/export
+
+The NFS export must be mountable and writable by the user and host
+running virt-v2v, since the virt-v2v program has to actually mount it
+when it runs.
+
+=item B<-q>
+
+=item B<--quiet>
+
+This disables progress bars and other unnecessary output.
+
+=item B<--root ask>
+
+=item B<--root single>
+
+=item B<--root first>
+
+=item B<--root> /dev/sdX
+
+=item B<--root> /dev/VG/LV
+
+Choose the root filesystem to be converted.
+
+In the case where the virtual machine is dual-boot or multi-boot, or
+where the VM has other filesystems that look like operating systems,
+this option can be used to select the root filesystem (a.k.a. C<C:>
+drive or C</>) of the operating system that is to be converted.  The
+Windows Recovery Console, certain attached DVD drives, and bugs in
+libguestfs inspection heuristics, can make a guest look like a
+multi-boot operating system.
+
+The default in virt-v2v E<le> 0.7.1 was S<I<--root single>>, which
+causes virt-v2v to die if a multi-boot operating system is found.
+
+Since virt-v2v E<ge> 0.7.2 the default is now S<I<--root ask>>: If the
+VM is found to be multi-boot, then virt-v2v will stop and list the
+possible root filesystems and ask the user which to use.  This
+requires that virt-v2v is run interactively.
+
+S<I<--root first>> means to choose the first root device in the case
+of a multi-boot operating system.  Since this is a heuristic, it may
+sometimes choose the wrong one.
+
+You can also name a specific root device, eg. S<I<--root /dev/sda2>>
+would mean to use the second partition on the first hard drive.  If
+the named root device does not exist or was not detected as a root
+device, then virt-v2v will fail.
+
+Note that there is a bug in grub which prevents it from successfully
+booting a multiboot system if VirtIO is enabled.  Grub is only able to
+boot an operating system from the first VirtIO disk.  Specifically,
+C</boot> must be on the first VirtIO disk, and it cannot chainload an
+OS which is not in the first VirtIO disk.
+
+=item B<-v>
+
+=item B<--verbose>
+
+Enable verbose messages for debugging.
+
+=item B<-V>
+
+=item B<--version>
+
+Display version number and exit.
+
+=item B<-x>
+
+Enable tracing of libguestfs API calls.
+
+=back
+
+=head1 MACHINE READABLE OUTPUT
+
+The I<--machine-readable> option can be used to make the output more
+machine friendly, which is useful when calling virt-v2v from
+other programs, GUIs etc.
+
+There are two ways to use this option.
+
+Firstly use the option on its own to query the capabilities of the
+virt-v2v binary.  Typical output looks like this:
+
+ $ virt-v2v --machine-readable
+ virt-v2v
+ libguestfs-rewrite
+
+A list of features is printed, one per line, and the program exits
+with status 0.
+
+Secondly use the option in conjunction with other options to make the
+regular program output more machine friendly.
+
+At the moment this means:
+
+=over 4
+
+=item 1.
+
+Progress bar messages can be parsed from stdout by looking for this
+regular expression:
+
+ ^[0-9]+/[0-9]+$
+
+=item 2.
+
+The calling program should treat messages sent to stdout (except for
+progress bar messages) as status messages.  They can be logged and/or
+displayed to the user.
+
+=item 3.
+
+The calling program should treat messages sent to stderr as error
+messages.  In addition, virt-v2v exits with a non-zero status
+code if there was a fatal error.
+
+=back
+
+Virt-v2v E<le> 0.9.1 did not support the I<--machine-readable>
+option at all.  The option was added when virt-v2v was rewritten in 2014.
+
+=head1 ENVIRONMENT VARIABLES
+
+=over 4
+
+=item TMPDIR
+
+Location of the temporary directory used for the potentially large
+temporary overlay file.
+
+You should ensure there is enough free space in the worst case for a
+full copy of the source disk (I<virtual> size), or else set C<$TMPDIR>
+to point to another directory that has enough space.
+
+This defaults to C</tmp>.
+
+Note that if C<$TMPDIR> is a tmpfs (eg. if C</tmp> is on tmpfs, or if
+you use C<TMPDIR=/dev/shm>), tmpfs defaults to a maximum size of
+I<half> of physical RAM.  If virt-v2v exceeds this, it will hang.
+The solution is either to use a real disk, or to increase the maximum
+size of the tmpfs mountpoint, eg:
+
+ mount -o remount,size=10G /tmp
+
+=back
+
+For other environment variables, see L<guestfs(3)/ENVIRONMENT VARIABLES>.
+
+=head1 SEE ALSO
+
+L<virt-df(1)>,
+L<virt-filesystems(1)>,
+L<guestfs(3)>,
+L<guestfish(1)>,
+L<qemu-img(1)>,
+L<http://libguestfs.org/>.
+
+=head1 AUTHORS
+
+Richard W.M. Jones L<http://people.redhat.com/~rjones/>
+
+Matthew Booth
+
+=head1 COPYRIGHT
+
+Copyright (C) 2009-2014 Red Hat Inc.
diff --git a/v2v/xml-c.c b/v2v/xml-c.c
new file mode 100644
index 0000000..9b79c6b
--- /dev/null
+++ b/v2v/xml-c.c
@@ -0,0 +1,240 @@
+/* virt-v2v
+ * Copyright (C) 2009-2014 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.
+ */
+
+/* Mini interface to libxml2 for parsing libvirt XML. */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <caml/alloc.h>
+#include <caml/custom.h>
+#include <caml/fail.h>
+#include <caml/memory.h>
+#include <caml/mlvalues.h>
+
+#include <libxml/xpath.h>
+
+#include "guestfs.h"
+#include "guestfs-internal-frontend.h"
+
+/* xmlDocPtr type */
+#define Doc_val(v) (*((xmlDocPtr *)Data_custom_val(v)))
+
+static void
+doc_finalize (value docv)
+{
+  xmlDocPtr doc = Doc_val (docv);
+
+  if (doc)
+    xmlFreeDoc (doc);
+}
+
+static struct custom_operations doc_custom_operations = {
+  (char *) "doc_custom_operations",
+  doc_finalize,
+  custom_compare_default,
+  custom_hash_default,
+  custom_serialize_default,
+  custom_deserialize_default
+};
+
+/* xmlXPathContextPtr type */
+#define Xpathctx_val(v) (*((xmlXPathContextPtr *)Data_custom_val(v)))
+
+static void
+xpathctx_finalize (value xpathctxv)
+{
+  xmlXPathContextPtr xpathctx = Xpathctx_val (xpathctxv);
+
+  if (xpathctx)
+    xmlXPathFreeContext (xpathctx);
+}
+
+static struct custom_operations xpathctx_custom_operations = {
+  (char *) "xpathctx_custom_operations",
+  xpathctx_finalize,
+  custom_compare_default,
+  custom_hash_default,
+  custom_serialize_default,
+  custom_deserialize_default
+};
+
+/* xmlXPathObjectPtr type */
+#define Xpathobj_val(v) (*((xmlXPathObjectPtr *)Data_custom_val(v)))
+
+static void
+xpathobj_finalize (value xpathobjv)
+{
+  xmlXPathObjectPtr xpathobj = Xpathobj_val (xpathobjv);
+
+  if (xpathobj)
+    xmlXPathFreeObject (xpathobj);
+}
+
+static struct custom_operations xpathobj_custom_operations = {
+  (char *) "xpathobj_custom_operations",
+  xpathobj_finalize,
+  custom_compare_default,
+  custom_hash_default,
+  custom_serialize_default,
+  custom_deserialize_default
+};
+
+value
+v2v_xml_parse_memory (value xmlv)
+{
+  CAMLparam1 (xmlv);
+  CAMLlocal1 (docv);
+  xmlDocPtr doc;
+
+  doc = xmlParseMemory (String_val (xmlv), caml_string_length (xmlv));
+  if (doc == NULL)
+    caml_invalid_argument ("parse_memory: unable to parse XML from libvirt");
+
+  docv = caml_alloc_custom (&doc_custom_operations, sizeof (xmlDocPtr), 0, 1);
+  Doc_val (docv) = doc;
+
+  CAMLreturn (docv);
+}
+
+value
+v2v_xml_xpath_new_context (value docv)
+{
+  CAMLparam1 (docv);
+  CAMLlocal1 (xpathctxv);
+  xmlDocPtr doc;
+  xmlXPathContextPtr xpathctx;
+
+  doc = Doc_val (docv);
+  xpathctx = xmlXPathNewContext (doc);
+  if (xpathctx == NULL)
+    caml_invalid_argument ("xpath_new_context: unable to create xmlXPathNewContext");
+
+  xpathctxv = caml_alloc_custom (&xpathctx_custom_operations,
+                                 sizeof (xmlXPathContextPtr), 0, 1);
+  Xpathctx_val (xpathctxv) = xpathctx;
+
+  CAMLreturn (xpathctxv);
+}
+
+value
+v2v_xml_xpath_eval_expression (value xpathctxv, value exprv)
+{
+  CAMLparam2 (xpathctxv, exprv);
+  CAMLlocal1 (xpathobjv);
+  xmlXPathContextPtr xpathctx;
+  xmlXPathObjectPtr xpathobj;
+
+  xpathctx = Xpathctx_val (xpathctxv);
+  xpathobj = xmlXPathEvalExpression (BAD_CAST String_val (exprv), xpathctx);
+  if (xpathobj == NULL)
+    caml_invalid_argument ("xpath_eval_expression: unable to evaluate XPath expression");
+
+  xpathobjv = caml_alloc_custom (&xpathobj_custom_operations,
+                                 sizeof (xmlXPathObjectPtr), 0, 1);
+  Xpathobj_val (xpathobjv) = xpathobj;
+
+  CAMLreturn (xpathobjv);
+}
+
+value
+v2v_xml_xpathobj_nr_nodes (value xpathobjv)
+{
+  CAMLparam1 (xpathobjv);
+  xmlXPathObjectPtr xpathobj = Xpathobj_val (xpathobjv);
+
+  CAMLreturn (Val_int (xpathobj->nodesetval->nodeNr));
+}
+
+value
+v2v_xml_xpathobj_get_node_ptr (value xpathobjv, value iv)
+{
+  CAMLparam2 (xpathobjv, iv);
+  xmlXPathObjectPtr xpathobj = Xpathobj_val (xpathobjv);
+  int i = Int_val (iv);
+
+  /* Because xmlNodePtrs are owned by the document, we don't want to
+   * wrap this up with a finalizer, so just pass the pointer straight
+   * back to OCaml as a value.  OCaml will ignore it because it's
+   * outside the heap, and just pass it back to us when needed.  This
+   * relies on the xmlDocPtr not being freed, but we pair the node
+   * pointer with the doc in the OCaml layer so the GC will not free
+   * one without freeing the other.
+   */
+  CAMLreturn ((value) xpathobj->nodesetval->nodeTab[i]);
+}
+
+value
+v2v_xml_xpathctx_set_node_ptr (value xpathctxv, value nodev)
+{
+  CAMLparam2 (xpathctxv, nodev);
+  xmlXPathContextPtr xpathctx = Xpathctx_val (xpathctxv);
+  xmlNodePtr node = (xmlNodePtr) nodev;
+
+  xpathctx->node = node;
+
+  CAMLreturn (Val_unit);
+}
+
+value
+v2v_xml_node_ptr_name (value nodev)
+{
+  CAMLparam1 (nodev);
+  xmlNodePtr node = (xmlNodePtr) nodev;
+
+  switch (node->type) {
+  case XML_ATTRIBUTE_NODE:
+  case XML_ELEMENT_NODE:
+    CAMLreturn (caml_copy_string ((char *) node->name));
+
+  default:
+    caml_invalid_argument ("node_name: don't know how to get the name of this node");
+  }
+}
+
+value
+v2v_xml_node_ptr_as_string (value docv, value nodev)
+{
+  CAMLparam2 (docv, nodev);
+  xmlDocPtr doc = Doc_val (docv);
+  xmlNodePtr node = (xmlNodePtr) nodev;
+  CLEANUP_FREE char *str = NULL;
+
+  switch (node->type) {
+  case XML_TEXT_NODE:
+  case XML_COMMENT_NODE:
+  case XML_CDATA_SECTION_NODE:
+  case XML_PI_NODE:
+    CAMLreturn (caml_copy_string ((char *) node->content));
+
+  case XML_ATTRIBUTE_NODE:
+  case XML_ELEMENT_NODE:
+    str = (char *) xmlNodeListGetString (doc, node->children, 1);
+
+    if (str == NULL)
+      caml_invalid_argument ("node_as_string: xmlNodeListGetString cannot convert node to string");
+
+    CAMLreturn (caml_copy_string (str));
+
+  default:
+    caml_invalid_argument ("node_as_string: don't know how to convert this node to a string");
+  }
+}
diff --git a/v2v/xml.ml b/v2v/xml.ml
new file mode 100644
index 0000000..5cd75c1
--- /dev/null
+++ b/v2v/xml.ml
@@ -0,0 +1,50 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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.
+ *)
+
+(* Mini interface to libxml2 for parsing libvirt XML. *)
+
+type doc
+type node_ptr
+type xpathctx
+type xpathobj
+
+(* Since node is owned by doc, we have to make that explicit to the
+ * garbage collector.
+ *)
+type node = doc * node_ptr
+
+external parse_memory : string -> doc = "v2v_xml_parse_memory"
+external xpath_new_context : doc -> xpathctx = "v2v_xml_xpath_new_context"
+external xpath_eval_expression : xpathctx -> string -> xpathobj = "v2v_xml_xpath_eval_expression"
+
+external xpathobj_nr_nodes : xpathobj -> int = "v2v_xml_xpathobj_nr_nodes"
+external xpathobj_get_node_ptr : xpathobj -> int -> node_ptr = "v2v_xml_xpathobj_get_node_ptr"
+let xpathobj_node doc xpathobj i =
+  let n = xpathobj_get_node_ptr xpathobj i in
+  (doc, n)
+
+external xpathctx_set_node_ptr : xpathctx -> node_ptr -> unit = "v2v_xml_xpathctx_set_node_ptr"
+let xpathctx_set_current_context xpathctx (_, node) =
+  xpathctx_set_node_ptr xpathctx node
+
+external node_ptr_name : node_ptr -> string = "v2v_xml_node_ptr_name"
+let node_name (_, node) = node_ptr_name node
+
+external node_ptr_as_string : doc -> node_ptr -> string = "v2v_xml_node_ptr_as_string"
+let node_as_string (doc, node) =
+  node_ptr_as_string doc node
diff --git a/v2v/xml.mli b/v2v/xml.mli
new file mode 100644
index 0000000..c4363ad
--- /dev/null
+++ b/v2v/xml.mli
@@ -0,0 +1,57 @@
+(* virt-v2v
+ * Copyright (C) 2009-2014 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.
+ *)
+
+(** Mini interface to libxml2 for parsing libvirt XML. *)
+
+type doc                                (** xmlDocPtr *)
+type node                               (** xmlNodePtr *)
+type xpathctx                           (** xmlXPathContextPtr *)
+type xpathobj                           (** xmlXPathObjectPtr *)
+
+val parse_memory : string -> doc
+(** xmlParseMemory *)
+val xpath_new_context : doc -> xpathctx
+(** xmlXPathNewContext *)
+val xpath_eval_expression : xpathctx -> string -> xpathobj
+(** xmlXPathEvalExpression *)
+
+val xpathobj_nr_nodes : xpathobj -> int
+(** Get the number of nodes in the node set of the xmlXPathObjectPtr. *)
+val xpathobj_node : doc -> xpathobj -> int -> node
+(** Get the number of nodes in the node set of the xmlXPathObjectPtr. *)
+
+val xpathctx_set_current_context : xpathctx -> node -> unit
+(** Set the current context of an xmlXPathContextPtr to the node.
+    Basically the same as the following C code:
+
+    {v
+    xpathctx->node = node
+    v}
+
+    It means the next expression you evaluate within this context will
+    start at this node, when evaluating relative paths
+    (eg. [./@attr]).
+*)
+
+val node_name : node -> string
+(** Get the name of the node.  Note that only things like elements and
+    attributes have names.  Other types of nodes will return an
+    error. *)
+
+val node_as_string : node -> string
+(** Converter to turn a node into a string *)
-- 
1.8.5.3




More information about the Libguestfs mailing list