import-to-ovirt script.
authorRichard W.M. Jones <rjones@redhat.com>
Wed, 16 Sep 2015 17:19:36 +0000 (18:19 +0100)
committerRichard W.M. Jones <rjones@redhat.com>
Thu, 17 Sep 2015 14:04:34 +0000 (15:04 +0100)
README
import-to-ovirt.pl [new file with mode: 0755]

diff --git a/README b/README
index b3c32b4..9b0a983 100644 (file)
--- a/README
+++ b/README
@@ -1,5 +1,20 @@
-This script lets you import any virtual machine disk image to RHEV
-or oVirt.
+This script lets you import a virtual machine disk image to RHEV or
+oVirt.
+
+NB. This script *requires* a 64 bit host.  Perl will cause silent
+overflows on 32 bit hosts.
+
+Requirements:
+
+    perl
+  * perl-Pod-Usage
+  * perl-Getopt-Long
+  * perl-File-Temp
+  * perl-POSIX
+    perl-XML-Writer
+    perl-Sys-Guestfs
+
+* = usually provided as part of base Perl package
 
 For instructions, read the script or do:
 
diff --git a/import-to-ovirt.pl b/import-to-ovirt.pl
new file mode 100755 (executable)
index 0000000..cb6c1db
--- /dev/null
@@ -0,0 +1,845 @@
+#!/usr/bin/perl -w
+# Copyright (C) 2015 Richard W.M. Jones <rjones@redhat.com>
+# Copyright (C) 2015 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.
+
+use warnings;
+use strict;
+use English;
+
+use Pod::Usage;
+use Getopt::Long;
+use File::Temp qw(tempdir);
+use POSIX qw(_exit strftime);
+use XML::Writer;
+
+use Sys::Guestfs;
+
+=head1 NAME
+
+import-to-ovirt.pl - Import virtual machine disk image to RHEV or oVirt
+
+=head1 SYNOPSIS
+
+ sudo ./import-to-ovirt.pl disk.img server:/esd
+
+ sudo ./import-to-ovirt.pl disk.img /esd_mountpoint
+
+=head1 IMPORTANT NOTE
+
+This tool should B<only> be used if the guest can already run on KVM.
+
+B<If you need to convert the guest from some foreign hypervisor, like VMware, Xen or Hyper-V, you should use L<virt-v2v(1)> instead.>
+
+=head1 EXAMPLES
+
+Import a KVM guest to the Export Storage Domain of your RHEV or oVirt
+system.  The NFS mount of the Export Storage Domain is C<server:/esd>.
+
+ sudo ./import-to-ovirt.pl disk.img server:/esd
+
+Import a KVM guest to an already-mounted Export Storage Domain:
+
+ sudo ./import-to-ovirt.pl disk.img /esd_mountpoint
+
+If the single guest has multiple disks, use:
+
+ sudo ./import-to-ovirt.pl disk1.img [disk2.img [...]] server:/esd
+
+If you are importing multiple guests then you must import each one
+separately.  Do not use multiple disk parameters if each disk comes
+from a different guest.
+
+=head1 DESCRIPTION
+
+This is a command line script for importing a KVM virtual machine to
+RHEV or oVirt.  The script assumes that the guest can already run on
+KVM, ie. that it was previously running on KVM and has the required
+drivers.  If the guest comes from a foreign hypervisor like VMware,
+Xen or Hyper-V, use L<virt-v2v(1)> instead.
+
+This script only imports the guest into the oVirt "Export Storage
+Domain" (ESD).  After the import is complete, you must then go to the
+oVirt user interface, go to the C<Storage> tab, select the right ESD,
+and use C<VM Import> to take the guest from the ESD to the data
+domain.  This process is outside the scope of this script, but could
+be automated using the oVirt API.
+
+=head2 Basic usage
+
+Basic usage is just:
+
+ ./import-to-ovirt.pl [list of disks] server:/esd
+
+If you are unclear about the C<server:/esd> parameter, go to the oVirt
+Storage tab, select the C<Domain Type> C<Export> and look in the
+C<General> tab under C<Path>.
+
+If the ESD is already mounted on your machine (or if you are using a
+non-NFS ESD), then you can supply a direct path to the mountpoint
+instead:
+
+ ./import-to-ovirt.pl [list of disks] /esd_mountpoint
+
+The list of disks should all belong to a single guest (most guests
+will only have a single disk).  If you want to import multiple guests,
+you must run the script multiple times.
+
+Importing from OVA etc is not supported.  Try C<ovirt-image-uploader>
+(if the OVA was exported from oVirt), or L<virt-v2v(1)> (if the OVA
+was exported from VMware).
+
+=head2 Permissions
+
+You probably need to run this script as root, because it has to create
+files on the ESD as a special C<vdsm> user (UID:GID C<36:36>).
+
+It may also be possible to run the script as the vdsm user.  But if
+you run it as some non-root, non-vdsm user, then oVirt won't be able
+to read the data from the ESD and will give an error.
+
+NFS "root squash" should be turned off on the NFS server, since it
+stops us from creating files as the vdsm user.  Also NFSv4 may not
+work unless you have set up idmap correctly (good luck!)
+
+=head2 Network card and disk model
+
+(See also L</TO DO> below)
+
+Currently this script doesn't add a network card to the guest.  You
+will need to add one yourself in the C<VM Import> tab when importing
+the guest.
+
+Similarly, the script always adds the disks as virtio-blk devices.  If
+the guest is expecting IDE, SCSI or virtio-scsi, you will need to
+change the disk type when importing the guest.
+
+=head1 OPTIONS
+
+=over 4
+
+=cut
+
+my $help;
+
+=item B<--help>
+
+Display brief help and exit.
+
+=cut
+
+my $man;
+
+=item B<--man>
+
+Display the manual page and exit.
+
+=cut
+
+my $memory_mb = 1024;
+
+=item B<--memory> MB
+
+Set the memory size I<in megabytes>.  The default is 1024.
+
+=cut
+
+my $name;
+
+=item B<--name> name
+
+Set the guest name.  If not present, a name is made up based on
+the filename of the first disk.
+
+=cut
+
+my $vcpus = 1;
+
+=item B<--vcpus> N
+
+Set the number of virtual CPUs.  The default is 1.
+
+=cut
+
+my $vmtype = "Desktop";
+
+=item B<--vmtype> Desktop
+
+=item B<--vmtype> Server
+
+Set the VmType field in the OVF.  It must be C<Desktop> or
+C<Server>.  The default is C<Desktop>.
+
+=cut
+
+=back
+
+=cut
+
+$| = 1;
+
+GetOptions ("help|?" => \$help,
+            "man" => \$man,
+            "memory=i" => \$memory_mb,
+            "name=s" => \$name,
+            "vmtype=s" => \$vmtype,
+    )
+    or die "$0: unknown command line option\n";
+
+pod2usage (1) if $help;
+pod2usage (-exitval => 0, -verbose => 2) if $man;
+
+# Get the parameters.
+if (@ARGV < 2) {
+    die "Use '$0 --man' to display the manual.\n"
+}
+
+my @disks = @ARGV[0 .. $#ARGV-1];
+my $output = $ARGV[$#ARGV];
+
+if (!defined $name) {
+    $name = $disks[0];
+    $name =~ s{.*/}{};
+    $name =~ s{\.[^.]+}{};
+}
+
+# Open the guest in libguestfs so we can inspect it.
+my $g = Sys::Guestfs->new ();
+$g->add_drive_opts ($_, readonly => 1) foreach (@disks);
+$g->launch ();
+my @roots = $g->inspect_os ();
+if (@roots == 0) {
+    die "$0: no operating system was found on the disk\n"
+}
+if (@roots > 1) {
+    die "$0: either this is a multi-OS disk, or you passed multiple unrelated guest disks on the command line\n"
+}
+my $root = $roots[0];
+
+# Save the inspection data.
+my $type = $g->inspect_get_type ($root);
+my $distro = $g->inspect_get_distro ($root);
+my $arch = $g->inspect_get_arch ($root);
+my $major_version = $g->inspect_get_major_version ($root);
+my $minor_version = $g->inspect_get_major_version ($root);
+my $product_name = $g->inspect_get_product_name ($root);
+my $product_variant = $g->inspect_get_product_variant ($root);
+
+# Get the virtual size of each disk.
+my @virtual_sizes;
+foreach (@disks) {
+    push @virtual_sizes, $g->disk_virtual_size ($_);
+}
+
+$g->close ();
+
+# Map inspection data to RHEV ostype.
+my $ostype;
+if ($type eq "linux" && $distro eq "rhel" && $major_version <= 6) {
+    if ($arch eq "x86_64") {
+        $ostype = "RHEL${major_version}x64"
+    } else {
+        $ostype = "RHEL$major_version"
+    }
+}
+elsif ($type eq "linux" && $distro eq "rhel") {
+    if ($arch eq "x86_64") {
+        $ostype = "rhel_${major_version}x64"
+    } else {
+        $ostype = "rhel_$major_version"
+    }
+}
+elsif ($type eq "linux") {
+    $ostype = "OtherLinux"
+}
+elsif ($type eq "windows" && $major_version == 5 && $minor_version == 1) {
+    $ostype = "WindowsXP"
+}
+elsif ($type eq "windows" && $major_version == 5 && $minor_version == 2) {
+    if ($product_name =~ /XP/) {
+        $ostype = "WindowsXP"
+    } elsif ($arch eq "x86_64") {
+        $ostype = "Windows2003x64"
+    } else {
+        $ostype = "Windows2003"
+    }
+}
+elsif ($type eq "windows" && $major_version == 6 && $minor_version == 0) {
+    if ($arch eq "x86_64") {
+        $ostype = "Windows2008x64"
+    } else {
+        $ostype = "Windows2008"
+    }
+}
+elsif ($type eq "windows" && $major_version == 6 && $minor_version == 1) {
+    if ($product_variant eq "Client") {
+        if ($arch eq "x86_64") {
+            $ostype = "Windows7x64"
+        } else {
+            $ostype = "Windows7"
+        }
+    } else {
+        $ostype = "Windows2008R2x64"
+    }
+}
+elsif ($type eq "windows" && $major_version == 6 && $minor_version == 2) {
+   if ($product_variant eq "Client") {
+       if ($arch eq "x86_64") {
+           $ostype = "windows_8x64"
+       } else {
+           $ostype = "windows_8"
+       }
+   } else {
+       $ostype = "windows_2012x64"
+   }
+}
+elsif ($type eq "windows" && $major_version == 6 && $minor_version == 3) {
+    $ostype = "windows_2012R2x64"
+}
+else {
+    $ostype = "Unassigned"
+}
+
+# Mount the ESD if needed (or just check it exists).
+my $mountpoint;
+if (-d $output) {
+    $mountpoint = $output;
+} elsif ($output =~ m{^.*:.*$}) {
+    my $umount;
+    $umount = $mountpoint = tempdir (CLEANUP => 1);
+    system ("mount", "-t", "nfs", $output, $mountpoint) == 0
+        or die "$0: mount $output failed: $?\n";
+    END { system ("umount", $umount) if defined $umount }
+} else {
+    die "$0: ESD $output is not a directory or an NFS mountpoint\n"
+}
+
+# Check the ESD looks like an ESD.
+my @entries = <$mountpoint/*>;
+@entries =
+    grep { m{/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$}i }
+    @entries;
+if (@entries == 0) {
+    die "$0: does $output really point to an oVirt Export Storage Domain?\n"
+}
+if (@entries > 1) {
+    die "$0: multiple GUIDs found in oVirt Export Storage Domain\n"
+}
+my $esd_uuid_dir = $entries[0];
+my $esd_uuid = $esd_uuid_dir;
+$esd_uuid =~ s{.*/}{};
+
+if (! -d $esd_uuid_dir ||
+    ! -d "$esd_uuid_dir/images" ||
+    ! -d "$esd_uuid_dir/master" ||
+    ! -d "$esd_uuid_dir/master/vms") {
+    die "$0: $output doesn't look like an Export Storage Domain\n"
+}
+
+# Start the import.
+print "Importing $product_name to $output...\n";
+
+# A helper function that forks and runs some code / a command as
+# an alternate UID:GID.
+sub run_as_vdsm
+{
+    my $fn = shift;
+
+    my $pid = fork ();
+    die "fork: $!" unless defined $pid;
+    if ($pid == 0) {
+        # Child process.
+        if ($EUID == 0) {
+            $GID = 36;
+            $UID = 36;
+        }
+        eval { &$fn () };
+        if ($@) {
+            print STDERR $@, "\n";
+            _exit (1);
+        }
+        _exit (0);
+    }
+    waitpid ($pid, 0) or die "waitpid: $!";
+    if ($? != 0) {
+        die "$0: run_as_vdsm: child process failed (status $?)\n";
+    }
+}
+
+# Generate a UUID.
+sub uuidgen
+{
+    local $_ = `uuidgen -r`;
+    chomp;
+    die unless length $_ >= 30; # Sanity check.
+    $_;
+}
+
+# Generate some random UUIDs.
+my $vm_uuid = uuidgen ();
+my @image_uuids;
+foreach (@disks) {
+    push @image_uuids, uuidgen ();
+}
+my @vol_uuids;
+foreach (@disks) {
+    push @vol_uuids, uuidgen ();
+}
+
+# Make sure the output is deleted on unsuccessful exit.  We set
+# $delete_output_on_exit to false at the end of the script.
+my $delete_output_on_exit = 1;
+END {
+    if ($delete_output_on_exit) {
+        # Can't use run_as_vdsm in an END{} block.
+        foreach (@image_uuids) {
+            system ("rm", "-rf", "$esd_uuid_dir/images/$_");
+        }
+        system ("rm", "-rf", "$esd_uuid_dir/master/vms/$vm_uuid");
+    }
+};
+
+# Copy and convert the disk images.
+my $i;
+my $time = time ();
+my $iso_time = strftime ("%Y/%m/%d %H:%M:%S", gmtime ());
+my $imported_by = "Imported by import-to-ovirt.pl";
+my @real_sizes;
+
+for ($i = 0; $i < @disks; ++$i) {
+    my $input_file = $disks[$i];
+    my $image_uuid = $image_uuids[$i];
+    run_as_vdsm (sub {
+        my $path = "$esd_uuid_dir/images/$image_uuid";
+        mkdir ($path, 0755) or die "mkdir: $path: $!";
+    });
+    my $output_file = "$esd_uuid_dir/images/$image_uuid/".$vol_uuids[$i];
+    run_as_vdsm (sub {
+        open (my $fh, ">", $output_file) or die "open: $output_file: $!";
+        # Well done NFS root_squash, you make the world less secure.
+        chmod (0666, $output_file) or die "chmod: $output_file: $!";
+    });
+    print "Copying $input_file ...\n";
+#    system ("qemu-img", "convert", "-p",
+#            $input_file,
+#            "-O", "qcow2",
+#            "-o", "compat=0.10", # for RHEL 6-based ovirt nodes
+#            $output_file) == 0
+#                or die "qemu-img: $input_file: failed (status $?)";
+#    push @real_sizes, -s $output_file;
+    push @real_sizes, 0;
+
+    my $size_in_sectors = $virtual_sizes[$i] / 512;
+
+    # Create .meta files per disk.
+    my $meta = <<"EOF";
+DOMAIN=$esd_uuid
+VOLTYPE=LEAF
+CTIME=$time
+MTIME=$time
+IMAGE=$image_uuid
+DISKTYPE=1
+PUUID=00000000-0000-0000-0000-000000000000
+LEGALITY=LEGAL
+POOL_UUID=
+SIZE=$size_in_sectors
+FORMAT=COW
+TYPE=SPARSE
+DESCRIPTION=$imported_by
+EOF
+    my $meta_file = $output_file . ".meta";
+    run_as_vdsm (sub {
+        open (my $fh, ">", $meta_file) or die "open: $meta_file: $!";
+        print $fh $meta
+    });
+}
+
+# Create the OVF.
+print "Creating OVF metadata ...\n";
+
+my $rasd_ns = "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData";
+my $vssd_ns = "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData";
+my $xsi_ns = "http://www.w3.org/2001/XMLSchema-instance";
+my $ovf_ns = "http://schemas.dmtf.org/ovf/envelope/1/";
+my %prefix_map = (
+    $rasd_ns => "rasd",
+    $vssd_ns => "vssd",
+    $xsi_ns => "xsi",
+    $ovf_ns => "ovf",
+);
+my @forced_ns_decls = keys %prefix_map;
+
+my $w = XML::Writer->new (
+    OUTPUT => "self",
+    NAMESPACES => 1,
+    PREFIX_MAP => \%prefix_map,
+    FORCED_NS_DECLS => \@forced_ns_decls,
+    DATA_MODE => 1,
+    DATA_INDENT => 4,
+);
+
+$w->startTag ([$ovf_ns, "Envelope"],
+              [$ovf_ns, "version"] => "0.9");
+$w->comment ($imported_by);
+
+$w->startTag ("References");
+
+for ($i = 0; $i < @disks; ++$i)
+{
+    my $href = $image_uuids[$i] . "/" . $vol_uuids[$i];
+    $w->startTag ("File",
+                  [$ovf_ns, "href"] => $href,
+                  [$ovf_ns, "id"] => $vol_uuids[$i],
+                  [$ovf_ns, "size"] => $virtual_sizes[$i],
+                  [$ovf_ns, "description"] => $imported_by);
+    $w->endTag ();
+}
+
+$w->endTag ();
+
+$w->startTag ("Section",
+              [$xsi_ns, "type"] => "ovf:NetworkSection_Type");
+$w->startTag ("Info");
+$w->characters ("List of networks");
+$w->endTag ();
+$w->endTag ();
+
+$w->startTag ("Section",
+              [$xsi_ns, "type"] => "ovf:DiskSection_Type");
+$w->startTag ("Info");
+$w->characters ("List of Virtual Disks");
+$w->endTag ();
+
+for ($i = 0; $i < @disks; ++$i)
+{
+    my $virtual_size_in_gb = $virtual_sizes[$i];
+    $virtual_size_in_gb /= 1024;
+    $virtual_size_in_gb /= 1024;
+    $virtual_size_in_gb /= 1024;
+    my $real_size_in_gb = $real_sizes[$i];
+    $real_size_in_gb /= 1024;
+    $real_size_in_gb /= 1024;
+    $real_size_in_gb /= 1024;
+    my $href = $image_uuids[$i] . "/" . $vol_uuids[$i];
+
+    my $boot_drive;
+    if ($i == 0) {
+        $boot_drive = "True";
+    } else {
+        $boot_drive = "False";
+    }
+
+    $w->startTag ("Disk",
+                  [$ovf_ns, "diskId" ] => $vol_uuids[$i],
+                  [$ovf_ns, "actual_size"] => $real_size_in_gb,
+                  [$ovf_ns, "size"] => $virtual_size_in_gb,
+                  [$ovf_ns, "fileRef"] => $href,
+                  [$ovf_ns, "parentRef"] => "",
+                  [$ovf_ns, "vm_snapshot_id"] => uuidgen (),
+                  [$ovf_ns, "volume-format"] => "COW",
+                  [$ovf_ns, "volume-type"] => "Sparse",
+                  [$ovf_ns, "format"] => "http://en.wikipedia.org/wiki/Byte",
+                  [$ovf_ns, "disk-interface"] => "VirtIO",
+                  [$ovf_ns, "disk-type"] => "System",
+                  [$ovf_ns, "boot"] => $boot_drive);
+    $w->endTag ();
+}
+
+$w->endTag ();
+
+$w->startTag ("Content",
+              [$ovf_ns, "id"] => "out",
+              [$xsi_ns, "type"] => "ovf:VirtualSystem_Type");
+$w->startTag ("Name");
+$w->characters ($name);
+$w->endTag ();
+$w->startTag ("TemplateId");
+$w->characters ("00000000-0000-0000-0000-000000000000");
+$w->endTag ();
+$w->startTag ("TemplateName");
+$w->characters ("Blank");
+$w->endTag ();
+$w->startTag ("Description");
+$w->characters ($imported_by);
+$w->endTag ();
+$w->startTag ("Domain");
+$w->endTag ();
+$w->startTag ("CreationDate");
+$w->characters ($iso_time);
+$w->endTag ();
+$w->startTag ("IsInitilized"); # sic
+$w->characters ("True");
+$w->endTag ();
+$w->startTag ("IsAutoSuspend");
+$w->characters ("False");
+$w->endTag ();
+$w->startTag ("TimeZone");
+$w->endTag ();
+$w->startTag ("IsStateless");
+$w->characters ("False");
+$w->endTag ();
+$w->startTag ("Origin");
+$w->characters ("0");
+$w->endTag ();
+$w->startTag ("VmType");
+$w->characters ($vmtype);
+$w->endTag ();
+$w->startTag ("DefaultDisplayType");
+$w->characters ("1"); # qxl
+$w->endTag ();
+
+$w->startTag ("Section",
+              [$ovf_ns, "id"] => $vm_uuid,
+              [$ovf_ns, "required"] => "false",
+              [$xsi_ns, "type"] => "ovf:OperatingSystemSection_Type");
+$w->startTag ("Info");
+$w->characters ($product_name);
+$w->endTag ();
+$w->startTag ("Description");
+$w->characters ($ostype);
+$w->endTag ();
+$w->endTag ();
+
+$w->startTag ("Section",
+              [$xsi_ns, "type"] => "ovf:VirtualHardwareSection_Type");
+$w->startTag ("Info");
+$w->characters (sprintf ("%d CPU, %d Memory", $vcpus, $memory_mb));
+$w->endTag ();
+
+$w->startTag ("Item");
+$w->startTag ([$rasd_ns, "Caption"]);
+$w->characters (sprintf ("%d virtual cpu", $vcpus));
+$w->endTag ();
+$w->startTag ([$rasd_ns, "Description"]);
+$w->characters ("Number of virtual CPU");
+$w->endTag ();
+$w->startTag ([$rasd_ns, "InstanceId"]);
+$w->characters ("1");
+$w->endTag ();
+$w->startTag ([$rasd_ns, "ResourceType"]);
+$w->characters ("3");
+$w->endTag ();
+$w->startTag ([$rasd_ns, "num_of_sockets"]);
+$w->characters ($vcpus);
+$w->endTag ();
+$w->startTag ([$rasd_ns, "cpu_per_socket"]);
+$w->characters (1);
+$w->endTag ();
+$w->endTag ("Item");
+
+$w->startTag ("Item");
+$w->startTag ([$rasd_ns, "Caption"]);
+$w->characters (sprintf ("%d MB of memory", $memory_mb));
+$w->endTag ();
+$w->startTag ([$rasd_ns, "Description"]);
+$w->characters ("Memory Size");
+$w->endTag ();
+$w->startTag ([$rasd_ns, "InstanceId"]);
+$w->characters ("2");
+$w->endTag ();
+$w->startTag ([$rasd_ns, "ResourceType"]);
+$w->characters ("4");
+$w->endTag ();
+$w->startTag ([$rasd_ns, "AllocationUnits"]);
+$w->characters ("MegaBytes");
+$w->endTag ();
+$w->startTag ([$rasd_ns, "VirtualQuantity"]);
+$w->characters ($memory_mb);
+$w->endTag ();
+$w->endTag ("Item");
+
+$w->startTag ("Item");
+$w->startTag ([$rasd_ns, "Caption"]);
+$w->characters ("USB Controller");
+$w->endTag ();
+$w->startTag ([$rasd_ns, "InstanceId"]);
+$w->characters ("3");
+$w->endTag ();
+$w->startTag ([$rasd_ns, "ResourceType"]);
+$w->characters ("23");
+$w->endTag ();
+$w->startTag ([$rasd_ns, "UsbPolicy"]);
+$w->characters ("Disabled");
+$w->endTag ();
+$w->endTag ("Item");
+
+$w->startTag ("Item");
+$w->startTag ([$rasd_ns, "Caption"]);
+$w->characters ("Graphical Controller");
+$w->endTag ();
+$w->startTag ([$rasd_ns, "InstanceId"]);
+$w->characters (uuidgen ());
+$w->endTag ();
+$w->startTag ([$rasd_ns, "ResourceType"]);
+$w->characters ("20");
+$w->endTag ();
+$w->startTag ("Type");
+$w->characters ("video");
+$w->endTag ();
+$w->startTag ([$rasd_ns, "VirtualQuantity"]);
+$w->characters ("1");
+$w->endTag ();
+$w->startTag ([$rasd_ns, "Device"]);
+$w->characters ("qxl");
+$w->endTag ();
+$w->endTag ("Item");
+
+for ($i = 0; $i < @disks; ++$i)
+{
+    my $href = $image_uuids[$i] . "/" . $vol_uuids[$i];
+
+    $w->startTag ("Item");
+
+    $w->startTag ([$rasd_ns, "Caption"]);
+    $w->characters ("Drive " . ($i+1));
+    $w->endTag ();
+    $w->startTag ([$rasd_ns, "InstanceId"]);
+    $w->characters ($vol_uuids[$i]);
+    $w->endTag ();
+    $w->startTag ([$rasd_ns, "ResourceType"]);
+    $w->characters ("17");
+    $w->endTag ();
+    $w->startTag ("Type");
+    $w->characters ("disk");
+    $w->endTag ();
+    $w->startTag ([$rasd_ns, "HostResource"]);
+    $w->characters ($href);
+    $w->endTag ();
+    $w->startTag ([$rasd_ns, "Parent"]);
+    $w->characters ("00000000-0000-0000-0000-000000000000");
+    $w->endTag ();
+    $w->startTag ([$rasd_ns, "Template"]);
+    $w->characters ("00000000-0000-0000-0000-000000000000");
+    $w->endTag ();
+    $w->startTag ([$rasd_ns, "ApplicationList"]);
+    $w->endTag ();
+    $w->startTag ([$rasd_ns, "StorageId"]);
+    $w->characters ($esd_uuid);
+    $w->endTag ();
+    $w->startTag ([$rasd_ns, "StoragePoolId"]);
+    $w->characters ("00000000-0000-0000-0000-000000000000");
+    $w->endTag ();
+    $w->startTag ([$rasd_ns, "CreationDate"]);
+    $w->characters ($iso_time);
+    $w->endTag ();
+    $w->startTag ([$rasd_ns, "LastModified"]);
+    $w->characters ($iso_time);
+    $w->endTag ();
+    $w->startTag ([$rasd_ns, "last_modified_date"]);
+    $w->characters ($iso_time);
+    $w->endTag ();
+
+    $w->endTag ("Item");
+}
+
+$w->endTag ("Section"); # ovf:VirtualHardwareSection_Type
+
+$w->endTag ("Content");
+
+$w->endTag ([$ovf_ns, "Envelope"]);
+$w->end ();
+
+my $ovf = $w->to_string;
+
+#print "OVF:\n$ovf\n";
+
+my $ovf_dir = "$esd_uuid_dir/master/vms/$vm_uuid";
+run_as_vdsm (sub {
+    mkdir ($ovf_dir, 0755) or die "mkdir: $ovf_dir: $!";
+});
+my $ovf_file = "$ovf_dir/$vm_uuid.ovf";
+run_as_vdsm (sub {
+    open (my $fh, ">", $ovf_file) or die "open: $ovf_file: $!";
+    print $fh $ovf
+});
+
+# Finished.
+#$delete_output_on_exit = 0;
+print "\n";
+print "OVF written to $ovf_file\n";
+print "\n";
+print "Import finished without errors.  Now go to the Storage tab ->\n";
+print "Export Storage Domain -> VM Import, and import the guest.\n";
+exit 0;
+
+__END__
+
+=head1 TO DO
+
+=over 4
+
+=item Network
+
+Add a network card to the OVF.  The problem is detecting what
+network devices the guest can support.
+
+=item Disk model
+
+Detect what disk models (eg. IDE, virtio-blk, virtio-scsi) the
+guest can support and add the correct type of disk.
+
+=back
+
+=head1 DEBUGGING IMPORT FAILURES
+
+When you export to the ESD, and then import that guest through the
+oVirt / RHEV-M UI, you may encounter an import failure.  Diagnosing
+these failures is infuriatingly difficult as the UI generally hides
+the true reason for the failure.
+
+There are two log files of interest.  The first is stored on the oVirt
+engine / RHEV-M server itself, and is called
+F</var/log/ovirt-engine/engine.log>
+
+The second file, which is the most useful, is found on the SPM host
+(SPM stands for "Storage Pool Manager").  This is a oVirt node that is
+elected to do all metadata modifications in the data center, such as
+image or snapshot creation.  You can find out which host is the
+current SPM from the "Hosts" tab "Spm Status" column.  Once you have
+located the SPM, log into it and grab the file
+F</var/log/vdsm/vdsm.log> which will contain detailed error messages
+from low-level commands.
+
+=head1 SEE ALSO
+
+L<virt-v2v(1)>,
+L<engine-image-uploader(8)>.
+
+=head1 AUTHOR
+
+Richard W.M. Jones <rjones@redhat.com>
+
+=head1 COPYRIGHT
+
+Copyright (C) 2015 Richard W.M. Jones <rjones@redhat.com>
+
+Copyright (C) 2015 Red Hat Inc.
+
+=head1 LICENSE
+
+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.