mrc0mmand / rpms / libguestfs

Forked from rpms/libguestfs 3 years ago
Clone

Blame SOURCES/0039-v2v-Add-o-rhv-upload-output-mode-RHBZ-1557273.patch

e9bfca
From d7a1c9c0bb074d0e5c1878a38a6b6315d33be89c Mon Sep 17 00:00:00 2001
e9bfca
From: "Richard W.M. Jones" <rjones@redhat.com>
e9bfca
Date: Mon, 12 Feb 2018 16:45:02 +0000
e9bfca
Subject: [PATCH] v2v: Add -o rhv-upload output mode (RHBZ#1557273).
e9bfca
MIME-Version: 1.0
e9bfca
Content-Type: text/plain; charset=UTF-8
e9bfca
Content-Transfer-Encoding: 8bit
e9bfca
e9bfca
This adds a new output mode to virt-v2v.  virt-v2v -o rhv-upload
e9bfca
streams images directly to an oVirt or RHV >= 4 Data Domain using the
e9bfca
oVirt SDK v4.  It is more efficient than -o rhv because it does not
e9bfca
need to go via the Export Storage Domain, and is possible for humans
e9bfca
to use unlike -o vdsm.
e9bfca
e9bfca
The implementation uses the Python SDK (‘ovirtsdk4’ module).  An
e9bfca
nbdkit Python 3 plugin translates NBD calls from qemu into HTTPS
e9bfca
requests to oVirt via the SDK.
e9bfca
e9bfca
(cherry picked from commit cc04573927cca97de60d544d37467e67c25867a7)
e9bfca
---
e9bfca
 .gitignore                                |   3 +
e9bfca
 TODO                                      |  27 ++
e9bfca
 v2v/Makefile.am                           |  30 +-
e9bfca
 v2v/cmdline.ml                            |  41 ++
e9bfca
 v2v/embed.sh                              |  48 +++
e9bfca
 v2v/output_rhv_upload.ml                  | 401 +++++++++++++++++++
e9bfca
 v2v/output_rhv_upload.mli                 |  33 ++
e9bfca
 v2v/output_rhv_upload_createvm_source.mli |  19 +
e9bfca
 v2v/output_rhv_upload_plugin_source.mli   |  19 +
e9bfca
 v2v/output_rhv_upload_precheck_source.mli |  19 +
e9bfca
 v2v/rhv-upload-createvm.py                |  86 +++++
e9bfca
 v2v/rhv-upload-plugin.py                  | 445 ++++++++++++++++++++++
e9bfca
 v2v/rhv-upload-precheck.py                |  73 ++++
e9bfca
 v2v/test-v2v-o-rhv-upload-oo-query.sh     |  38 ++
e9bfca
 v2v/test-v2v-python-syntax.sh             |  45 +++
e9bfca
 v2v/virt-v2v.pod                          | 138 ++++++-
e9bfca
 16 files changed, 1447 insertions(+), 18 deletions(-)
e9bfca
 create mode 100755 v2v/embed.sh
e9bfca
 create mode 100644 v2v/output_rhv_upload.ml
e9bfca
 create mode 100644 v2v/output_rhv_upload.mli
e9bfca
 create mode 100644 v2v/output_rhv_upload_createvm_source.mli
e9bfca
 create mode 100644 v2v/output_rhv_upload_plugin_source.mli
e9bfca
 create mode 100644 v2v/output_rhv_upload_precheck_source.mli
e9bfca
 create mode 100644 v2v/rhv-upload-createvm.py
e9bfca
 create mode 100644 v2v/rhv-upload-plugin.py
e9bfca
 create mode 100644 v2v/rhv-upload-precheck.py
e9bfca
 create mode 100755 v2v/test-v2v-o-rhv-upload-oo-query.sh
e9bfca
 create mode 100755 v2v/test-v2v-python-syntax.sh
e9bfca
e9bfca
diff --git a/.gitignore b/.gitignore
e9bfca
index e67013478..ecdac6df3 100644
e9bfca
--- a/.gitignore
e9bfca
+++ b/.gitignore
e9bfca
@@ -671,6 +671,9 @@ Makefile.in
e9bfca
 /utils/qemu-speed-test/qemu-speed-test
e9bfca
 /v2v/.depend
e9bfca
 /v2v/oUnit-*
e9bfca
+/v2v/output_rhv_upload_createvm_source.ml
e9bfca
+/v2v/output_rhv_upload_plugin_source.ml
e9bfca
+/v2v/output_rhv_upload_precheck_source.ml
e9bfca
 /v2v/real-*.d/
e9bfca
 /v2v/real-*.img
e9bfca
 /v2v/real-*.xml
e9bfca
diff --git a/TODO b/TODO
e9bfca
index 2e37ce67c..d196a3f6b 100644
e9bfca
--- a/TODO
e9bfca
+++ b/TODO
e9bfca
@@ -570,3 +570,30 @@ Subsecond handling in virt-diff, virt-ls
e9bfca
 
e9bfca
 Handle nanoseconds properly.  You should be able to specify them on
e9bfca
 the command line and display them.
e9bfca
+
e9bfca
+virt-v2v -o rhv-upload
e9bfca
+----------------------
e9bfca
+
e9bfca
+* Set or disable the ticket timeout.  The default is going to be
e9bfca
+  increased (from current 60 seconds), so maybe we won't have to
e9bfca
+  set it.  See also:
e9bfca
+  https://bugzilla.redhat.com/show_bug.cgi?id=1563278
e9bfca
+
e9bfca
+* qcow2 cannot be supported yet because there is not yet any
e9bfca
+  concept in imageio of read+write handles.
e9bfca
+  https://bugzilla.redhat.com/show_bug.cgi?id=1563299
e9bfca
+
e9bfca
+* preallocated cannot be supported yet because imageio doesn't
e9bfca
+  know how to zero the image efficiently, instead it runs an
e9bfca
+  fallocate process which writes to every block and that takes
e9bfca
+  many minutes.
e9bfca
+
e9bfca
+* Really check what insecure/rhv_cafile do and implement it correctly.
e9bfca
+
e9bfca
+* Measure and resolve performance problems.
e9bfca
+
e9bfca
+* Allocated image size is unknown for v2v uploads, but imageio needs
e9bfca
+  to know it.  We pass initial_size == provisioned_size == virtual size.
e9bfca
+  That can't be fixed from the v2v side.
e9bfca
+
e9bfca
+* There are unresolved issues about how to clean up disks on failure.
e9bfca
diff --git a/v2v/Makefile.am b/v2v/Makefile.am
e9bfca
index 5fcfeadba..affc00a8b 100644
e9bfca
--- a/v2v/Makefile.am
e9bfca
+++ b/v2v/Makefile.am
e9bfca
@@ -22,12 +22,19 @@ generator_built = \
e9bfca
 	uefi.mli
e9bfca
 
e9bfca
 BUILT_SOURCES = \
e9bfca
-	$(generator_built)
e9bfca
+	$(generator_built) \
e9bfca
+	output_rhv_upload_createvm_source.ml \
e9bfca
+	output_rhv_upload_plugin_source.ml \
e9bfca
+	output_rhv_upload_precheck_source.ml
e9bfca
 
e9bfca
 EXTRA_DIST = \
e9bfca
 	$(SOURCES_MLI) $(SOURCES_ML) $(SOURCES_C) \
e9bfca
 	copy_to_local.ml \
e9bfca
 	copy_to_local.mli \
e9bfca
+	embed-code.sh \
e9bfca
+	rhv-upload-createvm.py \
e9bfca
+	rhv-upload-plugin.py \
e9bfca
+	rhv-upload-precheck.py \
e9bfca
 	v2v_unit_tests.ml \
e9bfca
 	virt-v2v.pod \
e9bfca
 	virt-v2v-copy-to-local.pod
e9bfca
@@ -62,6 +69,10 @@ SOURCES_MLI = \
e9bfca
 	output_null.mli \
e9bfca
 	output_qemu.mli \
e9bfca
 	output_rhv.mli \
e9bfca
+	output_rhv_upload.mli \
e9bfca
+	output_rhv_upload_createvm_source.mli \
e9bfca
+	output_rhv_upload_plugin_source.mli \
e9bfca
+	output_rhv_upload_precheck_source.mli \
e9bfca
 	output_vdsm.mli \
e9bfca
 	parse_ova.mli \
e9bfca
 	parse_ovf_from_ova.mli \
e9bfca
@@ -116,6 +127,10 @@ SOURCES_ML = \
e9bfca
 	output_local.ml \
e9bfca
 	output_qemu.ml \
e9bfca
 	output_rhv.ml \
e9bfca
+	output_rhv_upload_createvm_source.ml \
e9bfca
+	output_rhv_upload_plugin_source.ml \
e9bfca
+	output_rhv_upload_precheck_source.ml \
e9bfca
+	output_rhv_upload.ml \
e9bfca
 	output_vdsm.ml \
e9bfca
 	inspect_source.ml \
e9bfca
 	target_bus_assignment.ml \
e9bfca
@@ -126,6 +141,15 @@ SOURCES_C = \
e9bfca
 	libvirt_utils-c.c \
e9bfca
 	qemuopts-c.c
e9bfca
 
e9bfca
+# These files are generated and contain rhv-upload-*.py embedded as an
e9bfca
+# OCaml string.
e9bfca
+output_rhv_upload_createvm_source.ml: rhv-upload-createvm.py
e9bfca
+	./embed.sh code $^ $@
e9bfca
+output_rhv_upload_plugin_source.ml: rhv-upload-plugin.py
e9bfca
+	./embed.sh code $^ $@
e9bfca
+output_rhv_upload_precheck_source.ml: rhv-upload-precheck.py
e9bfca
+	./embed.sh code $^ $@
e9bfca
+
e9bfca
 if HAVE_OCAML
e9bfca
 
e9bfca
 bin_PROGRAMS = virt-v2v virt-v2v-copy-to-local
e9bfca
@@ -295,6 +319,7 @@ TESTS_ENVIRONMENT = $(top_builddir)/run --test
e9bfca
 
e9bfca
 TESTS = \
e9bfca
 	test-v2v-docs.sh \
e9bfca
+	test-v2v-python-syntax.sh \
e9bfca
 	test-v2v-i-ova-bad-sha1.sh \
e9bfca
 	test-v2v-i-ova-bad-sha256.sh \
e9bfca
 	test-v2v-i-ova-formats.sh \
e9bfca
@@ -308,6 +333,7 @@ TESTS = \
e9bfca
 	test-v2v-i-ova-two-disks.sh \
e9bfca
 	test-v2v-i-vmx.sh \
e9bfca
 	test-v2v-it-vddk-io-query.sh \
e9bfca
+	test-v2v-o-rhv-upload-oo-query.sh \
e9bfca
 	test-v2v-o-vdsm-oo-query.sh \
e9bfca
 	test-v2v-bad-networks-and-bridges.sh
e9bfca
 
e9bfca
@@ -481,6 +507,7 @@ EXTRA_DIST += \
e9bfca
 	test-v2v-o-null.sh \
e9bfca
 	test-v2v-o-qemu.sh \
e9bfca
 	test-v2v-o-rhv.sh \
e9bfca
+	test-v2v-o-rhv-upload-oo-query.sh \
e9bfca
 	test-v2v-o-vdsm-oo-query.sh \
e9bfca
 	test-v2v-o-vdsm-options.sh \
e9bfca
 	test-v2v-oa-option.sh \
e9bfca
@@ -489,6 +516,7 @@ EXTRA_DIST += \
e9bfca
 	test-v2v-print-source.expected \
e9bfca
 	test-v2v-print-source.sh \
e9bfca
 	test-v2v-print-source.xml \
e9bfca
+	test-v2v-python-syntax.sh \
e9bfca
 	test-v2v-conversion-of.sh \
e9bfca
 	test-v2v-sound.sh \
e9bfca
 	test-v2v-sound.xml \
e9bfca
diff --git a/v2v/cmdline.ml b/v2v/cmdline.ml
e9bfca
index c9b859dd6..6321eb656 100644
e9bfca
--- a/v2v/cmdline.ml
e9bfca
+++ b/v2v/cmdline.ml
e9bfca
@@ -133,6 +133,8 @@ let parse_cmdline () =
e9bfca
     | "disk" | "local" -> output_mode := `Local
e9bfca
     | "null" -> output_mode := `Null
e9bfca
     | "ovirt" | "rhv" | "rhev" -> output_mode := `RHV
e9bfca
+    | "ovirt-upload" | "ovirt_upload" | "rhv-upload" | "rhv_upload" ->
e9bfca
+       output_mode := `RHV_Upload
e9bfca
     | "qemu" -> output_mode := `QEmu
e9bfca
     | "vdsm" -> output_mode := `VDSM
e9bfca
     | s ->
e9bfca
@@ -389,6 +391,16 @@ read the man page virt-v2v(1).
e9bfca
     | `Null -> no_options (); `Null
e9bfca
     | `RHV -> no_options (); `RHV
e9bfca
     | `QEmu -> no_options (); `QEmu
e9bfca
+    | `RHV_Upload ->
e9bfca
+       if is_query then (
e9bfca
+         Output_rhv_upload.print_output_options ();
e9bfca
+         exit 0
e9bfca
+       )
e9bfca
+       else (
e9bfca
+         let rhv_options =
e9bfca
+           Output_rhv_upload.parse_output_options output_options in
e9bfca
+         `RHV_Upload rhv_options
e9bfca
+       )
e9bfca
     | `VDSM ->
e9bfca
        if is_query then (
e9bfca
          Output_vdsm.print_output_options ();
e9bfca
@@ -571,6 +583,35 @@ read the man page virt-v2v(1).
e9bfca
       Output_rhv.output_rhv os output_alloc,
e9bfca
       output_format, output_alloc
e9bfca
 
e9bfca
+    | `RHV_Upload rhv_options ->
e9bfca
+      let output_conn =
e9bfca
+        match output_conn with
e9bfca
+        | None ->
e9bfca
+           error (f_"-o rhv-upload: use ‘-oc’ to point to the oVirt or RHV server REST API URL, which is usually https://servername/ovirt-engine/api")
e9bfca
+        | Some oc -> oc in
e9bfca
+      (* Output format / sparse must currently be raw+sparse.  We can
e9bfca
+       * change this in future.  See TODO file for details. XXX
e9bfca
+       *)
e9bfca
+      if output_alloc <> Sparse || output_format <> Some "raw" then
e9bfca
+        error (f_"-o rhv-upload: currently you must use ‘-of raw’ and you cannot use ‘-oa preallocated’ with this output mode.  These restrictions will be loosened in a future version.");
e9bfca
+      (* In theory we could make the password optional in future. *)
e9bfca
+      let output_password =
e9bfca
+        match output_password with
e9bfca
+        | None ->
e9bfca
+           error (f_"-o rhv-upload: output password file was not specified, use ‘-op’ to point to a file which contains the password used to connect to the oVirt or RHV server")
e9bfca
+        | Some op -> op in
e9bfca
+      let os =
e9bfca
+        match output_storage with
e9bfca
+        | None ->
e9bfca
+           error (f_"-o rhv-upload: output storage was not specified, use ‘-os’");
e9bfca
+        | Some os -> os in
e9bfca
+      if qemu_boot then
e9bfca
+        error_option_cannot_be_used_in_output_mode "rhv-upload" "--qemu-boot";
e9bfca
+      Output_rhv_upload.output_rhv_upload output_alloc output_conn
e9bfca
+                                          output_password os
e9bfca
+                                          rhv_options,
e9bfca
+      output_format, output_alloc
e9bfca
+
e9bfca
     | `VDSM vdsm_options ->
e9bfca
       if output_password <> None then
e9bfca
         error_option_cannot_be_used_in_output_mode "vdsm" "-op";
e9bfca
diff --git a/v2v/embed.sh b/v2v/embed.sh
e9bfca
new file mode 100755
e9bfca
index 000000000..363d7e2b0
e9bfca
--- /dev/null
e9bfca
+++ b/v2v/embed.sh
e9bfca
@@ -0,0 +1,48 @@
e9bfca
+#!/bin/bash -
e9bfca
+# Embed code or other content into an OCaml file.
e9bfca
+# Copyright (C) 2018 Red Hat Inc.
e9bfca
+#
e9bfca
+# This program is free software; you can redistribute it and/or modify
e9bfca
+# it under the terms of the GNU General Public License as published by
e9bfca
+# the Free Software Foundation; either version 2 of the License, or
e9bfca
+# (at your option) any later version.
e9bfca
+#
e9bfca
+# This program is distributed in the hope that it will be useful,
e9bfca
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
e9bfca
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
e9bfca
+# GNU General Public License for more details.
e9bfca
+#
e9bfca
+# You should have received a copy of the GNU General Public License
e9bfca
+# along with this program; if not, write to the Free Software
e9bfca
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
e9bfca
+
e9bfca
+# Embed code or other content into an OCaml file.
e9bfca
+#
e9bfca
+# It is embedded into a string.  As OCaml string literals have virtually
e9bfca
+# no restrictions on length or content we only have to escape double
e9bfca
+# quotes for backslash characters.
e9bfca
+
e9bfca
+if [ $# -ne 3 ]; then
e9bfca
+    echo "embed.sh identifier input output"
e9bfca
+    exit 1
e9bfca
+fi
e9bfca
+
e9bfca
+set -e
e9bfca
+set -u
e9bfca
+
e9bfca
+ident="$1"
e9bfca
+input="$2"
e9bfca
+output="$3"
e9bfca
+
e9bfca
+rm -f "$output" "$output"-t
e9bfca
+
e9bfca
+exec >"$output"-t
e9bfca
+
e9bfca
+echo "(* Generated by embed.sh from $input *)"
e9bfca
+echo
e9bfca
+echo let "$ident" = '"'
e9bfca
+sed -e 's/\(["\]\)/\\\1/g' < "$input"
e9bfca
+echo '"'
e9bfca
+
e9bfca
+chmod -w "$output"-t
e9bfca
+mv "$output"-t "$output"
e9bfca
diff --git a/v2v/output_rhv_upload.ml b/v2v/output_rhv_upload.ml
e9bfca
new file mode 100644
e9bfca
index 000000000..129461242
e9bfca
--- /dev/null
e9bfca
+++ b/v2v/output_rhv_upload.ml
e9bfca
@@ -0,0 +1,401 @@
e9bfca
+(* virt-v2v
e9bfca
+ * Copyright (C) 2009-2018 Red Hat Inc.
e9bfca
+ *
e9bfca
+ * This program is free software; you can redistribute it and/or modify
e9bfca
+ * it under the terms of the GNU General Public License as published by
e9bfca
+ * the Free Software Foundation; either version 2 of the License, or
e9bfca
+ * (at your option) any later version.
e9bfca
+ *
e9bfca
+ * This program is distributed in the hope that it will be useful,
e9bfca
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
e9bfca
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
e9bfca
+ * GNU General Public License for more details.
e9bfca
+ *
e9bfca
+ * You should have received a copy of the GNU General Public License along
e9bfca
+ * with this program; if not, write to the Free Software Foundation, Inc.,
e9bfca
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
e9bfca
+ *)
e9bfca
+
e9bfca
+open Printf
e9bfca
+open Unix
e9bfca
+
e9bfca
+open Std_utils
e9bfca
+open Tools_utils
e9bfca
+open Unix_utils
e9bfca
+open Common_gettext.Gettext
e9bfca
+
e9bfca
+open Types
e9bfca
+open Utils
e9bfca
+
e9bfca
+type rhv_options = {
e9bfca
+  rhv_cafile : string;
e9bfca
+  rhv_cluster : string option;
e9bfca
+  rhv_direct : bool;
e9bfca
+  rhv_verifypeer : bool;
e9bfca
+}
e9bfca
+
e9bfca
+let print_output_options () =
e9bfca
+  printf (f_"Output options (-oo) which can be used with -o rhv-upload:
e9bfca
+
e9bfca
+  -oo rhv-cafile=CA.PEM           Set ‘ca.pem’ certificate bundle filename.
e9bfca
+  -oo rhv-cluster=CLUSTERNAME     Set RHV cluster name.
e9bfca
+  -oo rhv-direct[=true|false]     Use direct transfer mode (default: false).
e9bfca
+  -oo rhv-verifypeer[=true|false] Verify server identity (default: false).
e9bfca
+")
e9bfca
+
e9bfca
+let parse_output_options options =
e9bfca
+  let rhv_cafile = ref None in
e9bfca
+  let rhv_cluster = ref None in
e9bfca
+  let rhv_direct = ref false in
e9bfca
+  let rhv_verifypeer = ref false in
e9bfca
+
e9bfca
+  List.iter (
e9bfca
+    function
e9bfca
+    | "rhv-cafile", v ->
e9bfca
+       if !rhv_cafile <> None then
e9bfca
+         error (f_"-o rhv-upload: -oo rhv-cafile set twice");
e9bfca
+       rhv_cafile := Some v
e9bfca
+    | "rhv-cluster", v ->
e9bfca
+       if !rhv_cluster <> None then
e9bfca
+         error (f_"-o rhv-upload: -oo rhv-cluster set twice");
e9bfca
+       rhv_cluster := Some v
e9bfca
+    | "rhv-direct", "" -> rhv_direct := true
e9bfca
+    | "rhv-direct", v -> rhv_direct := bool_of_string v
e9bfca
+    | "rhv-verifypeer", "" -> rhv_verifypeer := true
e9bfca
+    | "rhv-verifypeer", v -> rhv_verifypeer := bool_of_string v
e9bfca
+    | k, _ ->
e9bfca
+       error (f_"-o rhv-upload: unknown output option ‘-oo %s’") k
e9bfca
+  ) options;
e9bfca
+
e9bfca
+  let rhv_cafile =
e9bfca
+    match !rhv_cafile with
e9bfca
+    | Some s -> s
e9bfca
+    | None ->
e9bfca
+       error (f_"-o rhv-upload: must use ‘-oo rhv-cafile’ to supply the path to the oVirt or RHV user’s ‘ca.pem’ file") in
e9bfca
+  let rhv_cluster = !rhv_cluster in
e9bfca
+  let rhv_direct = !rhv_direct in
e9bfca
+  let rhv_verifypeer = !rhv_verifypeer in
e9bfca
+
e9bfca
+  { rhv_cafile; rhv_cluster; rhv_direct; rhv_verifypeer }
e9bfca
+
e9bfca
+let python3 = "python3" (* Defined by PEP 394 *)
e9bfca
+let pidfile_timeout = 30
e9bfca
+let finalization_timeout = 5*60
e9bfca
+
e9bfca
+class output_rhv_upload output_alloc output_conn
e9bfca
+                        output_password output_storage
e9bfca
+                        rhv_options =
e9bfca
+  (* Create a temporary directory which will be deleted on exit. *)
e9bfca
+  let tmpdir =
e9bfca
+    let base_dir = (open_guestfs ())#get_cachedir () in
e9bfca
+    let t = Mkdtemp.temp_dir ~base_dir "rhvupload." in
e9bfca
+    rmdir_on_exit t;
e9bfca
+    t in
e9bfca
+
e9bfca
+  let diskid_file_of_id id = tmpdir // sprintf "diskid.%d" id in
e9bfca
+
e9bfca
+  (* Write the Python precheck, plugin and create VM to a temporary file. *)
e9bfca
+  let precheck =
e9bfca
+    let precheck = tmpdir // "rhv-upload-precheck.py" in
e9bfca
+    with_open_out
e9bfca
+      precheck
e9bfca
+      (fun chan -> output_string chan Output_rhv_upload_precheck_source.code);
e9bfca
+    precheck in
e9bfca
+  let plugin =
e9bfca
+    let plugin = tmpdir // "rhv-upload-plugin.py" in
e9bfca
+    with_open_out
e9bfca
+      plugin
e9bfca
+      (fun chan -> output_string chan Output_rhv_upload_plugin_source.code);
e9bfca
+    plugin in
e9bfca
+  let createvm =
e9bfca
+    let createvm = tmpdir // "rhv-upload-createvm.py" in
e9bfca
+    with_open_out
e9bfca
+      createvm
e9bfca
+      (fun chan -> output_string chan Output_rhv_upload_createvm_source.code);
e9bfca
+    createvm in
e9bfca
+
e9bfca
+  (* Is SELinux enabled and enforcing on the host? *)
e9bfca
+  let have_selinux =
e9bfca
+    0 = Sys.command "getenforce 2>/dev/null | grep -isq Enforcing" in
e9bfca
+
e9bfca
+  (* Check that the Python binary is available. *)
e9bfca
+  let error_unless_python_binary_on_path () =
e9bfca
+    try ignore (which python3)
e9bfca
+    with Executable_not_found _ ->
e9bfca
+      error (f_"no python binary called ‘%s’ can be found on the $PATH")
e9bfca
+            python3
e9bfca
+  in
e9bfca
+
e9bfca
+  (* Check that nbdkit is available and new enough. *)
e9bfca
+  let error_unless_nbdkit_working () =
e9bfca
+    if 0 <> Sys.command "nbdkit --version >/dev/null" then
e9bfca
+      error (f_"nbdkit is not installed or not working.  It is required to use ‘-o rhv-upload’.  See \"OUTPUT TO RHV\" in the virt-v2v(1) manual.");
e9bfca
+
e9bfca
+    (* Check it's a new enough version.  The latest features we
e9bfca
+     * require are ‘--exit-with-parent’ and ‘--selinux-label’, both
e9bfca
+     * added in 1.1.14.  (We use 1.1.16 as the minimum here because
e9bfca
+     * it also adds the selinux=yes|no flag in --dump-config).
e9bfca
+     *)
e9bfca
+    let lines = external_command "nbdkit --help" in
e9bfca
+    let lines = String.concat " " lines in
e9bfca
+    if String.find lines "exit-with-parent" == -1 ||
e9bfca
+       String.find lines "selinux-label" == -1 then
e9bfca
+      error (f_"nbdkit is not new enough, you need to upgrade to nbdkit ≥ 1.1.16")
e9bfca
+  in
e9bfca
+
e9bfca
+  (* Check that the python3 plugin is installed and working
e9bfca
+   * and can load the plugin script.
e9bfca
+   *)
e9bfca
+  let error_unless_nbdkit_python3_working () =
e9bfca
+    let cmd = sprintf "nbdkit %s %s --dump-plugin >/dev/null"
e9bfca
+                      python3 (quote plugin) in
e9bfca
+    if Sys.command cmd <> 0 then
e9bfca
+      error (f_"nbdkit Python 3 plugin is not installed or not working.  It is required if you want to use ‘-o rhv-upload’.
e9bfca
+
e9bfca
+See also \"OUTPUT TO RHV\" in the virt-v2v(1) manual.")
e9bfca
+  in
e9bfca
+
e9bfca
+  (* Check that nbdkit was compiled with SELinux support (for the
e9bfca
+   * --selinux-label option).
e9bfca
+   *)
e9bfca
+  let error_unless_nbdkit_compiled_with_selinux () =
e9bfca
+    let lines = external_command "nbdkit --dump-config" in
e9bfca
+    (* In nbdkit <= 1.1.15 the selinux attribute was not present
e9bfca
+     * at all in --dump-config output so there was no way to tell.
e9bfca
+     * Ignore this case because there will be an error later when
e9bfca
+     * we try to use the --selinux-label parameter.
e9bfca
+     *)
e9bfca
+    if List.mem "selinux=no" (List.map String.trim lines) then
e9bfca
+      error (f_"nbdkit was compiled without SELinux support.  You will have to recompile nbdkit with libselinux-devel installed, or else set SELinux to Permissive mode while doing the conversion.")
e9bfca
+  in
e9bfca
+
e9bfca
+  (* JSON parameters which are invariant between disks. *)
e9bfca
+  let json_params = [
e9bfca
+    "verbose", JSON.Bool (verbose ());
e9bfca
+
e9bfca
+    "output_conn", JSON.String output_conn;
e9bfca
+    "output_password", JSON.String output_password;
e9bfca
+    "output_storage", JSON.String output_storage;
e9bfca
+    "output_sparse", JSON.Bool (match output_alloc with
e9bfca
+                                | Sparse -> true
e9bfca
+                                | Preallocated -> false);
e9bfca
+    "rhv_cafile", JSON.String rhv_options.rhv_cafile;
e9bfca
+    "rhv_cluster",
e9bfca
+      JSON.String (Option.default "Default" rhv_options.rhv_cluster);
e9bfca
+    "rhv_direct", JSON.Bool rhv_options.rhv_direct;
e9bfca
+
e9bfca
+    (* The 'Insecure' flag seems to be a number with various possible
e9bfca
+     * meanings, however we just set it to True/False.
e9bfca
+     *
e9bfca
+     * https://github.com/oVirt/ovirt-engine-sdk/blob/19aa7070b80e60a4cfd910448287aecf9083acbe/sdk/lib/ovirtsdk4/__init__.py#L395
e9bfca
+     *)
e9bfca
+    "insecure", JSON.Bool (not rhv_options.rhv_verifypeer);
e9bfca
+  ] in
e9bfca
+
e9bfca
+  (* nbdkit command line args which are invariant between disks. *)
e9bfca
+  let nbdkit_args =
e9bfca
+    let args = [
e9bfca
+      "nbdkit";
e9bfca
+
e9bfca
+      "--foreground";           (* run in foreground *)
e9bfca
+      "--exit-with-parent";     (* exit when virt-v2v exits *)
e9bfca
+      "--newstyle";             (* use newstyle NBD protocol *)
e9bfca
+      "--exportname"; "/";
e9bfca
+
e9bfca
+      "python3";                (* use the nbdkit Python 3 plugin *)
e9bfca
+      plugin;                   (* Python plugin script *)
e9bfca
+    ] in
e9bfca
+    let args = if verbose () then args @ ["--verbose"] else args in
e9bfca
+    let args =
e9bfca
+      (* label the socket so qemu can open it *)
e9bfca
+      if have_selinux then
e9bfca
+        args @ ["--selinux-label"; "system_u:object_r:svirt_t:s0"]
e9bfca
+      else args in
e9bfca
+    args in
e9bfca
+
e9bfca
+object
e9bfca
+  inherit output
e9bfca
+
e9bfca
+  method precheck () =
e9bfca
+    error_unless_python_binary_on_path ();
e9bfca
+    error_unless_nbdkit_working ();
e9bfca
+    error_unless_nbdkit_python3_working ();
e9bfca
+    if have_selinux then
e9bfca
+      error_unless_nbdkit_compiled_with_selinux ()
e9bfca
+
e9bfca
+  method as_options =
e9bfca
+    "-o rhv-upload" ^
e9bfca
+    (match output_alloc with
e9bfca
+     | Sparse -> "" (* default, don't need to print it *)
e9bfca
+     | Preallocated -> " -oa preallocated") ^
e9bfca
+    sprintf " -oc %s -op %s -os %s"
e9bfca
+            output_conn output_password output_storage
e9bfca
+
e9bfca
+  method supported_firmware = [ TargetBIOS ]
e9bfca
+
e9bfca
+  method prepare_targets source targets =
e9bfca
+    let output_name = source.s_name in
e9bfca
+    let json_params =
e9bfca
+      ("output_name", JSON.String output_name) :: json_params in
e9bfca
+
e9bfca
+    (* Python code prechecks.  These can't run in #precheck because
e9bfca
+     * we need to know the name of the virtual machine.
e9bfca
+     *)
e9bfca
+    let json_param_file = tmpdir // "params.json" in
e9bfca
+    with_open_out
e9bfca
+      json_param_file
e9bfca
+      (fun chan -> output_string chan (JSON.string_of_doc json_params));
e9bfca
+    if run_command [ python3; precheck; json_param_file ] <> 0 then
e9bfca
+      error (f_"failed server prechecks, see earlier errors");
e9bfca
+
e9bfca
+    (* Create an nbdkit instance for each disk and set the
e9bfca
+     * target URI to point to the NBD socket.
e9bfca
+     *)
e9bfca
+    List.map (
e9bfca
+      fun t ->
e9bfca
+        let id = t.target_overlay.ov_source.s_disk_id in
e9bfca
+        let disk_name = sprintf "%s-%03d" output_name id in
e9bfca
+        let json_params =
e9bfca
+          ("disk_name", JSON.String disk_name) :: json_params in
e9bfca
+
e9bfca
+        let disk_format =
e9bfca
+          match t.target_format with
e9bfca
+          | ("raw" | "qcow2") as fmt -> fmt
e9bfca
+          | _ ->
e9bfca
+             error (f_"rhv-upload: -of %s: Only output format ‘raw’ or ‘qcow2’ is supported.  If the input is in a different format then force one of these output formats by adding either ‘-of raw’ or ‘-of qcow2’ on the command line.")
e9bfca
+                   t.target_format in
e9bfca
+        let json_params =
e9bfca
+          ("disk_format", JSON.String disk_format) :: json_params in
e9bfca
+
e9bfca
+        let disk_size = t.target_overlay.ov_virtual_size in
e9bfca
+        let json_params =
e9bfca
+          ("disk_size", JSON.Int64 disk_size) :: json_params in
e9bfca
+
e9bfca
+        (* Ask the plugin to write the disk ID to a special file. *)
e9bfca
+        let diskid_file = diskid_file_of_id id in
e9bfca
+        let json_params =
e9bfca
+          ("diskid_file", JSON.String diskid_file) :: json_params in
e9bfca
+
e9bfca
+        (* Write the JSON parameters to a file. *)
e9bfca
+        let json_param_file = tmpdir // sprintf "params%d.json" id in
e9bfca
+        with_open_out
e9bfca
+          json_param_file
e9bfca
+          (fun chan -> output_string chan (JSON.string_of_doc json_params));
e9bfca
+
e9bfca
+        let sock = tmpdir // sprintf "nbdkit%d.sock" id in
e9bfca
+        let pidfile = tmpdir // sprintf "nbdkit%d.pid" id in
e9bfca
+
e9bfca
+        (* Add common arguments to per-target arguments. *)
e9bfca
+        let args =
e9bfca
+          nbdkit_args @ [ "--pidfile"; pidfile;
e9bfca
+                          "--unix"; sock;
e9bfca
+                          sprintf "params=%s" json_param_file ] in
e9bfca
+
e9bfca
+        (* Print the full command we are about to run when debugging. *)
e9bfca
+        if verbose () then (
e9bfca
+          eprintf "running nbdkit:\n";
e9bfca
+          List.iter (fun arg -> eprintf " %s" (quote arg)) args;
e9bfca
+          prerr_newline ()
e9bfca
+        );
e9bfca
+
e9bfca
+        (* Start an nbdkit instance in the background.  By using
e9bfca
+         * --exit-with-parent we don't have to worry about clean-up.
e9bfca
+         *)
e9bfca
+        let args = Array.of_list args in
e9bfca
+        let pid = fork () in
e9bfca
+        if pid = 0 then (
e9bfca
+          (* Child process (nbdkit). *)
e9bfca
+          execvp "nbdkit" args
e9bfca
+        );
e9bfca
+
e9bfca
+        (* Wait for the pidfile to appear so we know that nbdkit
e9bfca
+         * is listening for requests.
e9bfca
+         *)
e9bfca
+        if not (wait_for_file pidfile pidfile_timeout) then (
e9bfca
+          if verbose () then
e9bfca
+            error (f_"nbdkit did not start up.  See previous debugging messages for problems.")
e9bfca
+          else
e9bfca
+            error (f_"nbdkit did not start up.  There may be errors printed by nbdkit above.
e9bfca
+
e9bfca
+If the messages above are not sufficient to diagnose the problem then add the ‘virt-v2v -v -x’ options and examine the debugging output carefully.")
e9bfca
+        );
e9bfca
+
e9bfca
+        if have_selinux then (
e9bfca
+          (* Note that Unix domain sockets have both a file label and
e9bfca
+           * a socket/process label.  Using --selinux-label above
e9bfca
+           * only set the socket label, but we must also set the file
e9bfca
+           * label.
e9bfca
+           *)
e9bfca
+          ignore (
e9bfca
+              run_command ["chcon"; "system_u:object_r:svirt_image_t:s0";
e9bfca
+                           sock]
e9bfca
+          );
e9bfca
+        );
e9bfca
+        (* ... and the regular Unix permissions, in case qemu is
e9bfca
+         * running as another user.
e9bfca
+         *)
e9bfca
+        chmod sock 0o777;
e9bfca
+
e9bfca
+        (* Tell ‘qemu-img convert’ to write to the nbd socket which is
e9bfca
+         * connected to nbdkit.
e9bfca
+         *)
e9bfca
+        let json_params = [
e9bfca
+          "file.driver", JSON.String "nbd";
e9bfca
+          "file.path", JSON.String sock;
e9bfca
+          "file.export", JSON.String "/";
e9bfca
+        ] in
e9bfca
+        let target_file =
e9bfca
+          TargetURI ("json:" ^ JSON.string_of_doc json_params) in
e9bfca
+        { t with target_file }
e9bfca
+    ) targets
e9bfca
+
e9bfca
+  method create_metadata source targets _ guestcaps inspect target_firmware =
e9bfca
+    (* Get the UUIDs of each disk image.  These files are written
e9bfca
+     * out by the nbdkit plugins on successful finalization of the
e9bfca
+     * transfer.
e9bfca
+     *)
e9bfca
+    let nr_disks = List.length targets in
e9bfca
+    let image_uuids =
e9bfca
+      List.map (
e9bfca
+        fun t ->
e9bfca
+          let id = t.target_overlay.ov_source.s_disk_id in
e9bfca
+          let diskid_file = diskid_file_of_id id in
e9bfca
+          if not (wait_for_file diskid_file finalization_timeout) then
e9bfca
+            error (f_"transfer of disk %d/%d failed, see earlier error messages")
e9bfca
+                  (id+1) nr_disks;
e9bfca
+          let diskid = read_whole_file diskid_file in
e9bfca
+          diskid
e9bfca
+      ) targets in
e9bfca
+
e9bfca
+    (* We don't have the storage domain UUID, but instead we write
e9bfca
+     * in a magic value which the Python code (which can get it)
e9bfca
+     * will substitute.
e9bfca
+     *)
e9bfca
+    let sd_uuid = "@SD_UUID@" in
e9bfca
+
e9bfca
+    (* The volume and VM UUIDs are made up. *)
e9bfca
+    let vol_uuids = List.map (fun _ -> uuidgen ()) targets
e9bfca
+    and vm_uuid = uuidgen () in
e9bfca
+
e9bfca
+    (* Create the metadata. *)
e9bfca
+    let ovf =
e9bfca
+      Create_ovf.create_ovf source targets guestcaps inspect
e9bfca
+                            output_alloc
e9bfca
+                            sd_uuid image_uuids vol_uuids vm_uuid
e9bfca
+                            OVirt in
e9bfca
+    let ovf = DOM.doc_to_string ovf in
e9bfca
+
e9bfca
+    let json_param_file = tmpdir // "params.json" in
e9bfca
+    with_open_out
e9bfca
+      json_param_file
e9bfca
+      (fun chan -> output_string chan (JSON.string_of_doc json_params));
e9bfca
+
e9bfca
+    let ovf_file = tmpdir // "vm.ovf" in
e9bfca
+    with_open_out ovf_file (fun chan -> output_string chan ovf);
e9bfca
+    if run_command [ python3; createvm; json_param_file; ovf_file ] <> 0 then
e9bfca
+      error (f_"failed to create virtual machine, see earlier errors")
e9bfca
+
e9bfca
+end
e9bfca
+
e9bfca
+let output_rhv_upload = new output_rhv_upload
e9bfca
+let () = Modules_list.register_output_module "rhv-upload"
e9bfca
diff --git a/v2v/output_rhv_upload.mli b/v2v/output_rhv_upload.mli
e9bfca
new file mode 100644
e9bfca
index 000000000..f6cd69a61
e9bfca
--- /dev/null
e9bfca
+++ b/v2v/output_rhv_upload.mli
e9bfca
@@ -0,0 +1,33 @@
e9bfca
+(* virt-v2v
e9bfca
+ * Copyright (C) 2009-2018 Red Hat Inc.
e9bfca
+ *
e9bfca
+ * This program is free software; you can redistribute it and/or modify
e9bfca
+ * it under the terms of the GNU General Public License as published by
e9bfca
+ * the Free Software Foundation; either version 2 of the License, or
e9bfca
+ * (at your option) any later version.
e9bfca
+ *
e9bfca
+ * This program is distributed in the hope that it will be useful,
e9bfca
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
e9bfca
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
e9bfca
+ * GNU General Public License for more details.
e9bfca
+ *
e9bfca
+ * You should have received a copy of the GNU General Public License along
e9bfca
+ * with this program; if not, write to the Free Software Foundation, Inc.,
e9bfca
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
e9bfca
+ *)
e9bfca
+
e9bfca
+(** [-o rhv-upload] target. *)
e9bfca
+
e9bfca
+type rhv_options
e9bfca
+(** Miscellaneous extra command line parameters used by rhv-upload. *)
e9bfca
+
e9bfca
+val print_output_options : unit -> unit
e9bfca
+val parse_output_options : (string * string) list -> rhv_options
e9bfca
+(** Print and parse rhv-upload -oo options. *)
e9bfca
+
e9bfca
+val output_rhv_upload : Types.output_allocation -> string -> string ->
e9bfca
+                        string -> rhv_options -> Types.output
e9bfca
+(** [output_rhv_upload output_alloc output_conn output_password output_storage
e9bfca
+     rhv_options]
e9bfca
+    creates and returns a new {!Types.output} object specialized for writing
e9bfca
+    output to oVirt or RHV directly via RHV APIs. *)
e9bfca
diff --git a/v2v/output_rhv_upload_createvm_source.mli b/v2v/output_rhv_upload_createvm_source.mli
e9bfca
new file mode 100644
e9bfca
index 000000000..c1bafa15b
e9bfca
--- /dev/null
e9bfca
+++ b/v2v/output_rhv_upload_createvm_source.mli
e9bfca
@@ -0,0 +1,19 @@
e9bfca
+(* virt-v2v
e9bfca
+ * Copyright (C) 2018 Red Hat Inc.
e9bfca
+ *
e9bfca
+ * This program is free software; you can redistribute it and/or modify
e9bfca
+ * it under the terms of the GNU General Public License as published by
e9bfca
+ * the Free Software Foundation; either version 2 of the License, or
e9bfca
+ * (at your option) any later version.
e9bfca
+ *
e9bfca
+ * This program is distributed in the hope that it will be useful,
e9bfca
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
e9bfca
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
e9bfca
+ * GNU General Public License for more details.
e9bfca
+ *
e9bfca
+ * You should have received a copy of the GNU General Public License along
e9bfca
+ * with this program; if not, write to the Free Software Foundation, Inc.,
e9bfca
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
e9bfca
+ *)
e9bfca
+
e9bfca
+val code : string
e9bfca
diff --git a/v2v/output_rhv_upload_plugin_source.mli b/v2v/output_rhv_upload_plugin_source.mli
e9bfca
new file mode 100644
e9bfca
index 000000000..c1bafa15b
e9bfca
--- /dev/null
e9bfca
+++ b/v2v/output_rhv_upload_plugin_source.mli
e9bfca
@@ -0,0 +1,19 @@
e9bfca
+(* virt-v2v
e9bfca
+ * Copyright (C) 2018 Red Hat Inc.
e9bfca
+ *
e9bfca
+ * This program is free software; you can redistribute it and/or modify
e9bfca
+ * it under the terms of the GNU General Public License as published by
e9bfca
+ * the Free Software Foundation; either version 2 of the License, or
e9bfca
+ * (at your option) any later version.
e9bfca
+ *
e9bfca
+ * This program is distributed in the hope that it will be useful,
e9bfca
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
e9bfca
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
e9bfca
+ * GNU General Public License for more details.
e9bfca
+ *
e9bfca
+ * You should have received a copy of the GNU General Public License along
e9bfca
+ * with this program; if not, write to the Free Software Foundation, Inc.,
e9bfca
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
e9bfca
+ *)
e9bfca
+
e9bfca
+val code : string
e9bfca
diff --git a/v2v/output_rhv_upload_precheck_source.mli b/v2v/output_rhv_upload_precheck_source.mli
e9bfca
new file mode 100644
e9bfca
index 000000000..c1bafa15b
e9bfca
--- /dev/null
e9bfca
+++ b/v2v/output_rhv_upload_precheck_source.mli
e9bfca
@@ -0,0 +1,19 @@
e9bfca
+(* virt-v2v
e9bfca
+ * Copyright (C) 2018 Red Hat Inc.
e9bfca
+ *
e9bfca
+ * This program is free software; you can redistribute it and/or modify
e9bfca
+ * it under the terms of the GNU General Public License as published by
e9bfca
+ * the Free Software Foundation; either version 2 of the License, or
e9bfca
+ * (at your option) any later version.
e9bfca
+ *
e9bfca
+ * This program is distributed in the hope that it will be useful,
e9bfca
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
e9bfca
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
e9bfca
+ * GNU General Public License for more details.
e9bfca
+ *
e9bfca
+ * You should have received a copy of the GNU General Public License along
e9bfca
+ * with this program; if not, write to the Free Software Foundation, Inc.,
e9bfca
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
e9bfca
+ *)
e9bfca
+
e9bfca
+val code : string
e9bfca
diff --git a/v2v/rhv-upload-createvm.py b/v2v/rhv-upload-createvm.py
e9bfca
new file mode 100644
e9bfca
index 000000000..a34627ec8
e9bfca
--- /dev/null
e9bfca
+++ b/v2v/rhv-upload-createvm.py
e9bfca
@@ -0,0 +1,86 @@
e9bfca
+# -*- python -*-
e9bfca
+# oVirt or RHV upload create VM used by ‘virt-v2v -o rhv-upload’
e9bfca
+# Copyright (C) 2018 Red Hat Inc.
e9bfca
+#
e9bfca
+# This program is free software; you can redistribute it and/or modify
e9bfca
+# it under the terms of the GNU General Public License as published by
e9bfca
+# the Free Software Foundation; either version 2 of the License, or
e9bfca
+# (at your option) any later version.
e9bfca
+#
e9bfca
+# This program is distributed in the hope that it will be useful,
e9bfca
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
e9bfca
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
e9bfca
+# GNU General Public License for more details.
e9bfca
+#
e9bfca
+# You should have received a copy of the GNU General Public License along
e9bfca
+# with this program; if not, write to the Free Software Foundation, Inc.,
e9bfca
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
e9bfca
+
e9bfca
+import json
e9bfca
+import logging
e9bfca
+import sys
e9bfca
+import time
e9bfca
+
e9bfca
+from http.client import HTTPSConnection
e9bfca
+from urllib.parse import urlparse
e9bfca
+
e9bfca
+import ovirtsdk4 as sdk
e9bfca
+import ovirtsdk4.types as types
e9bfca
+
e9bfca
+# Parameters are passed in via a JSON doc from the OCaml code.
e9bfca
+# Because this Python code ships embedded inside virt-v2v there
e9bfca
+# is no formal API here.
e9bfca
+params = None
e9bfca
+ovf = None                      # OVF file
e9bfca
+
e9bfca
+if len(sys.argv) != 3:
e9bfca
+    raise RuntimeError("incorrect number of parameters")
e9bfca
+
e9bfca
+# Parameters are passed in via a JSON document.
e9bfca
+with open(sys.argv[1], 'r') as fp:
e9bfca
+    params = json.load(fp)
e9bfca
+
e9bfca
+# What is passed in is a password file, read the actual password.
e9bfca
+with open(params['output_password'], 'r') as fp:
e9bfca
+    output_password = fp.read()
e9bfca
+output_password = output_password.rstrip()
e9bfca
+
e9bfca
+# Read the OVF document.
e9bfca
+with open(sys.argv[2], 'r') as fp:
e9bfca
+    ovf = fp.read()
e9bfca
+
e9bfca
+# Parse out the username from the output_conn URL.
e9bfca
+parsed = urlparse(params['output_conn'])
e9bfca
+username = parsed.username or "admin@internal"
e9bfca
+
e9bfca
+# Connect to the server.
e9bfca
+connection = sdk.Connection(
e9bfca
+    url = params['output_conn'],
e9bfca
+    username = username,
e9bfca
+    password = output_password,
e9bfca
+    ca_file = params['rhv_cafile'],
e9bfca
+    log = logging.getLogger(),
e9bfca
+    insecure = params['insecure'],
e9bfca
+)
e9bfca
+
e9bfca
+system_service = connection.system_service()
e9bfca
+
e9bfca
+# Get the storage domain UUID and substitute it into the OVF doc.
e9bfca
+sds_service = system_service.storage_domains_service()
e9bfca
+sd = sds_service.list(search=("name=%s" % params['output_storage']))[0]
e9bfca
+sd_uuid = sd.id
e9bfca
+
e9bfca
+ovf.replace("@SD_UUID@", sd_uuid)
e9bfca
+
e9bfca
+vms_service = system_service.vms_service()
e9bfca
+vm = vms_service.add(
e9bfca
+    types.Vm(
e9bfca
+        cluster=types.Cluster(name = params['rhv_cluster']),
e9bfca
+        initialization=types.Initialization(
e9bfca
+            configuration = types.Configuration(
e9bfca
+                type = types.ConfigurationType.OVA,
e9bfca
+                data = ovf,
e9bfca
+            )
e9bfca
+        )
e9bfca
+    )
e9bfca
+)
e9bfca
diff --git a/v2v/rhv-upload-plugin.py b/v2v/rhv-upload-plugin.py
e9bfca
new file mode 100644
e9bfca
index 000000000..791c9e7d2
e9bfca
--- /dev/null
e9bfca
+++ b/v2v/rhv-upload-plugin.py
e9bfca
@@ -0,0 +1,445 @@
e9bfca
+# -*- python -*-
e9bfca
+# oVirt or RHV upload nbdkit plugin used by ‘virt-v2v -o rhv-upload’
e9bfca
+# Copyright (C) 2018 Red Hat Inc.
e9bfca
+#
e9bfca
+# This program is free software; you can redistribute it and/or modify
e9bfca
+# it under the terms of the GNU General Public License as published by
e9bfca
+# the Free Software Foundation; either version 2 of the License, or
e9bfca
+# (at your option) any later version.
e9bfca
+#
e9bfca
+# This program is distributed in the hope that it will be useful,
e9bfca
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
e9bfca
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
e9bfca
+# GNU General Public License for more details.
e9bfca
+#
e9bfca
+# You should have received a copy of the GNU General Public License along
e9bfca
+# with this program; if not, write to the Free Software Foundation, Inc.,
e9bfca
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
e9bfca
+
e9bfca
+import builtins
e9bfca
+import json
e9bfca
+import logging
e9bfca
+import ssl
e9bfca
+import sys
e9bfca
+import time
e9bfca
+
e9bfca
+from http.client import HTTPSConnection
e9bfca
+from urllib.parse import urlparse
e9bfca
+
e9bfca
+import ovirtsdk4 as sdk
e9bfca
+import ovirtsdk4.types as types
e9bfca
+
e9bfca
+# Timeout to wait for oVirt disks to change status, or the transfer
e9bfca
+# object to finish initializing [seconds].
e9bfca
+timeout = 5*60
e9bfca
+
e9bfca
+# Parameters are passed in via a JSON doc from the OCaml code.
e9bfca
+# Because this Python code ships embedded inside virt-v2v there
e9bfca
+# is no formal API here.
e9bfca
+params = None
e9bfca
+
e9bfca
+def config(key, value):
e9bfca
+    global params
e9bfca
+
e9bfca
+    if key == "params":
e9bfca
+        with builtins.open(value, 'r') as fp:
e9bfca
+            params = json.load(fp)
e9bfca
+    else:
e9bfca
+        raise RuntimeError("unknown configuration key '%s'" % key)
e9bfca
+
e9bfca
+def config_complete():
e9bfca
+    if params is None:
e9bfca
+        raise RuntimeError("missing configuration parameters")
e9bfca
+
e9bfca
+def debug(s):
e9bfca
+    if params['verbose']:
e9bfca
+        print(s, file=sys.stderr)
e9bfca
+        sys.stderr.flush()
e9bfca
+
e9bfca
+def open(readonly):
e9bfca
+    # Parse out the username from the output_conn URL.
e9bfca
+    parsed = urlparse(params['output_conn'])
e9bfca
+    username = parsed.username or "admin@internal"
e9bfca
+
e9bfca
+    # Read the password from file.
e9bfca
+    with builtins.open(params['output_password'], 'r') as fp:
e9bfca
+        password = fp.read()
e9bfca
+    password = password.rstrip()
e9bfca
+
e9bfca
+    # Connect to the server.
e9bfca
+    connection = sdk.Connection(
e9bfca
+        url = params['output_conn'],
e9bfca
+        username = username,
e9bfca
+        password = password,
e9bfca
+        ca_file = params['rhv_cafile'],
e9bfca
+        log = logging.getLogger(),
e9bfca
+        insecure = params['insecure'],
e9bfca
+    )
e9bfca
+
e9bfca
+    system_service = connection.system_service()
e9bfca
+
e9bfca
+    # Create the disk.
e9bfca
+    disks_service = system_service.disks_service()
e9bfca
+    if params['disk_format'] == "raw":
e9bfca
+        disk_format = types.DiskFormat.RAW
e9bfca
+    else:
e9bfca
+        disk_format = types.DiskFormat.COW
e9bfca
+    disk = disks_service.add(
e9bfca
+        disk = types.Disk(
e9bfca
+            name = params['disk_name'],
e9bfca
+            description = "Uploaded by virt-v2v",
e9bfca
+            format = disk_format,
e9bfca
+            initial_size = params['disk_size'],
e9bfca
+            provisioned_size = params['disk_size'],
e9bfca
+            # XXX Ignores params['output_sparse'].
e9bfca
+            # Handling this properly will be complex, see:
e9bfca
+            # https://www.redhat.com/archives/libguestfs/2018-March/msg00177.html
e9bfca
+            sparse = True,
e9bfca
+            storage_domains = [
e9bfca
+                types.StorageDomain(
e9bfca
+                    name = params['output_storage'],
e9bfca
+                )
e9bfca
+            ],
e9bfca
+        )
e9bfca
+    )
e9bfca
+
e9bfca
+    # Wait till the disk is up, as the transfer can't start if the
e9bfca
+    # disk is locked:
e9bfca
+    disk_service = disks_service.disk_service(disk.id)
e9bfca
+    debug("disk.id = %r" % disk.id)
e9bfca
+
e9bfca
+    endt = time.time() + timeout
e9bfca
+    while True:
e9bfca
+        time.sleep(5)
e9bfca
+        disk = disk_service.get()
e9bfca
+        if disk.status == types.DiskStatus.OK:
e9bfca
+            break
e9bfca
+        if time.time() > endt:
e9bfca
+            raise RuntimeError("timed out waiting for disk to become unlocked")
e9bfca
+
e9bfca
+    # Get a reference to the transfer service.
e9bfca
+    transfers_service = system_service.image_transfers_service()
e9bfca
+
e9bfca
+    # Create a new image transfer.
e9bfca
+    transfer = transfers_service.add(
e9bfca
+        types.ImageTransfer(
e9bfca
+            image = types.Image(
e9bfca
+                id = disk.id
e9bfca
+            )
e9bfca
+        )
e9bfca
+    )
e9bfca
+    debug("transfer.id = %r" % transfer.id)
e9bfca
+
e9bfca
+    # Get a reference to the created transfer service.
e9bfca
+    transfer_service = transfers_service.image_transfer_service(transfer.id)
e9bfca
+
e9bfca
+    # After adding a new transfer for the disk, the transfer's status
e9bfca
+    # will be INITIALIZING.  Wait until the init phase is over. The
e9bfca
+    # actual transfer can start when its status is "Transferring".
e9bfca
+    endt = time.time() + timeout
e9bfca
+    while True:
e9bfca
+        time.sleep(5)
e9bfca
+        transfer = transfer_service.get()
e9bfca
+        if transfer.phase != types.ImageTransferPhase.INITIALIZING:
e9bfca
+            break
e9bfca
+        if time.time() > endt:
e9bfca
+            raise RuntimeError("timed out waiting for transfer status " +
e9bfca
+                               "!= INITIALIZING")
e9bfca
+
e9bfca
+    # Now we have permission to start the transfer.
e9bfca
+    if params['rhv_direct']:
e9bfca
+        if transfer.transfer_url is None:
e9bfca
+            raise RuntimeError("direct upload to host not supported, " +
e9bfca
+                               "requires ovirt-engine >= 4.2 and only works " +
e9bfca
+                               "when virt-v2v is run within the oVirt/RHV " +
e9bfca
+                               "environment, eg. on an oVirt node.")
e9bfca
+        destination_url = urlparse(transfer.transfer_url)
e9bfca
+    else:
e9bfca
+        destination_url = urlparse(transfer.proxy_url)
e9bfca
+
e9bfca
+    context = ssl.create_default_context()
e9bfca
+    context.load_verify_locations(cafile = params['rhv_cafile'])
e9bfca
+
e9bfca
+    http = HTTPSConnection(
e9bfca
+        destination_url.hostname,
e9bfca
+        destination_url.port,
e9bfca
+        context = context
e9bfca
+    )
e9bfca
+
e9bfca
+    # Save everything we need to make requests in the handle.
e9bfca
+    return {
e9bfca
+        'can_flush': False,
e9bfca
+        'can_trim': False,
e9bfca
+        'can_zero': False,
e9bfca
+        'connection': connection,
e9bfca
+        'disk': disk,
e9bfca
+        'disk_service': disk_service,
e9bfca
+        'failed': False,
e9bfca
+        'got_options': False,
e9bfca
+        'highestwrite': 0,
e9bfca
+        'http': http,
e9bfca
+        'needs_auth': not params['rhv_direct'],
e9bfca
+        'path': destination_url.path,
e9bfca
+        'transfer': transfer,
e9bfca
+        'transfer_service': transfer_service,
e9bfca
+    }
e9bfca
+
e9bfca
+# Can we issue zero, trim or flush requests?
e9bfca
+def get_options(h):
e9bfca
+    if h['got_options']:
e9bfca
+        return
e9bfca
+    h['got_options'] = True
e9bfca
+
e9bfca
+    http = h['http']
e9bfca
+    transfer = h['transfer']
e9bfca
+
e9bfca
+    http.putrequest("OPTIONS", h['path'])
e9bfca
+    http.putheader("Authorization", transfer.signed_ticket)
e9bfca
+    http.endheaders()
e9bfca
+
e9bfca
+    r = http.getresponse()
e9bfca
+    if r.status == 200:
e9bfca
+        # New imageio never needs authentication.
e9bfca
+        h['needs_auth'] = False
e9bfca
+
e9bfca
+        j = json.loads(r.read())
e9bfca
+        h['can_zero'] = "zero" in j['features']
e9bfca
+        h['can_trim'] = "trim" in j['features']
e9bfca
+        h['can_flush'] = "flush" in j['features']
e9bfca
+
e9bfca
+    # Old imageio servers returned either 405 Method Not Allowed or
e9bfca
+    # 204 No Content (with an empty body).  If we see that we leave
e9bfca
+    # all the features as False and they will be emulated.
e9bfca
+    elif r.status == 405 or r.status == 204:
e9bfca
+        pass
e9bfca
+
e9bfca
+    else:
e9bfca
+        raise RuntimeError("could not use OPTIONS request: %d: %s" %
e9bfca
+                           (r.status, r.reason))
e9bfca
+
e9bfca
+def can_trim(h):
e9bfca
+    get_options(h)
e9bfca
+    return h['can_trim']
e9bfca
+
e9bfca
+def can_flush(h):
e9bfca
+    get_options(h)
e9bfca
+    return h['can_flush']
e9bfca
+
e9bfca
+def get_size(h):
e9bfca
+    return params['disk_size']
e9bfca
+
e9bfca
+# For documentation see:
e9bfca
+# https://github.com/oVirt/ovirt-imageio/blob/master/docs/random-io.md
e9bfca
+# For examples of working code to read/write from the server, see:
e9bfca
+# https://github.com/oVirt/ovirt-imageio/blob/master/daemon/test/server_test.py
e9bfca
+
e9bfca
+def pread(h, count, offset):
e9bfca
+    http = h['http']
e9bfca
+    transfer = h['transfer']
e9bfca
+    transfer_service = h['transfer_service']
e9bfca
+
e9bfca
+    http.putrequest("GET", h['path'])
e9bfca
+    # Authorization is only needed for old imageio.
e9bfca
+    if h['needs_auth']:
e9bfca
+        http.putheader("Authorization", transfer.signed_ticket)
e9bfca
+    http.putheader("Range", "bytes=%d-%d" % (offset, offset+count-1))
e9bfca
+    http.endheaders()
e9bfca
+
e9bfca
+    r = http.getresponse()
e9bfca
+    # 206 = HTTP Partial Content.
e9bfca
+    if r.status != 206:
e9bfca
+        h['transfer_service'].pause()
e9bfca
+        h['failed'] = True
e9bfca
+        raise RuntimeError("could not read sector (%d, %d): %d: %s" %
e9bfca
+                           (offset, count, r.status, r.reason))
e9bfca
+    return r.read()
e9bfca
+
e9bfca
+def pwrite(h, buf, offset):
e9bfca
+    http = h['http']
e9bfca
+    transfer = h['transfer']
e9bfca
+    transfer_service = h['transfer_service']
e9bfca
+
e9bfca
+    count = len(buf)
e9bfca
+    h['highestwrite'] = max(h['highestwrite'], offset+count)
e9bfca
+
e9bfca
+    http.putrequest("PUT", h['path'] + "?flush=n")
e9bfca
+    # Authorization is only needed for old imageio.
e9bfca
+    if h['needs_auth']:
e9bfca
+        http.putheader("Authorization", transfer.signed_ticket)
e9bfca
+    # The oVirt server only uses the first part of the range, and the
e9bfca
+    # content-length.
e9bfca
+    http.putheader("Content-Range", "bytes %d-%d/*" % (offset, offset+count-1))
e9bfca
+    http.putheader("Content-Length", str(count))
e9bfca
+    http.endheaders()
e9bfca
+    http.send(buf)
e9bfca
+
e9bfca
+    r = http.getresponse()
e9bfca
+    if r.status != 200:
e9bfca
+        transfer_service.pause()
e9bfca
+        h['failed'] = True
e9bfca
+        raise RuntimeError("could not write sector (%d, %d): %d: %s" %
e9bfca
+                           (offset, count, r.status, r.reason))
e9bfca
+
e9bfca
+def zero(h, count, offset, may_trim):
e9bfca
+    http = h['http']
e9bfca
+    transfer = h['transfer']
e9bfca
+    transfer_service = h['transfer_service']
e9bfca
+
e9bfca
+    # Unlike the trim and flush calls, there is no 'can_zero' method
e9bfca
+    # so nbdkit could call this even if the server doesn't support
e9bfca
+    # zeroing.  If this is the case we must emulate.
e9bfca
+    if not h['can_zero']:
e9bfca
+        emulate_zero(h, count, offset)
e9bfca
+        return
e9bfca
+
e9bfca
+    # Construct the JSON request for zeroing.
e9bfca
+    buf = json.dumps({'op': "zero",
e9bfca
+                      'offset': offset,
e9bfca
+                      'size': count,
e9bfca
+                      'flush': False}).encode()
e9bfca
+
e9bfca
+    http.putrequest("PATCH", h['path'])
e9bfca
+    http.putheader("Content-Type", "application/json")
e9bfca
+    http.putheader("Content-Length", len(buf))
e9bfca
+    http.endheaders()
e9bfca
+    http.send(buf)
e9bfca
+
e9bfca
+    r = http.getresponse()
e9bfca
+    if r.status != 200:
e9bfca
+        transfer_service.pause()
e9bfca
+        h['failed'] = True
e9bfca
+        raise RuntimeError("could not zero sector (%d, %d): %d: %s" %
e9bfca
+                           (offset, count, r.status, r.reason))
e9bfca
+
e9bfca
+def emulate_zero(h, count, offset):
e9bfca
+    # qemu-img convert starts by trying to zero/trim the whole device.
e9bfca
+    # Since we've just created a new disk it's safe to ignore these
e9bfca
+    # requests as long as they are smaller than the highest write seen.
e9bfca
+    # After that we must emulate them with writes.
e9bfca
+    if offset+count < h['highestwrite']:
e9bfca
+        http.putrequest("PUT", h['path'])
e9bfca
+        # Authorization is only needed for old imageio.
e9bfca
+        if h['needs_auth']:
e9bfca
+            http.putheader("Authorization", transfer.signed_ticket)
e9bfca
+        http.putheader("Content-Range",
e9bfca
+                       "bytes %d-%d/*" % (offset, offset+count-1))
e9bfca
+        http.putheader("Content-Length", str(count))
e9bfca
+        http.endheaders()
e9bfca
+
e9bfca
+        buf = bytearray(128*1024)
e9bfca
+        while count > len(buf):
e9bfca
+            http.send(buf)
e9bfca
+            count -= len(buf)
e9bfca
+        http.send(buffer(buf, 0, count))
e9bfca
+
e9bfca
+        r = http.getresponse()
e9bfca
+        if r.status != 200:
e9bfca
+            transfer_service.pause()
e9bfca
+            h['failed'] = True
e9bfca
+            raise RuntimeError("could not write zeroes (%d, %d): %d: %s" %
e9bfca
+                               (offset, count, r.status, r.reason))
e9bfca
+
e9bfca
+def trim(h, count, offset):
e9bfca
+    http = h['http']
e9bfca
+    transfer = h['transfer']
e9bfca
+    transfer_service = h['transfer_service']
e9bfca
+
e9bfca
+    # Construct the JSON request for trimming.
e9bfca
+    buf = json.dumps({'op': "trim",
e9bfca
+                      'offset': offset,
e9bfca
+                      'size': count,
e9bfca
+                      'flush': False}).encode()
e9bfca
+
e9bfca
+    http.putrequest("PATCH", h['path'])
e9bfca
+    http.putheader("Content-Type", "application/json")
e9bfca
+    http.putheader("Content-Length", len(buf))
e9bfca
+    http.endheaders()
e9bfca
+    http.send(buf)
e9bfca
+
e9bfca
+    r = http.getresponse()
e9bfca
+    if r.status != 200:
e9bfca
+        transfer_service.pause()
e9bfca
+        h['failed'] = True
e9bfca
+        raise RuntimeError("could not trim sector (%d, %d): %d: %s" %
e9bfca
+                           (offset, count, r.status, r.reason))
e9bfca
+
e9bfca
+def flush(h):
e9bfca
+    http = h['http']
e9bfca
+    transfer = h['transfer']
e9bfca
+    transfer_service = h['transfer_service']
e9bfca
+
e9bfca
+    # Construct the JSON request for flushing.
e9bfca
+    buf = json.dumps({'op': "flush"}).encode()
e9bfca
+
e9bfca
+    http.putrequest("PATCH", h['path'])
e9bfca
+    http.putheader("Content-Type", "application/json")
e9bfca
+    http.putheader("Content-Length", len(buf))
e9bfca
+    http.endheaders()
e9bfca
+    http.send(buf)
e9bfca
+
e9bfca
+    r = http.getresponse()
e9bfca
+    if r.status != 200:
e9bfca
+        transfer_service.pause()
e9bfca
+        h['failed'] = True
e9bfca
+        raise RuntimeError("could not flush: %d: %s" % (r.status, r.reason))
e9bfca
+
e9bfca
+def delete_disk_on_failure(h):
e9bfca
+    disk_service = h['disk_service']
e9bfca
+    disk_service.remove()
e9bfca
+
e9bfca
+def close(h):
e9bfca
+    http = h['http']
e9bfca
+    connection = h['connection']
e9bfca
+
e9bfca
+    # This is sometimes necessary because python doesn't set up
e9bfca
+    # sys.stderr to be line buffered and so debug, errors or
e9bfca
+    # exceptions printed previously might not be emitted before the
e9bfca
+    # plugin exits.
e9bfca
+    sys.stderr.flush()
e9bfca
+
e9bfca
+    # If the connection failed earlier ensure we clean up the disk.
e9bfca
+    if h['failed']:
e9bfca
+        delete_disk_on_failure(h)
e9bfca
+        connection.close()
e9bfca
+        return
e9bfca
+
e9bfca
+    try:
e9bfca
+        # Issue a flush request on close so that the data is written to
e9bfca
+        # persistent store before we create the VM.
e9bfca
+        if h['can_flush']:
e9bfca
+            flush(h)
e9bfca
+
e9bfca
+        http.close()
e9bfca
+
e9bfca
+        disk = h['disk']
e9bfca
+        transfer_service = h['transfer_service']
e9bfca
+
e9bfca
+        transfer_service.finalize()
e9bfca
+
e9bfca
+        # Wait until the transfer disk job is completed since
e9bfca
+        # only then we can be sure the disk is unlocked.  As this
e9bfca
+        # code is not very clear, what's happening is that we are
e9bfca
+        # waiting for the transfer object to cease to exist, which
e9bfca
+        # falls through to the exception case and then we can
e9bfca
+        # continue.
e9bfca
+        endt = time.time() + timeout
e9bfca
+        try:
e9bfca
+            while True:
e9bfca
+                time.sleep(1)
e9bfca
+                tmp = transfer_service.get()
e9bfca
+                if time.time() > endt:
e9bfca
+                    raise RuntimeError("timed out waiting for transfer " +
e9bfca
+                                       "to finalize")
e9bfca
+        except sdk.NotFoundError:
e9bfca
+            pass
e9bfca
+
e9bfca
+        # Write the disk ID file.  Only do this on successful completion.
e9bfca
+        with builtins.open(params['diskid_file'], 'w') as fp:
e9bfca
+            fp.write(disk.id)
e9bfca
+
e9bfca
+    except:
e9bfca
+        # Otherwise on any failure we must clean up the disk.
e9bfca
+        delete_disk_on_failure(h)
e9bfca
+        raise
e9bfca
+
e9bfca
+    connection.close()
e9bfca
diff --git a/v2v/rhv-upload-precheck.py b/v2v/rhv-upload-precheck.py
e9bfca
new file mode 100644
e9bfca
index 000000000..2798a29dd
e9bfca
--- /dev/null
e9bfca
+++ b/v2v/rhv-upload-precheck.py
e9bfca
@@ -0,0 +1,73 @@
e9bfca
+# -*- python -*-
e9bfca
+# oVirt or RHV pre-upload checks used by ‘virt-v2v -o rhv-upload’
e9bfca
+# Copyright (C) 2018 Red Hat Inc.
e9bfca
+#
e9bfca
+# This program is free software; you can redistribute it and/or modify
e9bfca
+# it under the terms of the GNU General Public License as published by
e9bfca
+# the Free Software Foundation; either version 2 of the License, or
e9bfca
+# (at your option) any later version.
e9bfca
+#
e9bfca
+# This program is distributed in the hope that it will be useful,
e9bfca
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
e9bfca
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
e9bfca
+# GNU General Public License for more details.
e9bfca
+#
e9bfca
+# You should have received a copy of the GNU General Public License along
e9bfca
+# with this program; if not, write to the Free Software Foundation, Inc.,
e9bfca
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
e9bfca
+
e9bfca
+import json
e9bfca
+import logging
e9bfca
+import sys
e9bfca
+import time
e9bfca
+
e9bfca
+from http.client import HTTPSConnection
e9bfca
+from urllib.parse import urlparse
e9bfca
+
e9bfca
+import ovirtsdk4 as sdk
e9bfca
+import ovirtsdk4.types as types
e9bfca
+
e9bfca
+# Parameters are passed in via a JSON doc from the OCaml code.
e9bfca
+# Because this Python code ships embedded inside virt-v2v there
e9bfca
+# is no formal API here.
e9bfca
+params = None
e9bfca
+
e9bfca
+if len(sys.argv) != 2:
e9bfca
+    raise RuntimeError("incorrect number of parameters")
e9bfca
+
e9bfca
+# Parameters are passed in via a JSON document.
e9bfca
+with open(sys.argv[1], 'r') as fp:
e9bfca
+    params = json.load(fp)
e9bfca
+
e9bfca
+# What is passed in is a password file, read the actual password.
e9bfca
+with open(params['output_password'], 'r') as fp:
e9bfca
+    output_password = fp.read()
e9bfca
+output_password = output_password.rstrip()
e9bfca
+
e9bfca
+# Parse out the username from the output_conn URL.
e9bfca
+parsed = urlparse(params['output_conn'])
e9bfca
+username = parsed.username or "admin@internal"
e9bfca
+
e9bfca
+# Connect to the server.
e9bfca
+connection = sdk.Connection(
e9bfca
+    url = params['output_conn'],
e9bfca
+    username = username,
e9bfca
+    password = output_password,
e9bfca
+    ca_file = params['rhv_cafile'],
e9bfca
+    log = logging.getLogger(),
e9bfca
+    insecure = params['insecure'],
e9bfca
+)
e9bfca
+
e9bfca
+system_service = connection.system_service()
e9bfca
+
e9bfca
+# Find if a virtual machine already exists with that name.
e9bfca
+vms_service = system_service.vms_service()
e9bfca
+vms = vms_service.list(
e9bfca
+    search = ("name=%s" % params['output_name']),
e9bfca
+)
e9bfca
+if len(vms) > 0:
e9bfca
+    vm = vms[0]
e9bfca
+    raise RuntimeError("VM already exists with name ‘%s’, id ‘%s’" %
e9bfca
+                       (params['output_name'], vm.id))
e9bfca
+
e9bfca
+# Otherwise everything is OK, exit with no error.
e9bfca
diff --git a/v2v/test-v2v-o-rhv-upload-oo-query.sh b/v2v/test-v2v-o-rhv-upload-oo-query.sh
e9bfca
new file mode 100755
e9bfca
index 000000000..29d69e1c1
e9bfca
--- /dev/null
e9bfca
+++ b/v2v/test-v2v-o-rhv-upload-oo-query.sh
e9bfca
@@ -0,0 +1,38 @@
e9bfca
+#!/bin/bash -
e9bfca
+# libguestfs virt-v2v test script
e9bfca
+# Copyright (C) 2018 Red Hat Inc.
e9bfca
+#
e9bfca
+# This program is free software; you can redistribute it and/or modify
e9bfca
+# it under the terms of the GNU General Public License as published by
e9bfca
+# the Free Software Foundation; either version 2 of the License, or
e9bfca
+# (at your option) any later version.
e9bfca
+#
e9bfca
+# This program is distributed in the hope that it will be useful,
e9bfca
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
e9bfca
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
e9bfca
+# GNU General Public License for more details.
e9bfca
+#
e9bfca
+# You should have received a copy of the GNU General Public License
e9bfca
+# along with this program; if not, write to the Free Software
e9bfca
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
e9bfca
+
e9bfca
+# Test -oo "?" option.
e9bfca
+
e9bfca
+set -e
e9bfca
+
e9bfca
+$TEST_FUNCTIONS
e9bfca
+skip_if_skipped
e9bfca
+
e9bfca
+export VIRT_TOOLS_DATA_DIR="$top_srcdir/test-data/fake-virt-tools"
e9bfca
+export VIRTIO_WIN="$top_srcdir/test-data/fake-virtio-win"
e9bfca
+
e9bfca
+f=test-v2v-o-rhv-upload-oo-query.actual
e9bfca
+rm -f $f
e9bfca
+
e9bfca
+$VG virt-v2v --debug-gc \
e9bfca
+    -o rhv-upload -oo "?" > $f
e9bfca
+
e9bfca
+grep -- "-oo rhv-cafile" $f
e9bfca
+grep -- "-oo rhv-verifypeer" $f
e9bfca
+
e9bfca
+rm $f
e9bfca
diff --git a/v2v/test-v2v-python-syntax.sh b/v2v/test-v2v-python-syntax.sh
e9bfca
new file mode 100755
e9bfca
index 000000000..b167f4610
e9bfca
--- /dev/null
e9bfca
+++ b/v2v/test-v2v-python-syntax.sh
e9bfca
@@ -0,0 +1,45 @@
e9bfca
+#!/bin/bash -
e9bfca
+# libguestfs
e9bfca
+# Copyright (C) 2018 Red Hat Inc.
e9bfca
+#
e9bfca
+# This program is free software; you can redistribute it and/or modify
e9bfca
+# it under the terms of the GNU General Public License as published by
e9bfca
+# the Free Software Foundation; either version 2 of the License, or
e9bfca
+# (at your option) any later version.
e9bfca
+#
e9bfca
+# This program is distributed in the hope that it will be useful,
e9bfca
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
e9bfca
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
e9bfca
+# GNU General Public License for more details.
e9bfca
+#
e9bfca
+# You should have received a copy of the GNU General Public License
e9bfca
+# along with this program; if not, write to the Free Software
e9bfca
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
e9bfca
+
e9bfca
+set -e
e9bfca
+
e9bfca
+$TEST_FUNCTIONS
e9bfca
+skip_if_skipped
e9bfca
+
e9bfca
+# Files to check.
e9bfca
+files="rhv-upload-createvm.py rhv-upload-plugin.py rhv-upload-precheck.py"
e9bfca
+
e9bfca
+# Base version of Python.
e9bfca
+python=python3
e9bfca
+
e9bfca
+# Checks the files are syntactically correct, but not very much else.
e9bfca
+for f in $files; do
e9bfca
+    $python -m py_compile $f
e9bfca
+done
e9bfca
+
e9bfca
+# Checks the files correspond to PEP8 coding style.
e9bfca
+# https://www.python.org/dev/peps/pep-0008/
e9bfca
+if $python-pep8 --version >/dev/null 2>&1; then
e9bfca
+    for f in $files; do
e9bfca
+        # Ignore:
e9bfca
+        # E226 missing whitespace around arithmetic operator
e9bfca
+        # E251 unexpected spaces around keyword / parameter equals
e9bfca
+        # E302 expected 2 blank lines, found 1
e9bfca
+        $python-pep8 --ignore=E226,E251,E302 $f
e9bfca
+    done
e9bfca
+fi
e9bfca
diff --git a/v2v/virt-v2v.pod b/v2v/virt-v2v.pod
e9bfca
index 5dd888e77..62b726b8f 100644
e9bfca
--- a/v2v/virt-v2v.pod
e9bfca
+++ b/v2v/virt-v2v.pod
e9bfca
@@ -6,15 +6,18 @@ virt-v2v - Convert a guest to use KVM
e9bfca
 
e9bfca
  virt-v2v -ic vpx://vcenter.example.com/Datacenter/esxi vmware_guest
e9bfca
 
e9bfca
- virt-v2v -ic vpx://vcenter.example.com/Datacenter/esxi vmware_guest \
e9bfca
-   -o rhv -os rhv.nfs:/export_domain --bridge ovirtmgmt
e9bfca
-
e9bfca
  virt-v2v -i libvirtxml guest-domain.xml -o local -os /var/tmp
e9bfca
 
e9bfca
  virt-v2v -i disk disk.img -o local -os /var/tmp
e9bfca
 
e9bfca
  virt-v2v -i disk disk.img -o glance
e9bfca
 
e9bfca
+ virt-v2v -ic vpx://vcenter.example.com/Datacenter/esxi vmware_guest \
e9bfca
+   -o rhv-upload -oc https://ovirt-engine.example.com/ovirt-engine/api \
e9bfca
+   -os ovirt-data -op /tmp/ovirt-admin-password -of raw \
e9bfca
+   -oo rhv-cafile=/tmp/ca.pem -oo rhv-direct \
e9bfca
+   --bridge ovirtmgmt
e9bfca
+
e9bfca
 =head1 DESCRIPTION
e9bfca
 
e9bfca
 Virt-v2v converts guests from a foreign hypervisor to run on KVM.  It
e9bfca
@@ -50,20 +53,18 @@ For more information see L</INPUT FROM VMWARE VCENTER SERVER> below.
e9bfca
 =head2 Convert from VMware to RHV/oVirt
e9bfca
 
e9bfca
 This is the same as the previous example, except you want to send the
e9bfca
-guest to a RHV-M Export Storage Domain which is located remotely
e9bfca
-(over NFS) at C<rhv.nfs:/export_domain>.  If you are unclear about
e9bfca
-the location of the Export Storage Domain you should check the
e9bfca
-settings on your RHV-M management console.  Guest network
e9bfca
+guest to a RHV Data Domain using the RHV REST API.  Guest network
e9bfca
 interface(s) are connected to the target network called C<ovirtmgmt>.
e9bfca
 
e9bfca
  virt-v2v -ic vpx://vcenter.example.com/Datacenter/esxi vmware_guest \
e9bfca
-   -o rhv -os rhv.nfs:/export_domain --bridge ovirtmgmt
e9bfca
+   -o rhv-upload -oc https://ovirt-engine.example.com/ovirt-engine/api \
e9bfca
+   -os ovirt-data -op /tmp/ovirt-admin-password -of raw \
e9bfca
+   -oo rhv-cafile=/tmp/ca.pem -oo rhv-direct \
e9bfca
+   --bridge ovirtmgmt
e9bfca
 
e9bfca
 In this case the host running virt-v2v acts as a B<conversion server>.
e9bfca
 
e9bfca
-Note that after conversion, the guest will appear in the RHV-M Export
e9bfca
-Storage Domain, from where you will need to import it using the RHV-M
e9bfca
-user interface.  (See L</OUTPUT TO RHV>).
e9bfca
+For more information see L</OUTPUT TO RHV> below.
e9bfca
 
e9bfca
 =head2 Convert from ESXi hypervisor over SSH to local libvirt
e9bfca
 
e9bfca
@@ -122,9 +123,9 @@ command line.
e9bfca
  Xen ───▶│ -i libvirt ──▶ │            │     │  (default) │
e9bfca
  ... ───▶│  (default) │   │            │ ──┐ └────────────┘
e9bfca
          └────────────┘   │            │ ─┐└──────▶ -o glance
e9bfca
- -i libvirtxml ─────────▶ │            │ ┐└─────────▶ -o rhv
e9bfca
- -i vmx ────────────────▶ │            │ └──────────▶ -o vdsm
e9bfca
-                          └────────────┘
e9bfca
+ -i libvirtxml ─────────▶ │            │ ┐├─────────▶ -o rhv
e9bfca
+ -i vmx ────────────────▶ │            │ │└─────────▶ -o vdsm
e9bfca
+                          └────────────┘ └──────────▶ -o rhv-upload
e9bfca
 
e9bfca
 Virt-v2v has a number of possible input and output modes, selected
e9bfca
 using the I<-i> and I<-o> options.  Only one input and output mode can
e9bfca
@@ -157,8 +158,9 @@ libvirt configuration file (mainly for testing).
e9bfca
 I<-o qemu> writes to a local disk image with a shell script for
e9bfca
 booting the guest directly in qemu (mainly for testing).
e9bfca
 
e9bfca
-I<-o rhv> is used to write to a RHV / oVirt target.  I<-o vdsm>
e9bfca
-is only used when virt-v2v runs under VDSM control.
e9bfca
+I<-o rhv-upload> is used to write to a RHV / oVirt target.  I<-o rhv>
e9bfca
+is a legacy method to write to RHV / oVirt E<lt> 4.2.  I<-o vdsm> is
e9bfca
+only used when virt-v2v runs under VDSM control.
e9bfca
 
e9bfca
 =head1 OPTIONS
e9bfca
 
e9bfca
@@ -426,6 +428,10 @@ written.
e9bfca
 
e9bfca
 This is the same as I<-o rhv>.
e9bfca
 
e9bfca
+=item B<-o> B<ovirt-upload>
e9bfca
+
e9bfca
+This is the same as I<-o rhv-upload>.
e9bfca
+
e9bfca
 =item B<-o> B<qemu>
e9bfca
 
e9bfca
 Set the output method to I<qemu>.
e9bfca
@@ -447,6 +453,16 @@ I<-os> parameter must also be used to specify the location of the
e9bfca
 Export Storage Domain.  Note this does not actually import the guest
e9bfca
 into RHV.  You have to do that manually later using the UI.
e9bfca
 
e9bfca
+See L</OUTPUT TO RHV (OLD METHOD)> below.
e9bfca
+
e9bfca
+=item B<-o> B<rhv-upload>
e9bfca
+
e9bfca
+Set the output method to I<rhv-upload>.
e9bfca
+
e9bfca
+The converted guest is written directly to a RHV Data Domain.
e9bfca
+This is a faster method than I<-o rhv>, but requires oVirt
e9bfca
+or RHV E<ge> 4.2.
e9bfca
+
e9bfca
 See L</OUTPUT TO RHV> below.
e9bfca
 
e9bfca
 =item B<-o> B<vdsm>
e9bfca
@@ -488,7 +504,33 @@ the output name is the same as the input name.
e9bfca
 Set output option(s) related to the current output mode.
e9bfca
 To display short help on what options are available you can use:
e9bfca
 
e9bfca
- virt-v2v -o vdsm -oo "?"
e9bfca
+ virt-v2v -o rhv-upload -oo "?"
e9bfca
+
e9bfca
+=item B<-oo rhv-cafile=>F<ca.pem>
e9bfca
+
e9bfca
+For I<-o rhv-upload> (L</OUTPUT TO RHV>) only, the F<ca.pem> file
e9bfca
+(Certificate Authority), copied from F</etc/pki/ovirt-engine/ca.pem>
e9bfca
+on the oVirt engine.
e9bfca
+
e9bfca
+=item B<-oo rhv-cluster=>C<CLUSTERNAME>
e9bfca
+
e9bfca
+For I<-o rhv-upload> (L</OUTPUT TO RHV>) only, set the RHV Cluster
e9bfca
+Name.  If not given it uses C<Default>.
e9bfca
+
e9bfca
+=item B<-oo rhv-direct>
e9bfca
+
e9bfca
+For I<-o rhv-upload> (L</OUTPUT TO RHV>) only, if this option is given
e9bfca
+then virt-v2v will attempt to directly upload the disk to the oVirt
e9bfca
+node, otherwise it will proxy the upload through the oVirt engine.
e9bfca
+Direct upload requires that you have network access to the oVirt
e9bfca
+nodes.  Non-direct upload is slightly slower but should work in all
e9bfca
+situations.
e9bfca
+
e9bfca
+=item B<-oo rhv-verifypeer>
e9bfca
+
e9bfca
+For I<-o rhv-upload> (L</OUTPUT TO RHV>) only, verify the oVirt/RHV
e9bfca
+server’s identity by checking the server‘s certificate against the
e9bfca
+Certificate Authority.
e9bfca
 
e9bfca
 =item B<-oo vdsm-compat=0.10>
e9bfca
 
e9bfca
@@ -1749,6 +1791,68 @@ Define the final guest in libvirt:
e9bfca
 
e9bfca
 =head1 OUTPUT TO RHV
e9bfca
 
e9bfca
+This new method to upload guests to oVirt or RHV directly via the REST
e9bfca
+API requires oVirt/RHV E<ge> 4.2.
e9bfca
+
e9bfca
+You need to specify I<-o rhv-upload> as well as the following extra
e9bfca
+parameters:
e9bfca
+
e9bfca
+=over 4
e9bfca
+
e9bfca
+=item I<-oc> C<https://ovirt-engine.example.com/ovirt-engine/api>
e9bfca
+
e9bfca
+The URL of the REST API which is usually the server name with
e9bfca
+C</ovirt-engine/api> appended, but might be different if you installed
e9bfca
+oVirt Engine on a different path.
e9bfca
+
e9bfca
+You can optionally add a username and port number to the URL.  If the
e9bfca
+username is not specified then virt-v2v defaults to using
e9bfca
+C<admin@internal> which is the typical superuser account for oVirt
e9bfca
+instances.
e9bfca
+
e9bfca
+=item I<-of raw>
e9bfca
+
e9bfca
+Currently you must use I<-of raw> and you cannot use I<-oa preallocated>.
e9bfca
+
e9bfca
+These restrictions will be loosened in a future version.
e9bfca
+
e9bfca
+=item I<-op> F<password-file>
e9bfca
+
e9bfca
+A file containing a password to be used when connecting to the oVirt
e9bfca
+engine.  Note the file should contain the whole password, B
e9bfca
+any trailing newline>, and for security the file should have mode
e9bfca
+C<0600> so that others cannot read it.
e9bfca
+
e9bfca
+=item I<-os> C<ovirt-data>
e9bfca
+
e9bfca
+The storage domain.
e9bfca
+
e9bfca
+=item I<-oo rhv-cafile=>F<ca.pem>
e9bfca
+
e9bfca
+The F<ca.pem> file (Certificate Authority), copied from
e9bfca
+F</etc/pki/ovirt-engine/ca.pem> on the oVirt engine.
e9bfca
+
e9bfca
+=item I<-oo rhv-cluster=>C<CLUSTERNAME>
e9bfca
+
e9bfca
+Set the RHV Cluster Name.  If not given it uses C<Default>.
e9bfca
+
e9bfca
+=item I<-oo rhv-direct>
e9bfca
+
e9bfca
+If this option is given then virt-v2v will attempt to directly upload
e9bfca
+the disk to the oVirt node, otherwise it will proxy the upload through
e9bfca
+the oVirt engine.  Direct upload requires that you have network access
e9bfca
+to the oVirt nodes.  Non-direct upload is slightly slower but should
e9bfca
+work in all situations.
e9bfca
+
e9bfca
+=item I<-oo rhv-verifypeer>
e9bfca
+
e9bfca
+Verify the oVirt/RHV server’s identity by checking the server‘s
e9bfca
+certificate against the Certificate Authority.
e9bfca
+
e9bfca
+=back
e9bfca
+
e9bfca
+=head1 OUTPUT TO RHV (OLD METHOD)
e9bfca
+
e9bfca
 This section only applies to the I<-o rhv> output mode.  If you use
e9bfca
 virt-v2v from the RHV-M user interface, then behind the scenes the
e9bfca
 import is managed by VDSM using the I<-o vdsm> output mode (which end
e9bfca
-- 
e9bfca
2.17.1
e9bfca