diff --git a/v2v/DOM.ml b/v2v/DOM.ml new file mode 100644 index 0000000000..d828db4536 --- /dev/null +++ b/v2v/DOM.ml @@ -0,0 +1,97 @@ +(* virt-v2v + * Copyright (C) 2009-2014 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + *) + +(* Poor man's XML DOM, mutable for easy of modification. *) + +open Common_utils +open Utils + +open Printf + +type node = + | PCData of string + | Element of element +and element = { + e_name : string; (* Name of element. *) + mutable e_attrs : attr list; (* Attributes. *) + mutable e_children : node list; (* Child elements. *) +} +and attr = string * string +and doc = element + +let doc name attrs children = + { e_name = name; e_attrs = attrs; e_children = children } + +let e name attrs children = + Element { e_name = name; e_attrs = attrs; e_children = children } + +let rec node_to_chan chan = function + | PCData str -> output_string chan (xml_quote_pcdata str) + | Element e -> element_to_chan chan e +and element_to_chan chan + { e_name = name; e_attrs = attrs; e_children = children } = + fprintf chan "<%s" name; + List.iter (fun (n, v) -> fprintf chan " %s='%s'" n (xml_quote_attr v)) attrs; + output_char chan '>'; + List.iter (node_to_chan chan) children; + fprintf chan "" name + +let doc_to_chan chan doc = + fprintf chan "\n"; + element_to_chan chan doc + +let path_to_nodes doc path = + match path with + | [] -> invalid_arg "path_to_nodes: empty path" + | top_name :: path -> + if doc.e_name <> top_name then [] + else ( + let rec loop nodes path = + match path with + | [] -> [] + | [p] -> + List.filter ( + function + | PCData _ -> false + | Element e when e.e_name = p -> true + | Element _ -> false + ) nodes + | p :: ps -> + let children = + filter_map ( + function + | PCData _ -> None + | Element e when e.e_name = p -> Some e.e_children + | Element _ -> None + ) nodes in + List.concat (List.map (fun nodes -> loop nodes ps) children) + in + loop doc.e_children path + ) + +let filter_node_list_by_attr nodes attr = + List.filter ( + function + | Element { e_attrs = attrs } when List.mem attr attrs -> true + | Element _ | PCData _ -> false + ) nodes + +let find_node_by_attr nodes attr = + match filter_node_list_by_attr nodes attr with + | [] -> raise Not_found + | x::_ -> x diff --git a/v2v/DOM.mli b/v2v/DOM.mli new file mode 100644 index 0000000000..2c97f5c97f --- /dev/null +++ b/v2v/DOM.mli @@ -0,0 +1,52 @@ +(* virt-v2v + * Copyright (C) 2009-2014 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + *) + +(** Poor man's XML DOM, mutable for easy of modification. *) + +type node = + | PCData of string + | Element of element +and element = { + e_name : string; (** Name of element. *) + mutable e_attrs : attr list; (** Attributes. *) + mutable e_children : node list; (** Child elements. *) +} +and attr = string * string +and doc = element + +val doc : string -> attr list -> node list -> doc +(** A quick way to create a document. *) + +val e : string -> attr list -> node list -> node +(** A quick way to create elements. *) + +val doc_to_chan : out_channel -> doc -> unit +(** Write the XML document to an output channel. *) + +val path_to_nodes : doc -> string list -> node list +(** Search down the path and return a list of all matching elements. + Returns an empty list if none were found. *) + +val filter_node_list_by_attr : node list -> attr -> node list +(** Find DOM elements which have a particular attribute name=value (not + recursively). If not found, returns an empty list. *) + +val find_node_by_attr : node list -> attr -> node +(** Find the first DOM element which has a particular attribute + name=value (not recursively). If not found, raises + [Not_found]. *) diff --git a/v2v/Makefile.am b/v2v/Makefile.am index 0d8ef09af8..97d6d3eab3 100644 --- a/v2v/Makefile.am +++ b/v2v/Makefile.am @@ -26,9 +26,11 @@ CLEANFILES = *~ *.cmi *.cmo *.cmx *.cmxa *.o virt-v2v SOURCES_MLI = \ convert_linux.mli \ convert_windows.mli \ + DOM.mli \ lib_linux.mli \ source_libvirt.mli \ target_local.mli \ + target_RHEV.mli \ types.mli \ xml.mli @@ -37,18 +39,21 @@ SOURCES_ML = \ types.ml \ utils.ml \ xml.ml \ + DOM.ml \ lib_linux.ml \ cmdline.ml \ source_libvirt.ml \ convert_linux.ml \ convert_windows.ml \ target_local.ml \ + target_RHEV.ml \ v2v.ml SOURCES_C = \ $(top_builddir)/fish/progress.c \ $(top_builddir)/mllib/tty-c.c \ $(top_builddir)/mllib/progress-c.c \ + $(top_builddir)/mllib/mkdtemp-c.c \ $(top_builddir)/customize/crypt-c.c \ utils-c.c \ xml-c.c @@ -73,6 +78,7 @@ BOBJECTS = \ $(top_builddir)/mllib/tTY.cmo \ $(top_builddir)/mllib/progress.cmo \ $(top_builddir)/mllib/config.cmo \ + $(top_builddir)/mllib/mkdtemp.cmo \ $(top_builddir)/customize/urandom.cmo \ $(top_builddir)/customize/random_seed.cmo \ $(top_builddir)/customize/hostname.cmo \ diff --git a/v2v/cmdline.ml b/v2v/cmdline.ml index 52a4959274..0d46fc67df 100644 --- a/v2v/cmdline.ml +++ b/v2v/cmdline.ml @@ -42,6 +42,7 @@ let parse_cmdline () = let quiet = ref false in let verbose = ref false in let trace = ref false in + let vmtype = ref "" in let input_mode = ref `Libvirt in let set_input_mode = function @@ -98,6 +99,7 @@ let parse_cmdline () = "--verbose", Arg.Set verbose, ditto; "-V", Arg.Unit display_version, " " ^ s_"Display version and exit"; "--version", Arg.Unit display_version, ditto; + "--vmtype", Arg.Set_string vmtype, "server|desktop " ^ s_"Set vmtype (for RHEV)"; "-x", Arg.Set trace, " " ^ s_"Enable tracing of libguestfs calls"; ] in long_options := argspec; @@ -139,6 +141,13 @@ read the man page virt-v2v(1). let root_choice = !root_choice in let verbose = !verbose in let trace = !trace in + let vmtype = + match !vmtype with + | "server" -> Some `Server + | "desktop" -> Some `Desktop + | "" -> None + | _ -> + error (f_"unknown --vmtype option, must be \"server\" or \"desktop\"") in (* No arguments and machine-readable mode? Print out some facts * about what this binary supports. @@ -177,6 +186,8 @@ read the man page virt-v2v(1). | `Libvirt -> if output_storage <> "" then error (f_"-o libvirt: do not use the -os option"); + if vmtype <> None then + error (f_"--vmtype option can only be used with '-o rhev'"); OutputLibvirt output_conn | `Local -> if output_storage = "" then @@ -184,11 +195,13 @@ read the man page virt-v2v(1). if not (is_directory output_storage) then error (f_"-os %s: output directory does not exist or is not a directory") output_storage; + if vmtype <> None then + error (f_"--vmtype option can only be used with '-o rhev'"); OutputLocal output_storage | `RHEV -> if output_storage = "" then error (f_"-o local: output storage was not specified, use '-os'"); - OutputRHEV output_storage in + OutputRHEV (output_storage, vmtype) in input, output, debug_gc, output_alloc, output_format, output_name, diff --git a/v2v/target_RHEV.ml b/v2v/target_RHEV.ml new file mode 100644 index 0000000000..d3f360d7a7 --- /dev/null +++ b/v2v/target_RHEV.ml @@ -0,0 +1,568 @@ +(* virt-v2v + * Copyright (C) 2009-2014 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + *) + +open Common_gettext.Gettext +open Common_utils + +open Unix +open Printf + +open Types +open Utils +open DOM + +let title = sprintf "Exported by virt-v2v %s" Config.package_version + +(* Describes a mounted Export Storage Domain. *) +type export_storage_domain = { + mp : string; (* Local mountpoint. *) + uuid : string; (* /mp/uuid *) +} + +(* Private data that we keep between calls. Keep it in a global + * variables since there's only one conversion going on. + *) +(* Export Storage Domain mountpoint. *) +let esd = ref { mp = ""; uuid = "" } +(* Target image directory, UUID. *) +let image_uuid = ref "" +let image_dir = ref "" +(* Flag to indicate if the target image (image_dir) should be + * deleted. This is set to false once we know the conversion was + * successful. + *) +let delete_target_directory = ref true +(* We set the creation time to be the same for all dates in + * all metadata files. + *) +let time = time () +let iso_time = + let tm = gmtime time in + sprintf "%04d/%02d/%02d %02d:%02d:%02d" + (tm.tm_year + 1900) (tm.tm_mon + 1) tm.tm_mday + tm.tm_hour tm.tm_min tm.tm_sec + +(* This is called early on in the conversion and lets us choose the + * name of the target files that eventually get written by the main + * code. + * + * 'os' is the output storage (-os nfs:/export). 'source' contains a + * few useful fields such as the guest name. 'overlays' describes the + * destination files. We modify and return this list. + * + * Note it's good to fail here (early) if there are any problems, since + * the next time we are called (in {!create_metadata}) we have already + * done the conversion and copy, and the user won't thank us for + * displaying errors there. + *) +let rec initialize ~verbose os source output_alloc overlays = + esd := mount_and_check_export_storage_domain ~verbose os; + if verbose then + eprintf "RHEV: ESD mountpoint: %s\nRHEV: ESD UUID: %s\n%!" !esd.mp !esd.uuid; + + (* Create a unique UUID for the final image. *) + image_uuid := uuidgen ~prog (); + image_dir := !esd.mp // !esd.uuid // "images" // !image_uuid; + + (* We need to create the target image directory so there's a place + * for the main program to copy the images to. However if image + * conversion fails for any reason then we delete this directory. + *) + mkdir !image_dir 0o755; + at_exit (fun () -> + if !delete_target_directory then ( + let cmd = sprintf "rm -rf %s" (quote !image_dir) in + ignore (Sys.command cmd) + ) + ); + if verbose then + eprintf "RHEV: export directory: %s\n%!" !image_dir; + + (* This loop has two purposes: (1) Generate the randomly named + * target files (just the names). (2) Generate the .meta file + * associated with each volume. At the end we have a directory + * structure like this: + * ///images// + * # first disk - will be created by main code + * .meta # first disk + * # second disk - will be created by main code + * .meta # second disk + * # etc + * .meta # + *) + let overlays = + let output_alloc_for_rhev = + match output_alloc with + | `Sparse -> "SPARSE" + | `Preallocated -> "PREALLOCATED" in + + List.map ( + fun ov -> + let vol_uuid = uuidgen ~prog () in + let target_file = !image_dir // vol_uuid in + + if verbose then + eprintf "RHEV: will export %s to %s\n%!" ov.ov_sd target_file; + + (* Create the per-volume metadata (.meta files, in an oVirt- + * specific format). + *) + let vol_meta = target_file ^ ".meta" in + + let size_in_sectors = + if ov.ov_virtual_size &^ 511L <> 0L then + error (f_"the virtual size of the input disk %s is not an exact multiple of 512 bytes. The virtual size is: %Ld.\n\nThis probably means something unexpected is going on, so please file a bug about this issue.") ov.ov_source_file ov.ov_virtual_size; + ov.ov_virtual_size /^ 512L in + + let format_for_rhev = + match ov.ov_target_format with + | "raw" -> "RAW" + | "qcow2" -> "COW" + | _ -> + error (f_"RHEV does not support the output format '%s', only raw or qcow2") ov.ov_target_format in + + let chan = open_out vol_meta in + let fpf fs = fprintf chan fs in + fpf "DOMAIN=%s\n" !esd.uuid; (* "Domain" as in Export Storage Domain *) + fpf "VOLTYPE=LEAF\n"; + fpf "CTIME=%.0f\n" time; + fpf "MTIME=%.0f\n" time; + fpf "IMAGE=%s\n" !image_uuid; + fpf "DISKTYPE=1\n"; + fpf "PUUID=00000000-0000-0000-0000-000000000000\n"; + fpf "LEGALITY=LEGAL\n"; + fpf "POOL_UUID=\n"; + fpf "SIZE=%Ld\n" size_in_sectors; + fpf "FORMAT=%s\n" format_for_rhev; + fpf "TYPE=%s\n" output_alloc_for_rhev; + fpf "DESCRIPTION=%s\n" title; + fpf "EOF\n"; + close_out chan; + + { ov with + ov_target_file_tmp = target_file; ov_target_file = target_file; + ov_vol_uuid = vol_uuid } + ) overlays in + + (* Return the list of overlays. *) + overlays + +and mount_and_check_export_storage_domain ~verbose os = + (* The user can either specify -os nfs:/export, or a local directory + * which is assumed to be the already-mounted NFS export. In either + * case we need to check that we have sufficient permissions to write + * to this mountpoint. + *) + match string_split ":/" os with + | mp, "" -> (* Already mounted directory. *) + check_export_storage_domain os mp + | server, export -> + let export = "/" ^ export in + + (* Try mounting it. *) + let mp = Mkdtemp.temp_dir "v2v." "" in + let cmd = + sprintf "mount %s:%s %s" (quote server) (quote export) (quote mp) in + if verbose then printf "%s\n%!" cmd; + if Sys.command cmd <> 0 then + error (f_"mount command failed, see earlier errors.\n\nThis probably means you didn't specify the right Export Storage Domain path [-os %s], or else you need to rerun virt-v2v as root.") os; + + (* Make sure it is unmounted at exit. *) + at_exit (fun () -> + let cmd = sprintf "umount %s" (quote mp) in + if verbose then printf "%s\n%!" cmd; + ignore (Sys.command cmd); + try rmdir mp with _ -> () + ); + + check_export_storage_domain os mp + +and check_export_storage_domain os mp = + (* Typical ESD mountpoint looks like this: + * $ ls /tmp/mnt + * 39b6af0e-1d64-40c2-97e4-4f094f1919c7 __DIRECT_IO_TEST__ lost+found + * $ ls /tmp/mnt/39b6af0e-1d64-40c2-97e4-4f094f1919c7 + * dom_md images master + * We expect exactly one of those magic UUIDs. + *) + let entries = + try Sys.readdir mp + with Sys_error msg -> + error (f_"could not read the Export Storage Domain specified by the '-os %s' parameter on the command line. Is it really an OVirt or RHEV-M Export Storage Domain? The original error is: %s") os msg in + let entries = Array.to_list entries in + let uuids = List.filter ( + fun entry -> + String.length entry = 36 && + entry.[8] = '-' && entry.[13] = '-' && entry.[18] = '-' && + entry.[23] = '-' + ) entries in + let uuid = + match uuids with + | [uuid] -> uuid + | [] -> + error (f_"there are no UUIDs in the Export Storage Domain (%s). Is it really an OVirt or RHEV-M Export Storage Domain?") os + | _::_ -> + error (f_"there are multiple UUIDs in the Export Storage Domain (%s). This is unexpected, and may be a bug in virt-v2v or OVirt.") os in + + (* Check that the domain has been attached to a Data Center by checking that + * the master/vms directory exists. + *) + if not (is_directory (mp // uuid // "master" // "vms")) then + error (f_"the Export Storage Domain (%s) has not been attached to any Data Center.\n\nYou have to do this through the RHEV-M / OVirt user interface first.") os; + + (* Check that the ESD is writable. *) + let testfile = mp // uuid // "v2v-write-test" in + let write_test_failed err = + error (f_"the Export Storage Domain (%s) is not writable.\n\nThis probably means you need to run virt-v2v as 'root'.\n\nOriginal error was: %s") os err; + in + (try + let chan = open_out testfile in + close_out chan; + unlink testfile + with + | Sys_error err -> write_test_failed err + | Unix_error (code, _, _) -> write_test_failed (error_message code) + ); + + (* Looks good, so return the ESD object. *) + { mp = mp; uuid = uuid } + +(* This is called after conversion to write the OVF metadata. *) +let rec create_metadata os vmtype source output_alloc + overlays inspect guestcaps = + let vm_uuid = uuidgen ~prog () in + + let memsize_mb = source.s_memory /^ 1024L /^ 1024L in + + let vmtype = + match vmtype with + | Some vmtype -> vmtype + | None -> get_vmtype inspect in + let vmtype = match vmtype with `Desktop -> "DESKTOP" | `Server -> "SERVER" in + let ostype = get_ostype inspect in + + let ovf : doc = + doc "ovf:Envelope" [ + "xmlns:rasd", "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"; + "xmlns:vssd", "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"; + "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"; + "xmlns:ovf", "http://schemas.dmtf.org/ovf/envelope/1/"; + "ovf:version", "0.9" + ] [ + e "References" [] []; + e "Section" ["xsi:type", "ovf:NetworkSection_Type"] [ + e "Info" [] [PCData "List of networks"] + ]; + e "Section" ["xsi:type", "ovf:DiskSection_Type"] [ + e "Info" [] [PCData "List of Virtual Disks"] + ]; + e "Content" ["ovf:id", "out"; "xsi:type", "ovf:VirtualSystem_Type"] [ + e "Name" [] [PCData source.s_name]; + e "TemplateId" [] [PCData "00000000-0000-0000-0000-000000000000"]; + e "TemplateName" [] [PCData "Blank"]; + e "Description" [] [PCData title]; + e "Domain" [] []; + e "CreationDate" [] [PCData iso_time]; + e "IsInitilized" [] [PCData "True"]; + e "IsAutoSuspend" [] [PCData "False"]; + e "TimeZone" [] []; + e "IsStateless" [] [PCData "False"]; + e "Origin" [] [PCData "0"]; + e "VmType" [] [PCData vmtype]; + e "DefaultDisplayType" [] [PCData "1"]; + + e "Section" ["ovf:id", vm_uuid; "ovf:required", "false"; + "xsi:type", "ovf:OperatingSystemSection_Type"] [ + e "Info" [] [PCData "Guest Operating System"]; + e "Description" [] [PCData ostype]; + ]; + + e "Section" ["xsi:type", "ovf:VirtualHardwareSection_Type"] [ + e "Info" [] [PCData (sprintf "%d CPU, %Ld Memory" source.s_vcpu memsize_mb)]; + e "Item" [] [ + e "rasd:Caption" [] [PCData (sprintf "%d virtual cpu" source.s_vcpu)]; + e "rasd:Description" [] [PCData "Number of virtual CPU"]; + e "rasd:InstanceId" [] [PCData "1"]; + e "rasd:ResourceType" [] [PCData "3"]; + e "rasd:num_of_sockets" [] [PCData (string_of_int source.s_vcpu)]; + e "rasd:cpu_per_socket"[] [PCData "1"]; + ]; + e "Item" [] [ + e "rasd:Caption" [] [PCData (sprintf "%Ld MB of memory" memsize_mb)]; + e "rasd:Description" [] [PCData "Memory Size"]; + e "rasd:InstanceId" [] [PCData "2"]; + e "rasd:ResourceType" [] [PCData "4"]; + e "rasd:AllocationUnits" [] [PCData "MegaBytes"]; + e "rasd:VirtualQuantity" [] [PCData (Int64.to_string memsize_mb)]; + ]; + e "Item" [] [ + e "rasd:Caption" [] [PCData "USB Controller"]; + e "rasd:InstanceId" [] [PCData "4"]; + e "rasd:ResourceType" [] [PCData "23"]; + e "rasd:UsbPolicy" [] [PCData "Disabled"]; + ]; + e "Item" [] [ + e "rasd:Caption" [] [PCData "Graphical Controller"]; + e "rasd:InstanceId" [] [PCData "5"]; + e "rasd:ResourceType" [] [PCData "20"]; + e "rasd:VirtualQuantity" [] [PCData "1"]; + e "rasd:Device" [] [PCData "qxl"]; + ] + ] + ] + ] in + + (* Add disks to the OVF XML. *) + add_disks output_alloc overlays guestcaps ovf; + + (* XXX Missing here from old virt-v2v: + cdroms and floppies + network interfaces + display + See: lib/Sys/VirtConvert/Connection/RHEVTarget.pm + *) + + + + + (* Write it to the metadata file. *) + let dir = !esd.mp // !esd.uuid // "master" // "vms" // vm_uuid in + mkdir dir 0o755; + let file = dir // vm_uuid ^ ".ovf" in + let chan = open_out file in + doc_to_chan chan ovf; + close_out chan; + + (* Finished, so don't delete the target directory on exit. *) + delete_target_directory := false + +(* Guess vmtype based on the guest inspection data. *) +and get_vmtype = function + | { i_type = "linux"; i_distro = "rhel"; i_major_version = major; + i_product_name = product } + when major >= 5 && string_find product "Server" >= 0 -> + `Server + + | { i_type = "linux"; i_distro = "rhel"; i_major_version = major } + when major >= 5 -> + `Desktop + + | { i_type = "linux"; i_distro = "rhel"; i_major_version = major; + i_product_name = product } + when major >= 3 && string_find product "ES" >= 0 -> + `Server + + | { i_type = "linux"; i_distro = "rhel"; i_major_version = major; + i_product_name = product } + when major >= 3 && string_find product "AS" >= 0 -> + `Server + + | { i_type = "linux"; i_distro = "rhel"; i_major_version = major } + when major >= 3 -> + `Desktop + + | { i_type = "linux"; i_distro = "fedora" } -> `Desktop + + | { i_type = "windows"; i_major_version = 5; i_minor_version = 1 } -> + `Desktop (* Windows XP *) + + | { i_type = "windows"; i_major_version = 5; i_minor_version = 2; + i_product_name = product } when string_find product "XP" >= 0 -> + `Desktop (* Windows XP *) + + | { i_type = "windows"; i_major_version = 5; i_minor_version = 2 } -> + `Server (* Windows 2003 *) + + | { i_type = "windows"; i_major_version = 6; i_minor_version = 0; + i_product_name = product } when string_find product "Server" >= 0 -> + `Server (* Windows 2008 *) + + | { i_type = "windows"; i_major_version = 6; i_minor_version = 0 } -> + `Desktop (* Vista *) + + | { i_type = "windows"; i_major_version = 6; i_minor_version = 1; + i_product_name = product } when string_find product "Server" >= 0 -> + `Server (* Windows 2008R2 *) + + | { i_type = "windows"; i_major_version = 6; i_minor_version = 1 } -> + `Server (* Windows 7 *) + + | _ -> `Server + +and get_ostype = function + | { i_type = "linux"; i_distro = "rhel"; i_major_version = v; + i_arch = "i386" } -> + sprintf "RHEL%d" v + + | { i_type = "linux"; i_distro = "rhel"; i_major_version = v; + i_arch = "x86_64" } -> + sprintf "RHEL%dx64" v + + | { i_type = "linux" } -> "OtherLinux" + + | { i_type = "windows"; i_major_version = 5; i_minor_version = 1 } -> + "WindowsXP" (* no architecture differentiation of XP on RHEV *) + + | { i_type = "windows"; i_major_version = 5; i_minor_version = 2; + i_product_name = product } when string_find product "XP" >= 0 -> + "WindowsXP" (* no architecture differentiation of XP on RHEV *) + + | { i_type = "windows"; i_major_version = 5; i_minor_version = 2; + i_arch = "i386" } -> + "Windows2003" + + | { i_type = "windows"; i_major_version = 5; i_minor_version = 2; + i_arch = "x86_64" } -> + "Windows2003x64" + + | { i_type = "windows"; i_major_version = 6; i_minor_version = 0; + i_arch = "i386" } -> + "Windows2008" + + | { i_type = "windows"; i_major_version = 6; i_minor_version = 0; + i_arch = "x86_64" } -> + "Windows2008x64" + + | { i_type = "windows"; i_major_version = 6; i_minor_version = 1; + i_arch = "i386" } -> + "Windows7" + + | { i_type = "windows"; i_major_version = 6; i_minor_version = 1; + i_arch = "x86_64"; i_product_variant = "Client" } -> + "Windows7x64" + + | { i_type = "windows"; i_major_version = 6; i_minor_version = 1; + i_arch = "x86_64" } -> + "Windows2008R2x64" + + | { i_type = typ; i_distro = distro; + i_major_version = major; i_minor_version = minor; + i_product_name = product } -> + warning ~prog (f_"unknown guest operating system: %s %s %d.%d (%s)") + typ distro major minor product; + "Unassigned" + +(* This modifies the OVF DOM, adding a section for each disk. *) +and add_disks output_alloc overlays guestcaps ovf = + let references = + let nodes = path_to_nodes ovf ["ovf:Envelope"; "References"] in + match nodes with + | [] | _::_::_ -> assert false + | [node] -> node in + let disk_section = + let sections = path_to_nodes ovf ["ovf:Envelope"; "Section"] in + try find_node_by_attr sections ("xsi:type", "ovf:DiskSection_Type") + with Not_found -> assert false in + let virtualhardware_section = + let sections = path_to_nodes ovf ["ovf:Envelope"; "Content"; "Section"] in + try find_node_by_attr sections ("xsi:type", "ovf:VirtualHardwareSection_Type") + with Not_found -> assert false in + + let append_child child = function + | PCData _ -> assert false + | Element e -> e.e_children <- e.e_children @ [child] + in + + (* Iterate over the disks, adding them to the OVF document. *) + iteri ( + fun i ov -> + let is_boot_drive = i == 0 in + + let target_file = ov.ov_target_file + and vol_uuid = ov.ov_vol_uuid in + assert (vol_uuid <> ""); + + let fileref = !image_uuid // vol_uuid in + + let size_gb = + Int64.to_float ov.ov_virtual_size /. 1024. /. 1024. /. 1024. in + let usage_gb = + let usage_mb = du_m target_file in + Int64.to_float usage_mb /. 1024. in + + let format_for_rhev = + match ov.ov_target_format with + | "raw" -> "RAW" + | "qcow2" -> "COW" + | _ -> + error (f_"RHEV does not support the output format '%s', only raw or qcow2") ov.ov_target_format in + + let output_alloc_for_rhev = + match output_alloc with + | `Sparse -> "SPARSE" + | `Preallocated -> "PREALLOCATED" in + + (* Add disk to node. *) + let disk = + e "File" [ + "ovf:href", fileref; + "ovf:id", vol_uuid; + "ovf:size", Int64.to_string ov.ov_virtual_size; + "ovf:description", title; + ] [] in + append_child disk references; + + (* Add disk to DiskSection. *) + let disk = + e "Disk" [ + "ovf:diskId", vol_uuid; + "ovf:size", sprintf "%.1f" size_gb; + "ovf:actual_size", sprintf "%.1f" usage_gb; + "ovf:fileRef", fileref; + "ovf:parentRef", ""; + "ovf:vm_snapshot_id", uuidgen ~prog (); + "ovf:volume-format", format_for_rhev; + "ovf:volume-type", output_alloc_for_rhev; + "ovf:format", "http://en.wikipedia.org/wiki/Byte"; (* wtf? *) + "ovf:disk-interface", + if guestcaps.gcaps_block_bus = "virtio" then "VirtIO" else "IDE"; + "ovf:disk-type", "System"; (* RHBZ#744538 *) + "ovf:boot", if is_boot_drive then "True" else "False"; + ] [] in + append_child disk disk_section; + + (* Add disk to VirtualHardware. *) + let item = + e "Item" [] [ + e "rasd:InstanceId" [] [PCData vol_uuid]; + e "rasd:ResourceType" [] [PCData "17"]; + e "rasd:HostResource" [] [PCData fileref]; + e "rasd:Parent" [] [PCData "00000000-0000-0000-0000-000000000000"]; + e "rasd:Template" [] [PCData "00000000-0000-0000-0000-000000000000"]; + e "rasd:ApplicationList" [] []; + e "rasd:StorageId" [] [PCData !esd.uuid]; + e "rasd:StoragePoolId" [] [PCData "00000000-0000-0000-0000-000000000000"]; + e "rasd:CreationDate" [] [PCData iso_time]; + e "rasd:LastModified" [] [PCData iso_time]; + e "rasd:last_modified_date" [] [PCData iso_time]; + ] in + append_child item virtualhardware_section; + ) overlays + +and du_m filename = + (* There's no OCaml binding for st_blocks, so run coreutils 'du -m' + * to get the used size in megabytes. + *) + let cmd = sprintf "du -m %s | awk '{print $1}'" (quote filename) in + let lines = external_command ~prog cmd in + (* We really don't want the metadata generation to fail because + * of some silly usage information, so ignore errors here. + *) + match lines with + | line::_ -> (try Int64.of_string line with _ -> 0L) + | [] -> 0L diff --git a/v2v/target_RHEV.mli b/v2v/target_RHEV.mli new file mode 100644 index 0000000000..6cc5c76fe0 --- /dev/null +++ b/v2v/target_RHEV.mli @@ -0,0 +1,24 @@ +(* virt-v2v + * Copyright (C) 2009-2014 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + *) + +val initialize : verbose:bool -> string -> Types.source -> + [`Sparse | `Preallocated] -> Types.overlay list -> Types.overlay list + +val create_metadata : string -> [`Server | `Desktop] option -> Types.source -> + [`Sparse | `Preallocated] -> Types.overlay list -> Types.inspect -> + Types.guestcaps -> unit diff --git a/v2v/types.ml b/v2v/types.ml index 849ad0f1f0..05aafb1865 100644 --- a/v2v/types.ml +++ b/v2v/types.ml @@ -27,7 +27,7 @@ type input = type output = | OutputLibvirt of string option | OutputLocal of string -| OutputRHEV of string +| OutputRHEV of string * [`Server|`Desktop] option type source = { s_dom_type : string; @@ -83,6 +83,7 @@ type overlay = { ov_preallocation : string option; ov_source_file : string; ov_source_format : string option; + ov_vol_uuid : string; } let string_of_overlay ov = @@ -96,6 +97,7 @@ ov_virtual_size = %Ld ov_preallocation = %s ov_source_file = %s ov_source_format = %s +ov_vol_uuid = %s " ov.ov_overlay ov.ov_target_file ov.ov_target_file_tmp @@ -105,6 +107,7 @@ ov_source_format = %s (match ov.ov_preallocation with None -> "None" | Some s -> s) ov.ov_source_file (match ov.ov_source_format with None -> "None" | Some s -> s) + ov.ov_vol_uuid type inspect = { i_root : string; diff --git a/v2v/types.mli b/v2v/types.mli index c8ef5c3748..0415a27a9b 100644 --- a/v2v/types.mli +++ b/v2v/types.mli @@ -26,7 +26,7 @@ type input = type output = | OutputLibvirt of string option (* -o libvirt: -oc *) | OutputLocal of string (* -o local: directory *) -| OutputRHEV of string (* -o rhev: output storage *) +| OutputRHEV of string * [`Server|`Desktop] option (* -o rhev: output storage *) (** The output arguments as specified on the command line. *) type source = { @@ -62,8 +62,11 @@ type overlay = { (* Note: the next two fields are for information only and must not * be opened/copied/etc. *) - ov_source_file : string; (** qemu URI for source file. *) + ov_source_file : string; (** qemu URI for source file. *) ov_source_format : string option; (** Source file format, if known. *) + + (* Only used by RHEV. XXX Should be parameterized type. *) + ov_vol_uuid : string; (** RHEV volume UUID *) } (** Disk overlays and destination disks. *) diff --git a/v2v/utils.ml b/v2v/utils.ml index b58c18d852..f065a6617b 100644 --- a/v2v/utils.ml +++ b/v2v/utils.ml @@ -40,6 +40,12 @@ let xml_quote_attr str = let str = Common_utils.replace_str str ">" ">" in str +let xml_quote_pcdata str = + let str = Common_utils.replace_str str "&" "&" in + let str = Common_utils.replace_str str "<" "<" in + let str = Common_utils.replace_str str ">" ">" in + str + external drive_name : int -> string = "v2v_utils_drive_name" let compare_app2_versions app1 app2 = diff --git a/v2v/v2v.ml b/v2v/v2v.ml index 4dca2feb19..6f6ee930fa 100644 --- a/v2v/v2v.ml +++ b/v2v/v2v.ml @@ -93,7 +93,7 @@ let rec main () = * work. *) let overlays = - initialize_target g + initialize_target ~verbose g source output output_alloc output_format output_name overlays in (* Inspection - this also mounts up the filesystems. *) @@ -197,7 +197,9 @@ let rec main () = | OutputLibvirt oc -> assert false | OutputLocal dir -> Target_local.create_metadata dir renamed_source overlays guestcaps - | OutputRHEV os -> assert false in + | OutputRHEV (os, vmtype) -> + Target_RHEV.create_metadata os vmtype renamed_source output_alloc + overlays inspect guestcaps in (* If we wrote to a temporary file, rename to the real file. *) List.iter ( @@ -213,7 +215,7 @@ let rec main () = if debug_gc then Gc.compact () -and initialize_target g +and initialize_target ~verbose g source output output_alloc output_format output_name overlays = let overlays = mapi ( @@ -244,7 +246,8 @@ and initialize_target g ov_target_file = ""; ov_target_file_tmp = ""; ov_target_format = format; ov_sd = sd; ov_virtual_size = vsize; ov_preallocation = preallocation; - ov_source_file = qemu_uri; ov_source_format = backing_format; } + ov_source_file = qemu_uri; ov_source_format = backing_format; + ov_vol_uuid = "" } ) overlays in let overlays = let renamed_source = @@ -254,7 +257,8 @@ and initialize_target g match output with | OutputLibvirt oc -> assert false | OutputLocal dir -> Target_local.initialize dir renamed_source overlays - | OutputRHEV os -> assert false in + | OutputRHEV (os, _) -> + Target_RHEV.initialize ~verbose os renamed_source output_alloc overlays in overlays and inspect_source g root_choice = diff --git a/v2v/virt-v2v.pod b/v2v/virt-v2v.pod index 48fa870460..084e4b6abb 100644 --- a/v2v/virt-v2v.pod +++ b/v2v/virt-v2v.pod @@ -132,14 +132,22 @@ For I<-o libvirt>, this is a libvirt pool (see S>). For I<-o local>, this is a directory name. The directory must exist. -For I<-o rhev>, this is an NFS path of the form -ChostE:EpathE>, eg: +For I<-o rhev>, this can be an NFS path of the Export Storage Domain +of the form ChostE:EpathE>, eg: rhev-storage.example.com:/rhev/export The NFS export must be mountable and writable by the user and host running virt-v2v, since the virt-v2v program has to actually mount it -when it runs. +when it runs. So you probably have to run virt-v2v as C. + +B You can mount the Export Storage Domain yourself, and point +I<-os> to the mountpoint. Note that virt-v2v will still need to write +to this remote directory, so virt-v2v will still need to run as +C. + +You will get an error if virt-v2v is unable to mount/write to the +Export Storage Domain. =item B<-q> @@ -202,6 +210,13 @@ Enable verbose messages for debugging. Display version number and exit. +=item B<--vmtype> server|desktop + +For the RHEV target only, specify the type of guest. You can set this +to C or C. If the option is not given, then a +suitable default is chosen based on the detected guest operating +system. + =item B<-x> Enable tracing of libguestfs API calls.