On Tue, 27 Feb 2018 21:33:24 +0000
"Richard W.M. Jones" <rjones(a)redhat.com> wrote:
PROBLEMS:
- Spools to a temporary disk
- Target cluster
Seeing how you parse and reconstruct the URI we may encode the cluster
into the fragment or query part.
- Delete disks on failure, or rename disks on success?
- Handling of sparseness in raw format disks
- Manual page
This adds a new output mode to virt-v2v. virt-v2v -o rhv-upload
streams images directly to an oVirt or RHV >= 4 Data Domain using the
oVirt SDK v4. It is more efficient than -o rhv because it does not
need to go via the Export Storage Domain, and is possible for humans
to use unlike -o vdsm.
The implementation uses the Python SDK by running snippets of Python
code to interact with the ‘ovirtsdk4’ module. It requires both Python 3
and the Python SDK v4 to be installed at run time (these are not,
however, new dependencies of virt-v2v since most people wouldn't have
them).
---
v2v/Makefile.am | 2 +
v2v/cmdline.ml | 38 ++++
v2v/output_rhv_upload.ml | 451 ++++++++++++++++++++++++++++++++++++++++++++++
v2v/output_rhv_upload.mli | 27 +++
4 files changed, 518 insertions(+)
diff --git a/v2v/Makefile.am b/v2v/Makefile.am
index c2eb31097..cd44dfc2a 100644
--- a/v2v/Makefile.am
+++ b/v2v/Makefile.am
@@ -64,6 +64,7 @@ SOURCES_MLI = \
output_null.mli \
output_qemu.mli \
output_rhv.mli \
+ output_rhv_upload.mli \
output_vdsm.mli \
parse_ovf_from_ova.mli \
parse_libvirt_xml.mli \
@@ -116,6 +117,7 @@ SOURCES_ML = \
output_local.ml \
output_qemu.ml \
output_rhv.ml \
+ output_rhv_upload.ml \
output_vdsm.ml \
inspect_source.ml \
target_bus_assignment.ml \
diff --git a/v2v/cmdline.ml b/v2v/cmdline.ml
index d725ae022..c53d1703b 100644
--- a/v2v/cmdline.ml
+++ b/v2v/cmdline.ml
@@ -65,6 +65,8 @@ let parse_cmdline () =
let output_password = ref None in
let output_storage = ref None in
let password_file = ref None in
+ let rhv_cafile = ref None in
+ let rhv_direct = ref false in
let vddk_config = ref None in
let vddk_cookie = ref None in
let vddk_libdir = ref None in
@@ -143,6 +145,8 @@ let parse_cmdline () =
| "disk" | "local" -> output_mode := `Local
| "null" -> output_mode := `Null
| "ovirt" | "rhv" | "rhev" -> output_mode := `RHV
+ | "ovirt-upload" | "ovirt_upload" | "rhv-upload" |
"rhv_upload" ->
+ output_mode := `RHV_Upload
| "qemu" -> output_mode := `QEmu
| "vdsm" -> output_mode := `VDSM
| s ->
@@ -229,6 +233,9 @@ let parse_cmdline () =
[ L"print-source" ], Getopt.Set print_source,
s_"Print source and stop";
[ L"qemu-boot" ], Getopt.Set qemu_boot, s_"Boot in qemu (-o qemu
only)";
+ [ L"rhv-cafile" ], Getopt.String ("ca.pem",
set_string_option_once "--rhv-cafile" rhv_cafile),
+ s_"For -o rhv-upload, set ‘ca.pem’ file";
+ [ L"rhv-direct" ], Getopt.Set rhv_direct, s_"Use direct transfer
mode";
[ L"root" ], Getopt.String ("ask|... ", set_root_choice),
s_"How to choose root filesystem";
[ L"vddk-config" ], Getopt.String ("filename",
set_string_option_once "--vddk-config" vddk_config),
@@ -322,6 +329,8 @@ read the man page virt-v2v(1).
let password_file = !password_file in
let print_source = !print_source in
let qemu_boot = !qemu_boot in
+ let rhv_cafile = !rhv_cafile in
+ let rhv_direct = !rhv_direct in
let root_choice = !root_choice in
let vddk_options =
{ vddk_config = !vddk_config;
@@ -546,6 +555,35 @@ read the man page virt-v2v(1).
Output_rhv.output_rhv os output_alloc,
output_format, output_alloc
+ | `RHV_Upload ->
+ let output_conn =
+ match output_conn with
+ | None ->
+ 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")
+ | Some oc -> oc in
+ (* In theory we could make the password optional in future. *)
+ let output_password =
+ match output_password with
+ | None ->
+ 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")
+ | Some op -> op in
+ let os =
+ match output_storage with
+ | None ->
+ error (f_"-o rhv-upload: output storage was not specified, use
‘-os’");
+ | Some os -> os in
+ if qemu_boot then
+ error_option_cannot_be_used_in_output_mode "rhv-upload"
"--qemu-boot";
+ let rhv_cafile =
+ match rhv_cafile with
+ | None ->
+ error (f_"-o rhv-upload: must use ‘--rhv-cafile’ to supply the path to
the oVirt or RHV server’s ‘ca.pem’ file")
+ | Some rhv_cafile -> rhv_cafile in
+ Output_rhv_upload.output_rhv_upload output_alloc output_conn
+ output_password os
+ rhv_cafile rhv_direct,
+ output_format, output_alloc
+
| `VDSM ->
if output_password <> None then
error_option_cannot_be_used_in_output_mode "vdsm" "-op";
diff --git a/v2v/output_rhv_upload.ml b/v2v/output_rhv_upload.ml
new file mode 100644
index 000000000..c5bcd45e3
--- /dev/null
+++ b/v2v/output_rhv_upload.ml
@@ -0,0 +1,451 @@
+(* virt-v2v
+ * Copyright (C) 2009-2018 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 Unix
+
+open Std_utils
+open Tools_utils
+open Unix_utils
+open Common_gettext.Gettext
+
+open Types
+open Utils
+
+(* Timeout to wait for oVirt disks to change status, or the transfer
+ * object to finish initializing [seconds].
+ *)
+let ovirt_timeout = 5*60
+
+(* These correspond mostly to the fields in the Python
+ * sdk.Connection object, except for the password which
+ * is handled separately.
+ *)
+type connection = {
+ conn_url : string;
+ conn_username : string;
+ conn_debug : bool;
+}
+
+let string_of_connection conn =
+ sprintf "url=%s username=%s debug=%b"
+ conn.conn_url conn.conn_username conn.conn_debug
+
+(* Python code fragments go first. Note these must not be
+ * indented because of Python's stupid whitespace thing.
+ *)
If you want some indentation it is possible. But you will have to modify
your python invocation to something ugly like:
let cmd = sprintf "%s -c '%s' %s" (quote python)
"import sys\nimport textwrap\nwith open(sys.argv[1]) as f:
exec(textwrap.dedent(f.read()))"
(quote filename) in
You need to be careful that the code snippets you glue together have
same leading indentation though. So the usefulness of this is
questionable at best.
Tomas
+
+(* Print the Python version. *)
+let python_get_version = "
+import sys
+print (sys.version[0]) # syntax works on py2 or py3
+"
+
+(* Import all the Python modules needed. *)
+let python_imports = "
+import logging
+import ovirtsdk4 as sdk
+import ovirtsdk4.types as types
+import ssl
+import sys
+import time
+
+from http.client import HTTPSConnection
+
+try:
+ from urllib.parse import urlparse
+except ImportError:
+ from urlparse import urlparse
+"
+
+(* Create the Python prologue which connects to the system service.
+ * This returns a string of Python code.
+ *)
+let python_connect tmpdir conn output_password =
+ sprintf "
+password_file = %s
+with open(password_file, 'r') as file:
+ password = file.read()
+password = password.rstrip()
+
+# Open the connection.
+connection = sdk.Connection(
+ url = %s,
+ username = %s,
+ password = password,
+ debug = %s,
+ log = logging.getLogger(),
+ insecure = True,
+)
+system_service = connection.system_service()
+" (py_quote output_password)
+ (py_quote conn.conn_url)
+ (py_quote conn.conn_username)
+ (py_bool conn.conn_debug)
+
+let python_get_storage_domain_id output_storage =
+ let search_term = sprintf "name=%s" output_storage in
+ sprintf "
+sds_service = system_service.storage_domains_service()
+sd = sds_service.list(search=%s)[0]
+print(sd.id)
+" (py_quote search_term)
+
+let python_create_one_disk disk_name disk_format
+ output_alloc sd_id disk_size =
+ sprintf "
+disks_service = system_service.disks_service()
+disk = disks_service.add(
+ disk = types.Disk(
+ name = %s,
+ format = %s,
+ sparse = %s,
+ provisioned_size = %Ld,
+ storage_domains = [types.StorageDomain(id = %s)],
+ )
+)
+disk_id = disk.id
+
+# Wait til the disk is up. The transfer cannot start if the
+# disk is locked.
+disk_service = disks_service.disk_service(disk_id)
+timeout = time.time() + %d
+while True:
+ time.sleep(5)
+ disk = disk_service.get()
+ if disk.status == types.DiskStatus.OK:
+ break
+ if time.time() > timeout:
+ raise RuntimeError(\"Timed out waiting for disk to become unlocked\")
+
+# Return the disk ID.
+print(disk_id)
+" (py_quote disk_name)
+ disk_format (* it's a raw Python expression, don't quote *)
+ (py_bool (output_alloc = Sparse))
+ disk_size
+ (py_quote sd_id)
+ ovirt_timeout
+
+(* XXX Temporary function. *)
+let python_upload_one_disk disk_id disk_size filename rhv_cafile rhv_direct =
+ sprintf "
+transfers_service = system_service.image_transfers_service()
+
+transfer = transfers_service.add(
+ types.ImageTransfer(
+ image = types.Image (id=%s)
+ )
+)
+
+transfer_service = transfers_service.image_transfer_service(transfer.id)
+
+# After adding a new transfer for the disk, the transfer's status will
+# be INITIALIZING. Wait until the init phase is over.
+timeout = time.time() + %d
+while True:
+ time.sleep(5)
+ transfer = transfer_service.get()
+ if transfer.phase != types.ImageTransferPhase.INITIALIZING:
+ break
+ if time.time() > timeout:
+ raise RuntimeError(\"Timed out waiting for transfer status !=
INITIALIZING\")
+
+if %s:
+ if transfer.transfer_url is None:
+ raise RuntimeError(\"Direct upload to host not supported, requires
ovirt-engine >= 4.2 and only works when virt-v2v is run within the oVirt/RHV
environment, eg. on an ovirt node.\")
+ destination_url = urlparse(transfer.transfer_url)
+else:
+ destination_url = urlparse(transfer.proxy_url)
+
+context = ssl.create_default_context()
+context.load_verify_locations(cafile = %s)
+
+proxy_connection = HTTPSConnection(
+ destination_url.hostname,
+ destination_url.port,
+ context = context
+)
+
+image_path = %s
+image_size = %Ld
+
+proxy_connection.putrequest(\"PUT\", destination_url.path)
+proxy_connection.putheader('Content-Length', image_size)
+proxy_connection.endheaders()
+
+# This seems to give the best throughput when uploading from Yaniv's
+# machine to a server that drops the data. You may need to tune this
+# on your setup.
+BUF_SIZE = 128 * 1024
+
+with open(image_path, \"rb\") as disk:
+ pos = 0
+ while pos < image_size:
+ # Send the next chunk to the proxy.
+ to_read = min(image_size - pos, BUF_SIZE)
+ chunk = disk.read(to_read)
+ if not chunk:
+ transfer_service.pause()
+ raise RuntimeError(\"Unexpected end of file at pos=%%d\" %% pos)
+
+ proxy_connection.send(chunk)
+ pos += len(chunk)
+
+# Get the response
+response = proxy_connection.getresponse()
+if response.status != 200:
+ transfer_service.pause()
+ raise RuntimeError(\"Upload failed: %%s %%s\" %%
+ (response.status, response.reason))
+
+# Successful cleanup
+transfer_service.finalize()
+connection.close()
+proxy_connection.close()
+" (py_quote disk_id)
+ ovirt_timeout
+ (py_bool rhv_direct)
+ (py_quote rhv_cafile)
+ (py_quote filename)
+ disk_size
+
+let python_create_virtual_machine ovf =
+ sprintf "
+vms_service = system_service.vms_service()
+vm = vms_service.add(
+ types.Vm(
+ cluster=types.Cluster(name = %s),
+ initialization=types.Initialization(
+ configuration = types.Configuration(
+ type = types.ConfigurationType.OVA,
+ data = %s
+ )
+ )
+ )
+)
+" (py_quote "Default" (* XXX target cluster *))
+ (py_quote (DOM.doc_to_string ovf))
+
+(* Find the Python 3 binary. *)
+let find_python3 () =
+ let rec loop = function
+ | [] ->
+ error "could not locate Python 3 binary on the $PATH. You may have to
install Python 3. If Python 3 is already installed then you may need to create a
directory containing a binary called ‘python3’ which runs Python 3."
+ | python :: rest ->
+ (* Use shell_command first to check the binary exists. *)
+ let cmd = sprintf "%s --help >/dev/null 2>&1" (quote python)
in
+ if shell_command cmd = 0 &&
+ run_python ~python python_get_version = ["3"] then (
+ debug "rhv-upload: python binary: %s" python;
+ python
+ )
+ else
+ loop rest
+ in
+ loop ["python3"; "python"]
+
+(* Parse the -oc URI. *)
+let parse_output_conn oc =
+ let uri = Xml.parse_uri oc in
+ if uri.Xml.uri_scheme <> Some "https" then
+ error (f_"rhv-upload: -oc: URI must start with https://...");
+ if uri.Xml.uri_server = None then
+ error (f_"rhv-upload: -oc: no remote server name in the URI");
+ if uri.Xml.uri_path = None || uri.Xml.uri_path = Some "/" then
+ error (f_"rhv-upload: -oc: URI path component looks incorrect");
+ let username =
+ match uri.Xml.uri_user with
+ | None ->
+ warning (f_"rhv-upload: -oc: username was missing from URI, assuming
‘admin@internal’");
+ "admin@internal"
+ | Some user -> user in
+ (* Reconstruct the URI without the username. *)
+ let url = sprintf "%s://%s%s"
+ (Option.default "https" uri.Xml.uri_scheme)
+ (Option.default "localhost" uri.Xml.uri_server)
+ (Option.default "" uri.Xml.uri_path) in
+
+ let conn = { conn_url = url; conn_username = username;
+ conn_debug = verbose () } in
+ debug "rhv-upload: connection=%s" (string_of_connection conn);
+ conn
+
+(* Get the storage domain ID. *)
+let get_storage_domain_id run_python tmpdir conn output_password
+ output_storage =
+ let code =
+ python_imports ^
+ python_connect tmpdir conn output_password ^
+ python_get_storage_domain_id output_storage in
+ match run_python code with
+ | [id] -> id
+ | _ ->
+ error (f_"rhv-upload: get_storage_domain_id: could not fetch storage domain ID
of ‘%s’ (does it exist on the server?). See previous output for more details.")
output_storage
+
+(* Create a single, empty disk on the target. *)
+let create_one_disk run_python tmpdir conn
+ output_password output_format
+ output_alloc sd_id
+ source target =
+ (* Give the disk a predictable name based on the source
+ * name and disk index.
+ *)
+ let disk_name =
+ let id = target.target_overlay.ov_source.s_disk_id in
+ sprintf "%s-%03d" source.s_name id in
+
+ let disk_format =
+ match output_format with
+ | `Raw -> "types.DiskFormat.RAW"
+ | `COW -> "types.DiskFormat.COW" in
+
+ (* This is the virtual size in bytes. *)
+ let disk_size = target.target_overlay.ov_virtual_size in
+
+ let code =
+ python_imports ^
+ python_connect tmpdir conn output_password ^
+ python_create_one_disk disk_name disk_format
+ output_alloc sd_id disk_size in
+ match run_python code with
+ | [id] -> id
+ | _ ->
+ error (f_"rhv-upload: create_one_disk: error creating disks, see previous
output")
+
+(* XXX Temporary function to upload spooled disk. *)
+let upload_one_disk run_python tmpdir conn output_password
+ rhv_cafile rhv_direct
+ t filename disk_id =
+ let disk_size = t.target_overlay.ov_virtual_size in
+
+ let code =
+ python_imports ^
+ python_connect tmpdir conn output_password ^
+ python_upload_one_disk disk_id disk_size filename rhv_cafile rhv_direct in
+ ignore (run_python code)
+
+(* Upload the virtual machine metadata (ie OVF) and create a VM. *)
+let create_virtual_machine run_python tmpdir conn output_password ovf =
+ let code =
+ python_imports ^
+ python_connect tmpdir conn output_password ^
+ python_create_virtual_machine ovf in
+ ignore (run_python code)
+
+class output_rhv_upload output_alloc output_conn
+ output_password output_storage
+ rhv_cafile rhv_direct =
+ let run_python =
+ let python = find_python3 () in
+ run_python ~python in
+ let conn = parse_output_conn output_conn in
+
+ (* The temporary directory is used for a few things such as passing
+ * passwords securely and (temporarily) for spooling disks (XXX).
+ *)
+ let tmpdir =
+ let base_dir = (open_guestfs ())#get_cachedir () in
+ let t = Mkdtemp.temp_dir ~base_dir "rhvupload." in
+ rmdir_on_exit t;
+ t in
+
+ (* Storage domain ID. *)
+ let sd_id =
+ get_storage_domain_id run_python tmpdir conn output_password
+ output_storage in
+object
+ inherit output
+
+ method precheck () =
+ (* Check all the dependencies including the Python 3 oVirt SDK v4
+ * module are installed. This will fail with a Python error message.
+ *)
+ ignore (run_python python_imports)
+
+ method as_options =
+ "-o rhv-upload" ^
+ (match output_alloc with
+ | Sparse -> "" (* default, don't need to print it *)
+ | Preallocated -> " -oa preallocated") ^
+ sprintf " -oc %s -op %s -os %s"
+ output_conn output_password output_storage
+
+ method supported_firmware = [ TargetBIOS ]
+
+ (* List of disks we have created. There will be one per target. *)
+ val mutable target_disk_ids = []
+
+ method prepare_targets source targets =
+ let targets =
+ List.map (
+ fun t ->
+ (* Only allow output format "raw" or "qcow2". *)
+ let output_format =
+ match t.target_format with
+ | "raw" -> `Raw
+ | "qcow2" -> `COW
+ | _ ->
+ 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.")
+ t.target_format in
+
+ let disk_id = create_one_disk run_python tmpdir conn output_password
+ output_format output_alloc
+ sd_id source t in
+
+ (* XXX Temporarily spool disks to tmpdir. *)
+ let target_file = TargetFile (tmpdir // t.target_overlay.ov_sd) in
+ { t with target_file }, disk_id
+ ) targets in
+ target_disk_ids <- List.map snd targets;
+ List.map fst targets
+
+ method create_metadata source targets _ guestcaps inspect target_firmware =
+ (* Upload the spooled disks. *)
+ List.iter (
+ fun (t, disk_id) ->
+ let filename =
+ match t.target_file with
+ | TargetFile filename -> filename
+ | TargetURI _ -> assert false in
+ upload_one_disk run_python tmpdir conn output_password
+ rhv_cafile rhv_direct
+ t filename disk_id
+ ) (List.combine targets target_disk_ids);
+
+ let image_uuids = target_disk_ids
+ and vol_uuids = List.map (fun _ -> uuidgen ()) target_disk_ids
+ and vm_uuid = uuidgen () in
+
+ (* Create the metadata. *)
+ let ovf =
+ Create_ovf.create_ovf source targets guestcaps inspect
+ Sparse
+ sd_id (* storage UUID *)
+ image_uuids
+ vol_uuids
+ vm_uuid
+ OVirt in
+
+ (* Add the virtual machine. *)
+ create_virtual_machine run_python tmpdir conn output_password ovf
+
+end
+
+let output_rhv_upload = new output_rhv_upload
+let () = Modules_list.register_output_module "rhv-upload"
diff --git a/v2v/output_rhv_upload.mli b/v2v/output_rhv_upload.mli
new file mode 100644
index 000000000..3e7086f85
--- /dev/null
+++ b/v2v/output_rhv_upload.mli
@@ -0,0 +1,27 @@
+(* virt-v2v
+ * Copyright (C) 2009-2018 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.
+ *)
+
+(** [-o rhv-upload] target. *)
+
+val output_rhv_upload : Types.output_allocation -> string -> string ->
+ string -> string -> bool ->
+ Types.output
+(** [output_rhv_upload output_alloc output_conn output_password output_storage
+ rhv_cafile rhv_direct]
+ creates and returns a new {!Types.output} object specialized for writing
+ output to oVirt or RHV directly via RHV APIs. *)
--
2.13.2
--
Tomáš Golembiovský <tgolembi(a)redhat.com>