[Libguestfs] [PREVIEW ONLY] Refactor data transfer code

Matthew Booth mbooth at redhat.com
Tue Sep 21 12:40:11 UTC 2010


This patch refactors the data transfer code with several goals:

* Have a common read(source)/write(target) loop so that common processing can
  happen in the middle of it, e.g. format change/progress bar
* Provide volume metadata to transfers to allow smarter reading/writing, e.g. of
  sparse files
* Simplify the data transfer code

The patch *isn't* NFC because there are some minor behaviour changes, but it
isn't intended to provide any new features either. For example, although sparse
info is passed around, nothing actually uses it yet.

This patch *may* allow qcow2->qcow2 conversions, although I haven't tested this.
A later patch will enable qcow2->raw and raw->qcow2 conversions.

Metadata sources where previously 'Connection's. Targets now become
'Connection's too. They both supply Volume objects. New objects are:

Volume
  Holds metadata on a specific object, and a handle to a Transfer object which
  can read/write it.

Transfer
  Provides a read or write stream to an underlying volume.
  This will also be extended to provide a file interface.

ReadStream/WriteStream
  Provides read/write streaming access to a volume's data.
---
 MANIFEST                                           |   12 +-
 lib/Sys/VirtV2V/Connection/LibVirt.pm              |  193 ++------
 lib/Sys/VirtV2V/Connection/LibVirtSource.pm        |  247 +++++++++
 .../LibVirt.pm => Connection/LibVirtTarget.pm}     |  228 +++------
 .../{LibVirtXML.pm => LibVirtXMLSource.pm}         |   81 ++-
 .../{Target/RHEV.pm => Connection/RHEVTarget.pm}   |  520 +++++++++++--------
 .../{Connection.pm => Connection/Source.pm}        |  116 ++++--
 lib/Sys/VirtV2V/Connection/Volume.pm               |  179 +++++++
 lib/Sys/VirtV2V/Transfer/ESX.pm                    |  533 ++++++++++++--------
 lib/Sys/VirtV2V/Transfer/Local.pm                  |  238 +++++++++
 lib/Sys/VirtV2V/Transfer/LocalCopy.pm              |  150 ------
 lib/Sys/VirtV2V/Transfer/SSH.pm                    |  396 ++++++++++-----
 lib/Sys/VirtV2V/Util.pm                            |   41 ++-
 v2v/virt-v2v.pl                                    |   92 ++--
 14 files changed, 1905 insertions(+), 1121 deletions(-)
 create mode 100644 lib/Sys/VirtV2V/Connection/LibVirtSource.pm
 rename lib/Sys/VirtV2V/{Target/LibVirt.pm => Connection/LibVirtTarget.pm} (70%)
 rename lib/Sys/VirtV2V/Connection/{LibVirtXML.pm => LibVirtXMLSource.pm} (57%)
 rename lib/Sys/VirtV2V/{Target/RHEV.pm => Connection/RHEVTarget.pm} (75%)
 rename lib/Sys/VirtV2V/{Connection.pm => Connection/Source.pm} (60%)
 create mode 100644 lib/Sys/VirtV2V/Connection/Volume.pm
 create mode 100644 lib/Sys/VirtV2V/Transfer/Local.pm
 delete mode 100644 lib/Sys/VirtV2V/Transfer/LocalCopy.pm

diff --git a/MANIFEST b/MANIFEST
index 4c8c5c2..a5c72be 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -5,8 +5,12 @@ COPYING
 COPYING.LIB
 lib/Sys/VirtV2V/Config.pm
 lib/Sys/VirtV2V/Connection/LibVirt.pm
-lib/Sys/VirtV2V/Connection/LibVirtXML.pm
-lib/Sys/VirtV2V/Connection.pm
+lib/Sys/VirtV2V/Connection/LibVirtSource.pm
+lib/Sys/VirtV2V/Connection/LibVirtTarget.pm
+lib/Sys/VirtV2V/Connection/LibVirtXMLSource.pm
+lib/Sys/VirtV2V/Connection/RHEVTarget.pm
+lib/Sys/VirtV2V/Connection/Source.pm
+lib/Sys/VirtV2V/Connection/Volume.pm
 lib/Sys/VirtV2V/Converter/Linux.pm
 lib/Sys/VirtV2V/Converter.pm
 lib/Sys/VirtV2V/Converter/Windows.pm
@@ -15,10 +19,8 @@ lib/Sys/VirtV2V/GuestfsHandle.pm
 lib/Sys/VirtV2V/GuestOS.pm
 lib/Sys/VirtV2V/GuestOS/RedHat.pm
 lib/Sys/VirtV2V.pm
-lib/Sys/VirtV2V/Target/LibVirt.pm
-lib/Sys/VirtV2V/Target/RHEV.pm
 lib/Sys/VirtV2V/Transfer/ESX.pm
-lib/Sys/VirtV2V/Transfer/LocalCopy.pm
+lib/Sys/VirtV2V/Transfer/Local.pm
 lib/Sys/VirtV2V/Transfer/SSH.pm
 lib/Sys/VirtV2V/Util.pm
 MANIFEST.SKIP
diff --git a/lib/Sys/VirtV2V/Connection/LibVirt.pm b/lib/Sys/VirtV2V/Connection/LibVirt.pm
index 51331da..7d634ff 100644
--- a/lib/Sys/VirtV2V/Connection/LibVirt.pm
+++ b/lib/Sys/VirtV2V/Connection/LibVirt.pm
@@ -1,5 +1,5 @@
 # Sys::VirtV2V::Connection::LibVirt
-# Copyright (C) 2009 Red Hat Inc.
+# Copyright (C) 2009,2010 Red Hat Inc.
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -20,8 +20,6 @@ package Sys::VirtV2V::Connection::LibVirt;
 use strict;
 use warnings;
 
-use Sys::VirtV2V::Connection;
-
 use Net::Netrc;
 use URI;
 use XML::DOM;
@@ -29,6 +27,11 @@ use XML::DOM;
 use Sys::Virt;
 
 use Sys::VirtV2V;
+use Sys::VirtV2V::Connection;
+use Sys::VirtV2V::Connection::Volume;
+use Sys::VirtV2V::Transfer::ESX;
+use Sys::VirtV2V::Transfer::SSH;
+use Sys::VirtV2V::Transfer::Local;
 use Sys::VirtV2V::Util qw(user_message);
 
 use Locale::TextDomain 'virt-v2v';
@@ -39,70 +42,47 @@ use Locale::TextDomain 'virt-v2v';
 
 =head1 NAME
 
-Sys::VirtV2V::Connection::LibVirt - Read libvirt metadata from libvirtd
-
-=head1 SYNOPSIS
-
- use Sys::VirtV2V::Connection::LibVirt;
-
- $conn = Sys::VirtV2V::Connection::LibVirt->new
-    ("xen+ssh://xenserver.example.com/", $name, $target);
- $dom = $conn->get_dom();
+Sys::VirtV2V::Connection::LibVirt - Access storage and metadata from libvirt
 
 =head1 DESCRIPTION
 
-Sys::VirtV2V::Connection::LibVirt is an implementation of
-Sys::VirtV2V::Connection which reads a guest's libvirt XML directly from a
-libvirt connection.
-
-=head1 METHODS
-
-=over
-
-=item new(uri, name, target)
-
-Create a new Sys::VirtV2V::Connection::LibVirt. Domain I<name> will be
-obtained from I<uri>. Remote storage will be create on I<target>.
+Do not use C<Sys::VirtV2V::Connection::LibVirt> directly. Instead use either
+C<Sys::VirtV2V::Connection::LibVirtSource> or
+C<Sys::VirtV2V::Connection::LibVirtTarget>.
 
 =cut
 
-sub new
+sub _libvirt_new
 {
     my $class = shift;
-
-    my ($uri, $name, $target) = @_;
+    my ($uri) = @_;
 
     my $self = {};
-
     bless($self, $class);
 
-    $self->{uri} = URI->new($uri);
-    $self->{name} = $name;
-
-    # Check that the guest doesn't already exist on the target
-    die(user_message(__x("Domain {name} already exists on the target.",
-                         name => $name))) if ($target->guest_exists($name));
+    $self->{uri} = $uri = URI->new($uri);
 
     # Parse uri authority for hostname and username
-    $self->{uri}->authority() =~ /^(?:([^:]*)(?::([^@]*))?@)?(.*)$/
+    $uri->authority() =~ /^(?:([^:]*)(?::([^@]*))?@)?(.*)$/
         or die(user_message(__x("Unable to parse URI authority: {auth}",
-                                auth => $self->{uri}->authority())));
-
-    my $username = $self->{username} = $1;
-    my $hostname = $self->{hostname} = $3;
+                                auth => $uri->authority())));
 
     warn user_message(__"WARNING: Specifying a password in the connection URI ".
-                        "is not supported. It has been ignored.") if (defined($2));
+                        "is not supported. It has been ignored.")
+        if (defined($2));
+
+    $self->{username} = $1;
+    $self->{hostname} = $3;
 
     # Look for credentials in .netrc if the URI contains a hostname
-    if (defined($hostname)) {
-        if (defined($username)) {
-            my $mach = Net::Netrc->lookup($hostname, $username);
+    if (defined($self->{hostname})) {
+        if (defined($self->{username})) {
+            my $mach = Net::Netrc->lookup($self->{hostname}, $self->{username});
             $self->{password} = $mach->password if (defined($mach));
         }
 
         else {
-            my $mach = Net::Netrc->lookup($hostname);
+            my $mach = Net::Netrc->lookup($self->{hostname});
 
             if (defined($mach)) {
                 $self->{username} = $mach->login;
@@ -111,11 +91,10 @@ sub new
         }
     }
 
-    my $sourcevmm;
+    my $vmm;
     eval {
-        $sourcevmm = Sys::Virt->new(
+        $vmm = Sys::Virt->new(
             uri => $uri,
-            readonly => 1,
             auth => 1,
             credlist => [
                 Sys::Virt::CRED_AUTHNAME,
@@ -142,117 +121,41 @@ sub new
                          uri => $uri,
                          error => $@->stringify()))) if ($@);
 
-    $self->{sourcevmm} = $sourcevmm;
-
-    $self->_check_shutdown();
-
-    $self->_get_dom();
-
-    my $transfer;
-    if ($self->{uri}->scheme eq "esx") {
-        $transfer = "Sys::VirtV2V::Transfer::ESX";
-    }
-
-    elsif ($self->{uri}->scheme =~ /\+ssh$/) {
-        $transfer = "Sys::VirtV2V::Transfer::SSH";
-    }
-
-    # Default to LocalCopy
-    # XXX: Need transfer methods for remote libvirt connections, e.g. scp
-    else {
-        $transfer = "Sys::VirtV2V::Transfer::LocalCopy";
-    }
-
-    $self->_storage_iterate($transfer, $target);
+    $self->{vmm} = $vmm;
 
     return $self;
 }
 
-sub _check_shutdown
+sub _get_transfer
 {
     my $self = shift;
+    my ($path, $is_sparse) = @_;
 
-    my $vmm = $self->{vmm};
-    my $domain = $self->_get_domain();
+    my $uri = $self->{uri};
 
-    # Check the domain is shutdown
-    die(user_message(__x("Guest {name} is currently {state}. It must be ".
-                         "shut down first.",
-                         state => _state_string($domain->get_info()->{state}),
-                         name => $self->{name})))
-        unless ($domain->get_info()->{state} ==
-                Sys::Virt::Domain::STATE_SHUTOFF);
-}
+    if ($uri->scheme eq "esx") {
+        my %query = $uri->query_form;
+        my $noverify = $query{no_verify} eq "1" ? 1 : 0;
 
-sub _state_string
-{
-    my ($state) = @_;
-
-    if ($state == Sys::Virt::Domain::STATE_NOSTATE) {
-        return __"idle";
-    } elsif ($state == Sys::Virt::Domain::STATE_RUNNING) {
-        return __"running";
-    } elsif ($state == Sys::Virt::Domain::STATE_BLOCKED) {
-        return __"blocked";
-    } elsif ($state == Sys::Virt::Domain::STATE_PAUSED) {
-        return __"paused";
-    } elsif ($state == Sys::Virt::Domain::STATE_SHUTDOWN) {
-        return __"shutting down";
-    } elsif ($state == Sys::Virt::Domain::STATE_SHUTOFF) {
-        return __"shut down";
-    } elsif ($state == Sys::Virt::Domain::STATE_CRASHED) {
-        return __"crashed";
-    } else {
-        return "unknown state ($state)";
+        return new Sys::VirtV2V::Transfer::ESX($path,
+                                               $self->{hostname},
+                                               $self->{username},
+                                               $self->{password},
+                                               $noverify,
+                                               $is_sparse);
     }
-}
-
-sub _get_domain
-{
-    my $self = shift;
-
-    return $self->{domain} if (defined($self->{domain}));
-
-    my $vmm = $self->{sourcevmm};
-    my $name = $self->{name};
-
-    # Lookup the domain
-    my $domain;
-    eval {
-        $domain = $vmm->get_domain_by_name($name);
-    };
-    die($@) if ($@);
-
-    # Check we found it
-    die(user_message(__x("{name} isn't a valid guest name", name => $name)))
-        unless($domain);
 
-    $self->{domain} = $domain;
-
-    return $domain;
-}
-
-sub _get_dom
-{
-    my $self = shift;
-
-    my $vmm = $self->{vmm};
-    my $name = $self->{name};
-
-    # Lookup the domain
-    my $domain = $self->_get_domain();
-
-    # Warn and exit if we didn't find it
-    return undef unless(defined($domain));
-
-    my $xml = $domain->get_xml_description();
+    elsif ($uri->scheme =~ /\+ssh$/) {
+        return new Sys::VirtV2V::Transfer::SSH($path,
+                                               $self->{hostname},
+                                               $self->{username},
+                                               $is_sparse);
+    }
 
-    my $dom = new XML::DOM::Parser->parse($xml);
-    $self->{dom} = $dom;
+    # Default to Local
+    return new Sys::VirtV2V::Transfer::Local($path, $is_sparse);
 }
 
-=back
-
 =head1 COPYRIGHT
 
 Copyright (C) 2009,2010 Red Hat Inc.
@@ -264,6 +167,8 @@ Please see the file COPYING.LIB for the full license.
 =head1 SEE ALSO
 
 L<Sys::VirtV2V::Connection(3)>,
+L<Sys::VirtV2V::Connection::LibVirtSource(3)>,
+L<Sys::VirtV2V::Connection::LibVirtTarget(3)>,
 L<virt-v2v(1)>,
 L<http://libguestfs.org/>.
 
diff --git a/lib/Sys/VirtV2V/Connection/LibVirtSource.pm b/lib/Sys/VirtV2V/Connection/LibVirtSource.pm
new file mode 100644
index 0000000..701d8f2
--- /dev/null
+++ b/lib/Sys/VirtV2V/Connection/LibVirtSource.pm
@@ -0,0 +1,247 @@
+# Sys::VirtV2V::Connection::LibVirt
+# Copyright (C) 2009,2010 Red Hat Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+package Sys::VirtV2V::Connection::LibVirtSource;
+
+use strict;
+use warnings;
+
+use URI;
+use XML::DOM;
+
+use Sys::Virt;
+
+use Sys::VirtV2V;
+use Sys::VirtV2V::Connection::Source;
+use Sys::VirtV2V::Connection::LibVirt;
+use Sys::VirtV2V::Transfer::ESX;
+use Sys::VirtV2V::Util qw(user_message parse_libvirt_volinfo);
+
+use Locale::TextDomain 'virt-v2v';
+
+ at Sys::VirtV2V::Connection::LibVirtSource::ISA =
+    qw(Sys::VirtV2V::Connection::Source Sys::VirtV2V::Connection::LibVirt);
+
+=pod
+
+=head1 NAME
+
+Sys::VirtV2V::Connection::LibVirtSource - Get storage and metadata from libvirt
+
+=head1 SYNOPSIS
+
+ use Sys::VirtV2V::Connection::LibVirtSource;
+
+ $conn = Sys::VirtV2V::Connection::LibVirtSource->new
+    ("xen+ssh://xenserver.example.com/", $name);
+ $dom = $conn->get_dom();
+
+=head1 DESCRIPTION
+
+Sys::VirtV2V::Connection::LibVirtSource reads a guest's libvirt XML directly
+from a libvirt connection. It accesses the guest's storage over the same
+transport as the libvirt connection.
+
+=head1 METHODS
+
+=over
+
+=item new(uri, name)
+
+Create a new libvirt source connection. Domain I<name> will be obtained from
+I<uri>.
+
+=cut
+
+sub new
+{
+    my $class = shift;
+    my ($uri, $name) = @_;
+
+    my $self = $class->SUPER::_libvirt_new($uri);
+
+    $self->{name} = $name;
+
+    $self->_check_shutdown();
+    $self->_get_dom();
+
+    return $self;
+}
+
+=item get_name
+
+Return the name of the domain.
+
+=cut
+
+sub get_name
+{
+    return shift->{name};
+}
+
+=item get_volume(path)
+
+Return a volume object for I<path>, where I<path> is the path of a volume on the
+connected hypervisor.
+
+=cut
+
+sub get_volume
+{
+    my $self = shift;
+    my ($path) = @_;
+
+    my $uri = $self->{uri};
+
+    my ($name, $format, $size, $is_sparse, $is_block);
+
+    # The libvirt storage APIs aren't yet reliably implemented for ESX, so we
+    # need to get volume metadata some other way
+    my $transfer;
+    if ($uri->scheme eq "esx") {
+        $transfer = $self->_get_transfer($path, 0);
+
+        $name = $transfer->esx_get_name();
+        $format = "raw";
+        $size = $transfer->esx_get_size();
+        $is_sparse = 0;
+        $is_block = 0;
+    }
+
+    else {
+        my $vol;
+        eval {
+            $vol = $self->{vmm}->get_storage_volume_by_path($path);
+        };
+        die(user_message(__x("Failed to retrieve storage volume {path}:".
+                             "{error}",
+                             path => $path,
+                             error => $@->stringify()))) if($@);
+
+        ($name, $format, $size, $is_sparse, $is_block) =
+            parse_libvirt_volinfo($vol);
+
+        $transfer = $self->_get_transfer($path, $is_sparse);
+    }
+
+    return new Sys::VirtV2V::Connection::Volume($name, $format, $path, $size,
+                                                $is_sparse, $is_block,
+                                                $transfer);
+}
+
+sub _check_shutdown
+{
+    my $self = shift;
+
+    my $vmm = $self->{vmm};
+    my $domain = $self->_get_domain();
+
+    # Check the domain is shutdown
+    die(user_message(__x("Guest {name} is currently {state}. It must be ".
+                         "shut down first.",
+                         state => _state_string($domain->get_info()->{state}),
+                         name => $self->{name})))
+        unless ($domain->get_info()->{state} ==
+                Sys::Virt::Domain::STATE_SHUTOFF);
+}
+
+sub _state_string
+{
+    my ($state) = @_;
+
+    if ($state == Sys::Virt::Domain::STATE_NOSTATE) {
+        return __"idle";
+    } elsif ($state == Sys::Virt::Domain::STATE_RUNNING) {
+        return __"running";
+    } elsif ($state == Sys::Virt::Domain::STATE_BLOCKED) {
+        return __"blocked";
+    } elsif ($state == Sys::Virt::Domain::STATE_PAUSED) {
+        return __"paused";
+    } elsif ($state == Sys::Virt::Domain::STATE_SHUTDOWN) {
+        return __"shutting down";
+    } elsif ($state == Sys::Virt::Domain::STATE_SHUTOFF) {
+        return __"shut down";
+    } elsif ($state == Sys::Virt::Domain::STATE_CRASHED) {
+        return __"crashed";
+    } else {
+        return "unknown state ($state)";
+    }
+}
+
+sub _get_domain
+{
+    my $self = shift;
+
+    return $self->{domain} if (defined($self->{domain}));
+
+    my $vmm = $self->{vmm};
+    my $name = $self->{name};
+
+    # Lookup the domain
+    my $domain;
+    eval {
+        $domain = $vmm->get_domain_by_name($name);
+    };
+    die($@) if ($@);
+
+    # Check we found it
+    die(user_message(__x("{name} isn't a valid guest name", name => $name)))
+        unless($domain);
+
+    $self->{domain} = $domain;
+
+    return $domain;
+}
+
+sub _get_dom
+{
+    my $self = shift;
+
+    my $vmm = $self->{vmm};
+    my $name = $self->{name};
+
+    # Lookup the domain
+    my $domain = $self->_get_domain();
+
+    # Warn and exit if we didn't find it
+    return undef unless(defined($domain));
+
+    my $xml = $domain->get_xml_description();
+
+    my $dom = new XML::DOM::Parser->parse($xml);
+    $self->{dom} = $dom;
+}
+
+=back
+
+=head1 COPYRIGHT
+
+Copyright (C) 2009,2010 Red Hat Inc.
+
+=head1 LICENSE
+
+Please see the file COPYING.LIB for the full license.
+
+=head1 SEE ALSO
+
+L<Sys::VirtV2V::Connection::Source(3)>,
+L<virt-v2v(1)>,
+L<http://libguestfs.org/>.
+
+=cut
+
+1;
diff --git a/lib/Sys/VirtV2V/Target/LibVirt.pm b/lib/Sys/VirtV2V/Connection/LibVirtTarget.pm
similarity index 70%
rename from lib/Sys/VirtV2V/Target/LibVirt.pm
rename to lib/Sys/VirtV2V/Connection/LibVirtTarget.pm
index 279e93d..9a3b1c5 100644
--- a/lib/Sys/VirtV2V/Target/LibVirt.pm
+++ b/lib/Sys/VirtV2V/Connection/LibVirtTarget.pm
@@ -1,4 +1,4 @@
-# Sys::VirtV2V::Target::LibVirt
+# Sys::VirtV2V::Connection::LibVirtTarget
 # Copyright (C) 2010 Red Hat Inc.
 #
 # This library is free software; you can redistribute it and/or
@@ -18,161 +18,38 @@
 use strict;
 use warnings;
 
-package Sys::VirtV2V::Target::LibVirt::Vol;
+package Sys::VirtV2V::Connection::LibVirtTarget;
 
-use POSIX;
-
-use Sys::VirtV2V::Util qw(user_message);
+use Sys::VirtV2V::Connection::LibVirt;
+use Sys::VirtV2V::Util qw(user_message parse_libvirt_volinfo);
 
 use Locale::TextDomain 'virt-v2v';
 
-sub _new
-{
-    my $class = shift;
-    my ($vol) = @_;
-
-    my $self = {};
-    bless($self, $class);
-
-    $self->{vol} = $vol;
-
-    return $self;
-}
-
-sub _create
-{
-    my $class = shift;
-    my ($pool, $name, $size) = @_;
-
-    my $vol_xml = "
-        <volume>
-            <name>$name</name>
-            <capacity>$size</capacity>
-        </volume>
-    ";
-
-    my $vol;
-    eval {
-        $vol = $pool->create_volume($vol_xml);
-    };
-    die(user_message(__x("Failed to create storage volume: {error}",
-                         error => $@->stringify()))) if ($@);
-
-    return $class->_new($vol);
-}
-
-sub _get
-{
-    my $class = shift;
-    my ($pool, $name) = @_;
-
-    my $vol;
-    eval {
-        $vol = $pool->get_volume_by_name($name);
-    };
-    die(user_message(__x("Failed to get storage volume: {error}",
-                          error => $@->stringify()))) if ($@);
-
-    return $class->_new($vol);
-}
-
-sub get_path
-{
-    my $self = shift;
-
-    return $self->{vol}->get_path();
-}
-
-sub get_format
-{
-    my $self = shift;
-
-    my $vol = $self->{vol};
-    my $voldom = new XML::DOM::Parser->parse($vol->get_xml_description());
-
-    my ($format) = $voldom->findnodes('/volume/target/format/@type');
-    $format = $format->getValue() if (defined($format));
-    $format ||= 'auto';
-
-    return $format;
-}
-
-sub is_block
-{
-    my $self = shift;
-
-    my $type = $self->{vol}->get_info()->{type};
-    return $type == Sys::Virt::StorageVol::TYPE_BLOCK;
-}
-
-sub open
-{
-    my $self = shift;
-
-    my $path = $self->get_path();
-
-    # We want to open the existing volume without truncating it
-    sysopen(my $fd, $path, O_WRONLY)
-        or die(user_message(__x("Error opening storage volume {path} ".
-                                "for writing: {error}", error => $!)));
-
-    $self->{fd} = $fd;
-}
-
-sub write
-{
-    my $self = shift;
-    my ($data) = @_;
-
-    defined($self->{fd}) or die("write called without open");
-
-    syswrite($self->{fd}, $data)
-        or die(user_message(__x("Error writing to {path}: {error}",
-                                path => $self->get_path(),
-                                error => $!)));
-}
-
-sub close
-{
-    my $self = shift;
-
-    close($self->{fd})
-        or die(user_message(__x("Error closing volume handle: {error}",
-                                error => $!)));
-
-    delete($self->{fd});
-}
-
-package Sys::VirtV2V::Target::LibVirt;
-
-use Sys::Virt;
-use Sys::Virt::Error;
-
-use Sys::VirtV2V::Util qw(user_message);
-use Locale::TextDomain 'virt-v2v';
+ at Sys::VirtV2V::Connection::LibVirtTarget::ISA =
+    qw(Sys::VirtV2V::Connection::LibVirt);
 
 =head1 NAME
 
-Sys::VirtV2V::Target::LibVirt - Output to libvirt
+Sys::VirtV2V::Connection::LibVirtTarget - Output to libvirt
 
 =head1 SYNOPSIS
 
- use Sys::VirtV2V::Target::LibVirt;
+ use Sys::VirtV2V::Connection::LibVirtTarget;
 
- my $target = new Sys::VirtV2V::Target::LibVirt($uri, $poolname);
+ my $target = new Sys::VirtV2V::Connection::LibVirtTarget($uri, $poolname);
 
 =head1 DESCRIPTION
 
-Sys::VirtV2V::Target::LibVirt creates a new libvirt domain using the given
-target URI. New storage will be created in the target pool.
+Sys::VirtV2V::Connection::LibVirtTarget creates a new libvirt domain using the
+given target URI. New storage will be created in the target pool.
 
 =head1 METHODS
 
 =over
 
-=item Sys::VirtV2V::Target::LibVirt->new(uri, poolname)
+=item Sys::VirtV2V::Connection::LibVirtTarget->new(uri, poolname)
 
-Create a new Sys::VirtV2V::Target::LibVirt object.
+Create a new LibVirtTarget object.
 
 =over
 
@@ -193,20 +70,14 @@ sub new
     my $class = shift;
     my ($uri, $poolname) = @_;
 
-    my $self = {};
-    bless($self, $class);
-
-    $self->{vmm} = Sys::Virt->new(auth => 1, uri => $uri);
+    my $self = $class->SUPER::_libvirt_new($uri);
 
     eval {
         $self->{pool} = $self->{vmm}->get_storage_pool_by_name($poolname);
     };
-
-    if ($@) {
-        die(user_message(__x("Output pool {poolname} is not a valid ".
-                             "storage pool",
-                             poolname => $poolname)));
-    }
+    die(user_message(__x("Output pool {poolname} is not a valid ".
+                         "storage pool",
+                         poolname => $poolname))) if ($@);
 
     return $self;
 }
@@ -221,23 +92,56 @@ Create a new volume in the pool whose name was passed to new().
 
 The name of the volume which is being created.
 
+=item format
+
+The file format of the new volume.
+
 =item size
 
 The size of the volume which is being created in bytes.
 
+=item sparse
+
+1 if the output should be sparse if possible, 0 otherwise.
+
 =back
 
-create_volume() returns a Sys::VirtV2V::Target::LibVirt::Vol object.
+create_volume() returns a Sys::VirtV2V::Connection::Volume object.
 
 =cut
 
 sub create_volume
 {
     my $self = shift;
-    my ($name, $size) = @_;
+    my ($name, $format, $size, $sparse) = @_;
+
+    my $allocation = $sparse ? 0 : $size;
+    my $vol_xml = "
+        <volume>
+            <name>$name</name>
+            <capacity>$size</capacity>
+            <allocation>$allocation</allocation>
+            <target>
+                <format>$format</format>
+            </target>
+        </volume>
+    ";
 
-    return Sys::VirtV2V::Target::LibVirt::Vol->_create($self->{pool},
-                                                       $name, $size);
+    my $vol;
+    eval {
+        $vol = $self->{pool}->create_volume($vol_xml);
+    };
+    die(user_message(__x("Failed to create storage volume: {error}",
+                         error => $@->stringify()))) if ($@);
+
+    my $info = $vol->get_info();
+    my $is_block = $info->{type} == Sys::Virt::StorageVol::TYPE_BLOCK ? 1 : 0;
+
+    my $transfer = $self->_get_transfer($vol->get_path(), $sparse);
+    return new Sys::VirtV2V::Connection::Volume($name, $format,
+                                                $vol->get_path(), $size,
+                                                $sparse, $is_block,
+                                                $transfer);
 }
 
 =item volume_exists (name)
@@ -278,7 +182,8 @@ sub volume_exists
 
 =item get_volume (name)
 
-Get a reference to an existing volume. See L<create_volume> for return value.
+Get a reference to an existing volume. get_volume returns a
+Sys::VirtV2V::Connection::Volume object.
 
 =cut
 
@@ -287,7 +192,24 @@ sub get_volume
     my $self = shift;
     my ($name) = @_;
 
-    return Sys::VirtV2V::Target::LibVirt::Vol->_get($self->{pool}, $name);
+    my $uri = $self->{uri};
+    my $pool = $self->{pool};
+
+    my $vol;
+    eval {
+        $vol = $pool->get_volume_by_name($name);
+    };
+    die(user_message(__x("Failed to get storage volume: {error}",
+                          error => $@->stringify()))) if ($@);
+
+    my (undef, $format, $size, $is_sparse, $is_block) =
+        parse_libvirt_volinfo($vol);
+
+    my $transfer = $self->_get_transfer($vol->get_path(), $is_sparse);
+    return new Sys::VirtV2V::Connection::Volume($name, $format,
+                                                $vol->get_path(), $size,
+                                                $is_sparse, $is_block,
+                                                $transfer);
 }
 
 =item guest_exists(name)
@@ -317,7 +239,7 @@ sub guest_exists
     return 1;
 }
 
-=item create_guest(dom)
+=item create_guest(desc, dom, guestcaps)
 
 Create the guest in the target
 
diff --git a/lib/Sys/VirtV2V/Connection/LibVirtXML.pm b/lib/Sys/VirtV2V/Connection/LibVirtXMLSource.pm
similarity index 57%
rename from lib/Sys/VirtV2V/Connection/LibVirtXML.pm
rename to lib/Sys/VirtV2V/Connection/LibVirtXMLSource.pm
index 7dc122f..25d705a 100644
--- a/lib/Sys/VirtV2V/Connection/LibVirtXML.pm
+++ b/lib/Sys/VirtV2V/Connection/LibVirtXMLSource.pm
@@ -1,5 +1,5 @@
-# Sys::VirtV2V::Connection::LibVirtXML
-# Copyright (C) 2009 Red Hat Inc.
+# Sys::VirtV2V::Connection::LibVirtXMLSource
+# Copyright (C) 2009,2010 Red Hat Inc.
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -15,48 +15,38 @@
 # License along with this library; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
-package Sys::VirtV2V::Connection::LibVirtXML;
+package Sys::VirtV2V::Connection::LibVirtXMLSource;
 
 use strict;
 use warnings;
 
+use Sys::Virt;
 use XML::DOM;
 use XML::DOM::XPath;
 
-use Sys::VirtV2V::Connection;
-use Sys::VirtV2V::Util qw(user_message);
+use Sys::VirtV2V::Connection::Source;
+use Sys::VirtV2V::Transfer::Local;
+use Sys::VirtV2V::Util qw(user_message parse_libvirt_volinfo);
 
 use Locale::TextDomain 'virt-v2v';
 
- at Sys::VirtV2V::Connection::LibVirtXML::ISA = qw(Sys::VirtV2V::Connection);
+ at Sys::VirtV2V::Connection::Source::LibVirtXMLSource::ISA =
+    qw(Sys::VirtV2V::Connection::Source);
 
 =pod
 
 =head1 NAME
 
-Sys::VirtV2V::Connection::LibVirtXML - Read libvirt XML from a file
-
-=head1 SYNOPSIS
-
- use Sys::VirtV2V::Connection::LibVirtXML;
-
- $conn = Sys::VirtV2V::Connection::LibVirtXML->new($path, $target);
- $dom = $conn->get_dom();
-
-=head1 DESCRIPTION
-
-Sys::VirtV2V::Connection::LibVirtXML is an implementation of
-Sys::VirtV2V::Connection which reads libvirt XML guest descriptions from a
-file.
+Sys::VirtV2V::Connection::Source::LibVirtXMLSource - Read domain XML from a file
 
 =head1 METHODS
 
 =over
 
-=item new(path, target)
+=item new(path)
 
-Create a new LibVirtXML connection. The metadata itself is read from I<path>.
-Storage will be copied to I<target>.
+Create a new LibVirtXMLSource connection. The metadata itself is read from
+I<path>.
 
 =cut
 
@@ -73,12 +63,23 @@ sub new
 
     $self->_get_dom($path);
 
-    # Only support LocalCopy for libvirtxml
-    $self->_storage_iterate("Sys::VirtV2V::Transfer::LocalCopy", $target);
-
     return $self;
 }
 
+=item get_name
+
+Return the name of the domain.
+
+=cut
+
+sub get_name
+{
+    my $dom = shift->{dom};
+
+    my ($name) = $dom->findnodes('/domain/name');
+    return $name;
+}
+
 sub _get_dom
 {
     my $self = shift;
@@ -102,6 +103,32 @@ sub _get_dom
                          path => $self->{path}))) unless (defined($dummy));
 }
 
+=item get_volume(path)
+
+Return a Sys::VirtV2V::Connection::Volume object for I<path>, where I<path> is
+the path to a locally available volume.
+
+=cut
+
+sub get_volume
+{
+    my $self = shift;
+    my ($path) = @_;
+
+    # Use a libvirt session connection to inspect local volumes
+    my $vmm = Sys::Virt->new(uri => 'qemu:///session');
+    my $vol = $vmm->get_storage_volume_by_path($path);
+
+    my ($name, $format, $size, $is_sparse, $is_block) =
+        parse_libvirt_volinfo($vol, $path);
+
+    my $transfer = new Sys::VirtV2V::Transfer::Local($path, $is_sparse);
+
+    return new Sys::VirtV2V::Connection::Volume($name, $format, $path,
+                                                $size, $is_sparse, $is_block,
+                                                $transfer);
+}
+
 =back
 
 =head1 COPYRIGHT
@@ -114,7 +141,7 @@ Please see the file COPYING.LIB for the full license.
 
 =head1 SEE ALSO
 
-L<Sys::VirtV2V::Connection(3pm>,
+L<Sys::VirtV2V::Connection::Source(3pm>,
 L<virt-v2v(1)>,
 L<http://libguestfs.org/>.
 
diff --git a/lib/Sys/VirtV2V/Target/RHEV.pm b/lib/Sys/VirtV2V/Connection/RHEVTarget.pm
similarity index 75%
rename from lib/Sys/VirtV2V/Target/RHEV.pm
rename to lib/Sys/VirtV2V/Connection/RHEVTarget.pm
index b865c56..765a035 100644
--- a/lib/Sys/VirtV2V/Target/RHEV.pm
+++ b/lib/Sys/VirtV2V/Connection/RHEVTarget.pm
@@ -1,4 +1,4 @@
-# Sys::VirtV2V::Target::RHEV
+# Sys::VirtV2V::Connection::RHEVTarget
 # Copyright (C) 2010 Red Hat Inc.
 #
 # This library is free software; you can redistribute it and/or
@@ -18,7 +18,15 @@
 use strict;
 use warnings;
 
-package Sys::VirtV2V::Target::RHEV::UUIDHelper;
+package rhev_util;
+
+use Exporter 'import';
+our @EXPORT = qw(nfs_helper get_uuid);
+
+sub nfs_helper
+{
+    return Sys::VirtV2V::Connection::RHEVTarget::NFSHelper->new(@_);
+}
 
 sub get_uuid
 {
@@ -35,14 +43,13 @@ sub get_uuid
     return $uuid;
 }
 
-package Sys::VirtV2V::Target::RHEV::NFSHelper;
+
+package Sys::VirtV2V::Connection::RHEVTarget::NFSHelper;
 
 use Carp;
 use File::Temp qw(tempfile);
 use POSIX qw(:sys_wait_h setuid setgid);
 
-use Sys::VirtV2V::Util qw(user_message);
-
 use Locale::TextDomain 'virt-v2v';
 
 sub new
@@ -83,13 +90,14 @@ sub new
         close($tochild_read);
         close($fromchild_write);
 
-        # Set EUID and EGID to RHEV magic values 36:36
+        # Set EUID and EGID to RHEV magic values
         # execute the wrapped function, trapping errors
         eval {
             setgid(36) or die("setgid failed: $!");
             setuid(36) or die("setuid failed: $!");
 
-            &$sub();
+            # Print out the values returned, 1 per line
+            print join("\n", &$sub());
         };
 
         # Don't exit, which would cause destructors to be called in the child.
@@ -113,10 +121,28 @@ sub new
     return $self;
 }
 
+sub values
+{
+    my $self = shift;
+
+    my @values;
+    my $fromchild = $self->{fromchild};
+    while(<$fromchild>) {
+        chomp;
+        push(@values, $_);
+    }
+
+    $self->check_exit();
+    return @values;
+}
+
 sub check_exit
 {
     my $self = shift;
 
+    # Make sure it's not waiting on more input
+    close($self->{tochild});
+
     my $ret = waitpid($self->{pid}, 0);
 
     # If the process terminated normally, check the exit status and stderr
@@ -158,215 +184,282 @@ sub DESTROY
     $? = $retval;
 }
 
-package Sys::VirtV2V::Target::RHEV::Vol;
 
-use File::Path;
-use File::Temp qw(tempdir);
-use POSIX;
+package Sys::VirtV2V::Connection::RHEVTarget::WriteStream;
 
-use Sys::VirtV2V::Util qw(user_message);
+use File::Spec::Functions qw(splitpath);
 
+use Sys::VirtV2V::Util qw(user_message);
 use Locale::TextDomain 'virt-v2v';
 
-our %vols_by_path;
-our @vols;
-our $tmpdir;
-
-sub _new
+sub new
 {
     my $class = shift;
-    my ($mountdir, $domainuuid, $insize) = @_;
+    my ($volume) = @_;
 
     my $self = {};
     bless($self, $class);
 
-    $self->{insize} = $insize;
-    # RHEV needs disks to be a multiple of 512 in size. Additionally, SIZE in
-    # the disk meta file has units of kilobytes. To ensure everything matches up
-    # exactly, we will pad to to a 1024 byte boundary.
-    $self->{outsize} = ceil($insize/1024) * 1024;
+    $self->{volume} = $volume;
+    $self->{writer} = rhev_util::nfs_helper(sub {
+        my $path = $self->{volume}->get_path();
 
-    my $imageuuid = Sys::VirtV2V::Target::RHEV::UUIDHelper::get_uuid();
-    my $voluuid   = Sys::VirtV2V::Target::RHEV::UUIDHelper::get_uuid();
-    $self->{imageuuid}  = $imageuuid;
-    $self->{voluuid}    = $voluuid;
-    $self->{domainuuid} = $domainuuid;
+        # Create the output directory
+        my (undef, $dir, undef) = splitpath($path);
+        mkdir($dir)
+            or die(user_message(__x("Failed to create directory {dir}: {error}",
+                                    dir => $dir,
+                                    error => $!)));
 
-    my $root = "$mountdir/$domainuuid";
-    unless (defined($tmpdir)) {
-        my $nfs = Sys::VirtV2V::Target::RHEV::NFSHelper->new(sub {
-            print tempdir("v2v.XXXXXXXX", DIR => $root);
-        });
-        my $fromchild = $nfs->{fromchild};
-        ($tmpdir) = <$fromchild>;
-        $nfs->check_exit();
-    }
+        # Write data using dd in 2MB chunks
+        # XXX - mbooth at redhat.com 06/04/2010 (Fedora 12 writing to RHEL 5 NFS)
+        # Use direct IO as writing a large amount of data to NFS regularly
+        # crashes my machine.  Using direct io doesn't.
+        exec('dd', 'obs=2M', 'oflag=direct', 'of='.$path)
+            or die("Unable to execute dd: $!");
+    });
+    $self->{written} = 0;
 
-    $self->{dir}  = "$root/images/$imageuuid";
-    $self->{tmpdir} = "$tmpdir/$imageuuid";
+    return $self;
+}
 
-    $self->{path} = $self->{tmpdir}."/$voluuid";
+sub _write_metadata
+{
+    my $self = shift;
 
-    $self->{creation} = time();
+    my $nfs = rhev_util::nfs_helper(sub {
+        my $volume = $self->{volume};
 
-    $vols_by_path{$self->{path}} = $self;
-    push(@vols, $self);
+        my $path = $volume->get_path().'.meta';
 
-    return $self;
-}
+        my $sizek = $self->{written} / 1024;
 
-sub _get_by_path
-{
-    my $class = shift;
-    my ($path) = @_;
+        # Write out the .meta file
+        my $meta;
+        open($meta, '>', $path)
+            or die(user_message(__x("Unable to open {path} for writing: ".
+                                    "{error}",
+                                    path => $path,
+                                    error => $!)));
 
-    return $vols_by_path{$path};
+        print $meta "DOMAIN=".$volume->_get_domainuuid()."\n";
+        print $meta "VOLTYPE=LEAF\n";
+        print $meta "CTIME=".$volume->_get_creation()."\n";
+        print $meta "FORMAT=".$volume->_get_rhev_format()."\n";
+        print $meta "IMAGE=".$volume->_get_imageuuid()."\n";
+        print $meta "DISKTYPE=1\n";
+        print $meta "PUUID=00000000-0000-0000-0000-000000000000\n";
+        print $meta "LEGALITY=LEGAL\n";
+        print $meta "MTIME=".$volume->_get_creation()."\n";
+        print $meta "POOL_UUID=00000000-0000-0000-0000-000000000000\n";
+        print $meta "SIZE=$sizek\n";
+        print $meta "TYPE=".uc($volume->_get_rhev_type())."\n";
+        print $meta "DESCRIPTION=Exported by virt-v2v\n";
+        print $meta "EOF\n";
+
+        close($meta)
+            or die(user_message(__x("Error closing {path}: {error}",
+                                    path => $path,
+                                    error => $!)));
+    });
+    $nfs->check_exit();
 }
 
-sub _get_size
+sub write
 {
     my $self = shift;
+    my ($buf) = @_;
+
+    print { $self->{writer}->{tochild} } $buf
+        or die(user_message(__x("Writing to {path} failed: {error}",
+                                path => $self->{volume}->get_path(),
+                                error => $!)));
 
-    return $self->{outsize};
+    $self->{written} += length($buf);
 }
 
-sub _get_imageuuid
+sub close
 {
     my $self = shift;
 
-    return $self->{imageuuid};
+    # Pad the file up to a 1K boundary
+    my $pad = (1024 - ($self->{written} % 1024)) % 1024;
+    $self->write("\0" x $pad) if ($pad);
+
+    $self->{writer}->check_exit();
+    $self->_write_metadata();
 }
 
-sub _get_voluuid
+sub DESTROY
 {
     my $self = shift;
 
-    return $self->{voluuid};
+    $self->close() if (exists($self->{pid}));
 }
 
-sub _get_creation
-{
-    my $self = shift;
 
-    return $self->{creation};
-}
+package Sys::VirtV2V::Connection::RHEVTarget::Transfer;
 
-sub get_path
+use Carp;
+use Locale::TextDomain 'virt-v2v';
+
+sub new
 {
-    my $self = shift;
+    my $class = shift;
+    my ($volume) = @_;
 
-    return $self->{path};
+    my $self = {};
+    bless($self, $class);
+
+    $self->{volume} = $volume;
+
+    return $self;
 }
 
-sub get_format
+sub local_path
 {
-    my $self = shift;
+    return shift->{volume}->get_path();
+}
 
-    return "raw";
+sub get_read_stream
+{
+    die(user_message(__"Unable to read data from RHEV"));
 }
 
-sub is_block
+sub get_write_stream
 {
-    my $self = shift;
+    my $volume = shift->{volume};
+    return new Sys::VirtV2V::Connection::RHEVTarget::WriteStream($volume);
+}
 
-    return 0;
+sub DESTROY
+{
+    # Remove circular reference
+    delete(shift->{volume});
 }
 
-sub open
+
+package Sys::VirtV2V::Connection::RHEVTarget::Vol;
+
+use File::Path;
+use File::Spec::Functions;
+use File::Temp qw(tempdir);
+use POSIX;
+
+use Sys::VirtV2V::Util qw(user_message);
+use Locale::TextDomain 'virt-v2v';
+
+our %vols_by_path;
+our @vols;
+our $tmpdir;
+
+ at Sys::VirtV2V::Connection::RHEVTarget::Vol::ISA =
+    qw(Sys::VirtV2V::Connection::Volume);
+
+sub new
 {
-    my $self = shift;
+    my $class = shift;
+    my ($mountdir, $domainuuid, $format, $insize, $sparse) = @_;
 
-    my $now = $self->{creation};
-    $self->{written} = 0;
+    my $root = catdir($mountdir, $domainuuid);
 
-    $self->{writer} = Sys::VirtV2V::Target::RHEV::NFSHelper->new(sub {
-        my $dir = $self->{tmpdir};
-        my $path = $self->{path};
+    # Initialise the package-wide temp directory if required
+    unless (defined($tmpdir)) {
+        my $nfs = rhev_util::nfs_helper(sub {
+            return tempdir("v2v.XXXXXXXX", DIR => $root);
+        });
+        ($tmpdir) = $nfs->values();
+    }
 
-        mkdir($dir)
-            or die(user_message(__x("Failed to create directory {dir}: {error}",
-                                    dir => $dir,
-                                    error => $!)));
+    my $imageuuid = rhev_util::get_uuid();
+    my $voluuid   = rhev_util::get_uuid();
 
-        # Write out the .meta file
-        my $meta;
-        open($meta, '>', "$path.meta")
-            or die(user_message(__x("Unable to open {path} for writing: ".
-                                    "{error}",
-                                    path => "$path.meta",
-                                    error => $!)));
+    my $imagedir    = catdir($root, 'images', $imageuuid);
+    my $imagetmpdir = catdir($tmpdir, $imageuuid);
+    my $volpath     = catfile($imagetmpdir, $voluuid);
 
-        print $meta "DOMAIN=".$self->{domainuuid}."\n";
-        print $meta "VOLTYPE=LEAF\n";
-        print $meta "CTIME=$now\n";
-        print $meta "FORMAT=RAW\n";
-        print $meta "IMAGE=".$self->{imageuuid}."\n";
-        print $meta "DISKTYPE=1\n";
-        print $meta "PUUID=00000000-0000-0000-0000-000000000000\n";
-        print $meta "LEGALITY=LEGAL\n";
-        print $meta "MTIME=$now\n";
-        print $meta "POOL_UUID=00000000-0000-0000-0000-000000000000\n";
-        print $meta "SIZE=".($self->{outsize} / 1024)."\n";
-        print $meta "TYPE=SPARSE\n";
-        print $meta "DESCRIPTION=Exported by virt-v2v\n";
-        print $meta "EOF\n";
+    # RHEV needs disks to be a multiple of 512 in size. Additionally, SIZE in
+    # the disk meta file has units of kilobytes. To ensure everything matches up
+    # exactly, we will pad to to a 1024 byte boundary.
+    my $outsize = ceil($insize/1024) * 1024;
 
-        close($meta)
-            or die(user_message(__x("Error closing {path}: {error}",
-                                    path => "$path.meta",
-                                    error => $!)));
+    my $creation = time();
 
-        # Write the remainder of the data using dd in 2MB chunks
-        # XXX - mbooth at redhat.com 06/04/2010 (Fedora 12 writing to RHEL 5 NFS)
-        # Use direct IO as writing a large amount of data to NFS regularly
-        # crashes my machine.  Using direct io crashes less.
-        exec('dd', 'obs='.1024*1024*2, 'oflag=direct', 'of='.$path)
-            or die("Unable to execute dd: $!");
-    });
-}
+    my $self = $class->SUPER::new($imageuuid, $format, $volpath, $outsize,
+                                  $sparse, 0);
+    $self->{transfer} =
+        new Sys::VirtV2V::Connection::RHEVTarget::Transfer($self);
 
-sub write
-{
-    my $self = shift;
-    my ($data) = @_;
+    $self->{insize}  = $insize;
+    $self->{outsize} = $outsize;
+
+    $self->{imageuuid}  = $imageuuid;
+    $self->{voluuid}    = $voluuid;
+    $self->{domainuuid} = $domainuuid;
 
-    defined($self->{writer}) or die("write called without open");
+    $self->{imagedir}    = $imagedir;
+    $self->{imagetmpdir} = $imagetmpdir;
 
-    unless(print {$self->{writer}->{tochild}} $data) {
-        # This should only have failed if there was an error from the helper
-        $self->{writer}->check_exit();
+    $self->{creation} = $creation;
 
-        # die() explicitly in case the above didn't
-        die("Error writing to helper: $!");
+    # Convert format into something RHEV understands
+    my $rhev_format;
+    if ($format eq 'raw') {
+        $self->{rhev_format} = 'RAW';
+    } elsif ($format eq 'qcow2') {
+        $self->{rhev_format} = 'COW';
+    } else {
+        die(user_message(__x("RHEV cannot handle volumes of format {format}",
+                             format => $format)));
     }
 
-    $self->{written} += length($data);
+    # Generate the RHEV type
+    # N.B. This must be in mixed case in the OVF, but in upper case in the .meta
+    # file. We store it in mixed case and convert to upper when required.
+    $self->{rhev_type} = $sparse ? 'Sparse' : 'Preallocated';
+
+    $vols_by_path{$volpath} = $self;
+    push(@vols, $self);
+
+    return $self;
 }
 
-sub close
+sub _get_by_path
 {
-    my $self = shift;
+    my $class = shift;
+    my ($path) = @_;
 
-    # Check we wrote the full file
-    die(user_message(__x("Didn't write full volume. Expected {expected} ".
-                         "bytes, wrote {actual} bytes.",
-                         expected => $self->{insize},
-                         actual => $self->{written})))
-        unless ($self->{written} == $self->{insize});
+    return $vols_by_path{$path};
+}
 
-    # Pad the output up to outsize
-    my $pad = $self->{outsize} - $self->{insize};
-    $self->write("\0" x $pad) if ($pad);
+sub _get_domainuuid
+{
+    return shift->{domainuuid};
+}
 
-    # Close the writer pipe, which will cause the child to exit
-    close($self->{writer}->{tochild})
-        or die("Error closing tochild pipe");
+sub _get_imageuuid
+{
+    return shift->{imageuuid};
+}
 
-    # Wait for the child to exit
-    $self->{writer}->check_exit();
+sub _get_voluuid
+{
+    return shift->{voluuid};
+}
+
+sub _get_creation
+{
+    return shift->{creation};
+}
 
-    delete($self->{writer});
-    delete($self->{written});
+sub _get_rhev_format
+{
+    return shift->{rhev_format};
+}
+
+sub _get_rhev_type
+{
+    return shift->{rhev_type};
 }
 
 sub _move_vols
@@ -374,11 +467,11 @@ sub _move_vols
     my $class = shift;
 
     foreach my $vol (@vols) {
-        rename($vol->{tmpdir}, $vol->{dir})
+        rename($vol->{imagetmpdir}, $vol->{imagedir})
             or die(user_message(__x("Unable to move volume from temporary ".
                                     "location {tmpdir} to {dir}",
-                                    tmpdir => $vol->{tmpdir},
-                                    dir => $vol->{dir})));
+                                    tmpdir => $vol->{imagetmpdir},
+                                    dir => $vol->{imagedir})));
     }
 
     $class->_cleanup();
@@ -405,9 +498,10 @@ sub _cleanup
     $tmpdir = undef;
 }
 
-package Sys::VirtV2V::Target::RHEV;
+package Sys::VirtV2V::Connection::RHEVTarget;
 
 use File::Temp qw(tempdir);
+use File::Spec::Functions;
 use Time::gmtime;
 
 use Sys::VirtV2V::ExecHelper;
@@ -417,26 +511,15 @@ use Locale::TextDomain 'virt-v2v';
 
 =head1 NAME
 
-Sys::VirtV2V::Target::RHEV - Output to a RHEV Export storage domain
-
-=head1 SYNOPSIS
-
- use Sys::VirtV2V::Target::RHEV;
-
- my $target = new Sys::VirtV2V::Target::RHEV($domain_path);
-
-=head1 DESCRIPTION
-
-Sys::VirtV2V::Target::RHEV write the converted guest to a RHEV Export storage
-domain. This can later be imported to RHEV by the user.
+Sys::VirtV2V::Connection::RHEVTarget - Output to a RHEV Export storage domain
 
 =head1 METHODS
 
 =over
 
-=item Sys::VirtV2V::Target::RHEV->new(domain_path)
+=item Sys::VirtV2V::Connection::RHEVTarget->new(domain_path)
 
-Create a new Sys::VirtV2V::Target::RHEV object.
+Create a new Sys::VirtV2V::Connection::RHEVTarget object.
 
 =over
 
@@ -479,60 +562,46 @@ sub new
                              output => $eh->output())));
     }
 
-    my $nfs = Sys::VirtV2V::Target::RHEV::NFSHelper->new(sub {
+    my $nfs = rhev_util::nfs_helper(sub {
         opendir(my $dir, $mountdir)
             or die(user_message(__x("Unable to open {mountdir}: {error}",
                                     mountdir => $mountdir,
                                     error => $!)));
 
+        my @entries;
         foreach my $entry (readdir($dir)) {
             # return entries which look like uuids
-            print "$entry\n"
+            push(@entries, $entry)
                 if ($entry =~ /^[0-9a-z]{8}-(?:[0-9a-z]{4}-){3}[0-9a-z]{12}$/);
         }
+
+        return @entries;
     });
+    my @entries = $nfs->values();
 
-    # Get the UUID of the storage domain
-    my $domainuuid;
-    my $fromchild = $nfs->{fromchild};
-    while (<$fromchild>) {
-        if (defined($domainuuid)) {
-            die(user_message(__x("{domain_path} contains multiple possible ".
-                                 "domains. It may only contain one.",
-                                 domain_path => $domain_path)));
-        }
-        chomp;
-        $domainuuid = $_;
-    }
-    $nfs->check_exit();
+    die(user_message(__x("{domain_path} contains multiple possible ".
+                         "domains. It may only contain one.",
+                         domain_path => $domain_path))) if (@entries > 1);
 
-    if (!defined($domainuuid)) {
-        die(user_message(__x("{domain_path} does not contain an initialised ".
-                             "storage domain",
-                             domain_path => $domain_path)));
-    }
+    my ($domainuuid) = @entries;
+    die(user_message(__x("{domain_path} does not contain an initialised ".
+                         "storage domain",
+                         domain_path => $domain_path)))
+        unless (defined($domainuuid));
     $self->{domainuuid} = $domainuuid;
 
     # Check that the domain has been attached to a Data Center by checking that
     # the master/vms directory exists
-    my $vms_rel = $domainuuid.'/master/vms';
-    my $vms_abs = $mountdir.'/'.$vms_rel;
-    $nfs = Sys::VirtV2V::Target::RHEV::NFSHelper->new(sub {
-        if (-d $vms_abs) {
-            print "1\n";
-        } else {
-            print "0\n";
-        }
+    my $vms_rel = catdir($domainuuid, 'master', 'vms');
+    my $vms_abs = catdir($mountdir, $vms_rel);
+    $nfs = rhev_util::nfs_helper(sub {
+        return -d $vms_abs ? 1 : 0;
     });
-    $fromchild = $nfs->{fromchild};
-    while (<$fromchild>) {
-        chomp;
-        die(user_message(__x("{domain_path} has not been attached to a RHEV ".
-                             "data center ({path} does not exist).",
-                             domain_path => $domain_path,
-                             path => $vms_rel))) if ($_ eq "0");
-    }
-    $nfs->check_exit();
+    my ($attached) = $nfs->values();
+    die(user_message(__x("{domain_path} has not been attached to a RHEV ".
+                         "data center ({path} does not exist).",
+                         domain_path => $domain_path,
+                         path => $vms_rel))) unless ($attached);
 
     return $self;
 }
@@ -547,8 +616,8 @@ sub DESTROY
     my $retval = $?;
 
     eval {
-        my $nfs = Sys::VirtV2V::Target::RHEV::NFSHelper->new(sub {
-            Sys::VirtV2V::Target::RHEV::Vol->_cleanup();
+        my $nfs = rhev_util::nfs_helper(sub {
+            Sys::VirtV2V::Connection::RHEVTarget::Vol->_cleanup();
         });
         $nfs->check_exit();
     };
@@ -577,7 +646,7 @@ sub DESTROY
     $? = $retval;
 }
 
-=item create_volume(name, size)
+=item create_volume(name, format, size, is_sparse)
 
 Create a new volume in the export storage domain
 
@@ -587,27 +656,37 @@ Create a new volume in the export storage domain
 
 The name of the volume which is being created.
 
+=item format
+
+The file format of the target volume, as returned by qemu.
+
 =item size
 
 The size of the volume which is being created in bytes.
 
+=item is_sparse
+
+1 if the target volume is sparse, 0 otherwise.
+
 =back
 
-create_volume() returns a Sys::VirtV2V::Target::RHEV::Vol object.
+create_volume() returns a Sys::VirtV2V::Connection::RHEVTarget::Vol object.
 
 =cut
 
 sub create_volume
 {
     my $self = shift;
-    my ($name, $size) = @_;
+    my ($name, $format, $size, $is_sparse) = @_;
 
-    return Sys::VirtV2V::Target::RHEV::Vol->_new($self->{mountdir},
-                                                 $self->{domainuuid},
-                                                 $size);
+    return Sys::VirtV2V::Connection::RHEVTarget::Vol->new($self->{mountdir},
+                                                          $self->{domainuuid},
+                                                          $format,
+                                                          $size,
+                                                          $is_sparse);
 }
 
-=item volume_exists (name)
+=item volume_exists(name)
 
 Check if volume I<name> exists in the target storage domain.
 
@@ -617,13 +696,10 @@ Always returns 0, as RHEV storage domains don't have names
 
 sub volume_exists
 {
-    my $self = shift;
-    my ($name) = @_;
-
     return 0;
 }
 
-=item get_volume (name)
+=item get_volume(name)
 
 Not defined for RHEV output
 
@@ -675,7 +751,7 @@ sub create_guest
     # Generate a creation date
     my $vmcreation = _format_time(gmtime());
 
-    my $vmuuid = Sys::VirtV2V::Target::RHEV::UUIDHelper::get_uuid();
+    my $vmuuid = rhev_util::get_uuid();
 
     my $ostype = _get_os_type($desc);
 
@@ -755,20 +831,20 @@ EOF
     $self->_disks($ovf, $dom);
     $self->_networks($ovf, $dom);
 
-    my $nfs = Sys::VirtV2V::Target::RHEV::NFSHelper->new(sub {
+    my $nfs = rhev_util::nfs_helper(sub {
         my $mountdir = $self->{mountdir};
         my $domainuuid = $self->{domainuuid};
 
-        my $dir = $mountdir.'/'.$domainuuid.'/master/vms/'.$vmuuid;
+        my $dir = catdir($mountdir, $domainuuid, 'master', 'vms', $vmuuid);
         mkdir($dir)
             or die(user_message(__x("Failed to create directory {dir}: {error}",
                                     dir => $dir,
                                     error => $!)));
 
-        Sys::VirtV2V::Target::RHEV::Vol->_move_vols();
+        Sys::VirtV2V::Connection::RHEVTarget::Vol->_move_vols();
 
         my $vm;
-        my $ovfpath = $dir.'/'.$vmuuid.'.ovf';
+        my $ovfpath = catfile($dir, $vmuuid.'.ovf');
         open($vm, '>', $ovfpath)
             or die(user_message(__x("Unable to open {path} for writing: ".
                                     "{error}",
@@ -958,13 +1034,13 @@ sub _disks
         my ($bus) = $disk->findnodes('target/@bus');
         $bus = $bus->getNodeValue();
 
-        my $vol = Sys::VirtV2V::Target::RHEV::Vol->_get_by_path($path);
+        my $vol = Sys::VirtV2V::Connection::RHEVTarget::Vol->_get_by_path($path);
 
         die("dom contains path not written by virt-v2v: $path\n".
             $dom->toString()) unless (defined($vol));
 
-        my $fileref = $vol->_get_imageuuid().'/'.$vol->_get_voluuid();
-        my $size_gb = int($vol->_get_size()/1024/1024/1024);
+        my $fileref = catdir($vol->_get_imageuuid(), $vol->_get_voluuid());
+        my $size_gb = int($vol->get_size()/1024/1024/1024);
 
         # Add disk to References
         my $file = $ovf->createElement("File");
@@ -972,7 +1048,7 @@ sub _disks
 
         $file->setAttribute('ovf:href', $fileref);
         $file->setAttribute('ovf:id', $vol->_get_voluuid());
-        $file->setAttribute('ovf:size', $vol->_get_size());
+        $file->setAttribute('ovf:size', $vol->get_size());
         $file->setAttribute('ovf:description', 'imported by virt-v2v');
 
         # Add disk to DiskSection
@@ -986,8 +1062,8 @@ sub _disks
         $diske->setAttribute('ovf:parentRef', '');
         $diske->setAttribute('ovf:vm_snapshot_id',
                              '00000000-0000-0000-0000-000000000000');
-        $diske->setAttribute('ovf:volume-format', 'RAW');
-        $diske->setAttribute('ovf:volume-type', 'Sparse');
+        $diske->setAttribute('ovf:volume-format', $vol->_get_rhev_format());
+        $diske->setAttribute('ovf:volume-type', $vol->_get_rhev_type());
         $diske->setAttribute('ovf:format', 'http://en.wikipedia.org/wiki/Byte');
         # IDE = 0, SCSI = 1, VirtIO = 2
         $diske->setAttribute('ovf:disk-interface', $bus eq 'virtio' ? 2 : 0);
diff --git a/lib/Sys/VirtV2V/Connection.pm b/lib/Sys/VirtV2V/Connection/Source.pm
similarity index 60%
rename from lib/Sys/VirtV2V/Connection.pm
rename to lib/Sys/VirtV2V/Connection/Source.pm
index 8029230..71c5f1f 100644
--- a/lib/Sys/VirtV2V/Connection.pm
+++ b/lib/Sys/VirtV2V/Connection/Source.pm
@@ -1,5 +1,5 @@
-# Sys::VirtV2V::Connection
-# Copyright (C) 2009 Red Hat Inc.
+# Sys::VirtV2V::Connection::Source
+# Copyright (C) 2009,2010 Red Hat Inc.
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -15,16 +15,13 @@
 # License along with this library; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
-package Sys::VirtV2V::Connection;
+package Sys::VirtV2V::Connection::Source;
 
 use strict;
 use warnings;
 
 use Sys::Virt;
 
-use Sys::VirtV2V::Transfer::ESX;
-use Sys::VirtV2V::Transfer::LocalCopy;
-use Sys::VirtV2V::Transfer::SSH;
 use Sys::VirtV2V::Util qw(user_message);
 
 use Locale::TextDomain 'virt-v2v';
@@ -37,21 +34,21 @@ Sys::VirtV2V::Connection - Obtain domain metadata
 
 =head1 SYNOPSIS
 
- use Sys::VirtV2V::Connection::LibVirt;
+ use Sys::VirtV2V::Connection::LibVirtSource;
 
- $conn = Sys::VirtV2V::Connection::LibVirt->new($uri, $name, $target);
+ $conn = Sys::VirtV2V::Connection::LibVirtSource->new($uri, $name, $target);
  $dom = $conn->get_dom();
  $storage = $conn->get_storage_paths();
  $devices = $conn->get_storage_devices();
 
 =head1 DESCRIPTION
 
-Sys::VirtV2V::Connection describes a connection to a, possibly remote, source of
-guest metadata and storage. It is a virtual superclass and can't be instantiated
-directly. Use one of the subclasses:
+Sys::VirtV2V::Source provides access to a source of guest metadata and storage.
+It is a virtual superclass and can't be instantiated directly. Use one of the
+subclasses:
 
- Sys::VirtV2V::Connection::LibVirt
- Sys::VirtV2V::Connection::LibVirtXML
+ Sys::VirtV2V::Target::LibVirtSource
+ Sys::VirtV2V::Source::LibVirtXML
 
 =head1 METHODS
 
@@ -102,14 +99,58 @@ sub get_dom
     return $self->{dom};
 }
 
+sub _volume_copy
+{
+    my ($src, $dst) = @_;
+
+    my $src_s;
+    my $dst_s;
+
+    # Can we just do a straight copy?
+    if ($src->get_format() eq $dst->get_format()) {
+        $src_s = $src->get_read_stream();
+        $dst_s = $dst->get_write_stream();
+    }
+
+    else {
+        die("Different formats");
+    }
+
+    # Copy the contents of the source stream to the destination stream
+    my $total = 0;
+    for (;;) {
+        my $buf = $src_s->read(4 * 1024 * 1024);
+        last if (length($buf) == 0);
+
+        $total += length($buf);
+
+        $dst_s->write($buf);
+    }
+
+    # This would be closed implicitly, but we want to report read/write errors
+    # before checking for a short volume
+    $dst_s->close();
 
-# Iterate over returned storage. Transfer it and update DOM as necessary. To be
-# called by subclasses.
-sub _storage_iterate
+    die(user_message(__x("Didn't receive full volume. Received {received} ".
+                         "of {total} bytes.",
+                         received => $total,
+                         total => $src->get_size())))
+        unless ($total == $src->get_size());
+
+    return $dst;
+}
+
+=item copy_storage(target)
+
+Copy all of a guests storage devices to I<target>. Update the guest metadata to
+reflect their new locations and properties.
+
+=cut
+
+sub copy_storage
 {
     my $self = shift;
-
-    my ($transfer, $target) = @_;
+    my ($target) = @_;
 
     my $dom = $self->get_dom();
 
@@ -118,7 +159,8 @@ sub _storage_iterate
     # A list of libvirt target device names
     my @devices;
 
-    foreach my $disk ($dom->findnodes("/domain/devices/disk[\@device='disk']")) {
+    foreach my $disk ($dom->findnodes("/domain/devices/disk[\@device='disk']"))
+    {
         my ($source_e) = $disk->findnodes('source');
 
         my ($source) = $source_e->findnodes('@file | @dev');
@@ -129,17 +171,30 @@ sub _storage_iterate
         defined($dev) or die("disk does not have a target device: \n".
                              $dom->toString());
 
-        my $path = $source->getValue();
+        my $src = $self->get_volume($source->getValue());
+        my $dst;
+        if ($target->volume_exists($src->get_name())) {
+            warn user_message(__x("WARNING: storage volume {name} already ".
+                                  "exists on the target. NOT copying it ".
+                                  "again. Delete the volume and retry to ".
+                                  "copy again.",
+                                  name => $src->get_name()));
+            $dst = $target->get_volume($src->get_name());;
+        } else {
+            $dst = $target->create_volume($src->get_name(),
+                                          $src->get_format(),
+                                          $src->get_size(),
+                                          $src->is_sparse());
+        }
 
-        # Die if transfer required and no output target
-        die (user_message(__"No output target was specified"))
-            unless (defined($target));
+        # This will die if libguestfs can't use the result directly, so we do it
+        # before copying all the data.
+        push(@paths, $dst->get_local_path());
 
-        # Fetch the remote storage
-        my $vol = $transfer->transfer($self, $path, $target);
+        _volume_copy($src, $dst);
 
         # Export the new path
-        $path = $vol->get_path();
+        my $path = $dst->get_path();
 
         # Find any existing driver element.
         my ($driver) = $disk->findnodes('driver');
@@ -151,13 +206,13 @@ sub _storage_iterate
             $disk->appendChild($driver);
         }
         $driver->setAttribute('name', 'qemu');
-        $driver->setAttribute('type', $vol->get_format());
+        $driver->setAttribute('type', $dst->get_format());
 
         # Remove the @file or @dev attribute before adding a new one
         $source_e->removeAttributeNode($source);
 
         # Set @file or @dev as appropriate
-        if ($vol->is_block()) {
+        if ($dst->is_block()) {
             $disk->setAttribute('type', 'block');
             $source_e->setAttribute('dev', $path);
         } else {
@@ -165,7 +220,6 @@ sub _storage_iterate
             $source_e->setAttribute('file', $path);
         }
 
-        push(@paths, $path);
         push(@devices, $dev->getNodeValue());
     }
 
@@ -205,8 +259,8 @@ Please see the file COPYING.LIB for the full license.
 
 =head1 SEE ALSO
 
-L<Sys::VirtV2V::Connection::LibVirt(3pm)>,
-L<Sys::VirtV2V::Connection::LibVirtXML(3pm)>,
+L<Sys::VirtV2V::Source::LibVirt(3pm)>,
+L<Sys::VirtV2V::Source::LibVirtXML(3pm)>,
 L<virt-v2v(1)>,
 L<http://libguestfs.org/>.
 
diff --git a/lib/Sys/VirtV2V/Connection/Volume.pm b/lib/Sys/VirtV2V/Connection/Volume.pm
new file mode 100644
index 0000000..7961bd5
--- /dev/null
+++ b/lib/Sys/VirtV2V/Connection/Volume.pm
@@ -0,0 +1,179 @@
+# Sys::VirtV2V::Connection::Volume
+# Copyright (C) 2010 Red Hat Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+package Sys::VirtV2V::Connection::Volume;
+
+use strict;
+use warnings;
+
+use Data::Dumper;
+
+use Sys::VirtV2V::Util qw(user_message);
+
+use Locale::TextDomain 'virt-v2v';
+
+=pod
+
+=head1 NAME
+
+Sys::VirtV2V::Connection::Volume - Read and write storage volumes
+
+=head1 METHODS
+
+=over
+
+=item new(name, format, path, size, is_block, transfer)
+
+Create a new Volume which returns the given metadata, and uses I<transfer> for
+transferring data.
+
+=cut
+
+sub new
+{
+    my $class = shift;
+    my ($name, $format, $path, $size, $is_sparse, $is_block, $transfer) = @_;
+
+    my $self = {};
+    bless($self, $class);
+
+    $self->{name}      = $name;
+    $self->{format}    = $format;
+    $self->{path}      = $path;
+    $self->{size}      = $size;
+    $self->{is_sparse} = $is_block;
+    $self->{is_block}  = $is_block;
+    $self->{transfer}  = $transfer;
+
+    return $self;
+}
+
+=item get_size
+
+Return size for this Volume.
+
+=cut
+
+sub get_size
+{
+    return shift->{size};
+}
+
+=item get_format
+
+Return the on-disk format of this Volume.
+
+=cut
+
+sub get_format
+{
+    return shift->{format};
+}
+
+=item get_name
+
+Return the name of this Volume.
+
+=cut
+
+sub get_name
+{
+    return shift->{name};
+}
+
+=item get_path
+
+Return the native path of this Volume.
+
+=cut
+
+sub get_path
+{
+    return shift->{path};
+}
+
+=item get_local_path
+
+=cut
+
+sub get_local_path
+{
+    return shift->{transfer}->local_path();
+}
+
+=item is_block
+
+Return 1 if the volume is stored directly on a block device, 0 otherwise.
+
+=cut
+
+sub is_block
+{
+    return shift->{is_block};
+}
+
+=item is_sparse
+
+Return 1 if the volume is not fully allocated, 0 otherwise.
+
+=cut
+
+sub is_sparse
+{
+    return shift->{is_sparse};
+}
+
+=item get_read_stream
+
+Return a ReadStream for this volume.
+
+=cut
+
+sub get_read_stream
+{
+    return shift->{transfer}->get_read_stream();
+}
+
+=item get_write_stream
+
+Return a WriteStream for this volume.
+
+=cut
+
+sub get_write_stream
+{
+    return shift->{transfer}->get_write_stream();
+}
+
+=back
+
+=head1 COPYRIGHT
+
+Copyright (C) 2010 Red Hat Inc.
+
+=head1 LICENSE
+
+Please see the file COPYING.LIB for the full license.
+
+=head1 SEE ALSO
+
+L<virt-v2v(1)>,
+L<http://libguestfs.org/>.
+
+=cut
+
+1;
diff --git a/lib/Sys/VirtV2V/Transfer/ESX.pm b/lib/Sys/VirtV2V/Transfer/ESX.pm
index 798285b..5628a9b 100644
--- a/lib/Sys/VirtV2V/Transfer/ESX.pm
+++ b/lib/Sys/VirtV2V/Transfer/ESX.pm
@@ -15,17 +15,16 @@
 # License along with this library; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
-package Sys::VirtV2V::Transfer::ESX::UA;
-
 use strict;
 use warnings;
 
-use Sys::Virt::Error;
+package Sys::VirtV2V::Transfer::ESX::UA;
 
-use Sys::VirtV2V;
+use DateTime;
+use MIME::Base64;
 
+use Sys::VirtV2V;
 use Sys::VirtV2V::Util qw(user_message);
-
 use Locale::TextDomain 'virt-v2v';
 
 # This is a gross hack to bring sanity to Net::HTTPS's SSL handling. Net::HTTPS
@@ -37,37 +36,24 @@ use Locale::TextDomain 'virt-v2v';
 # will silently do nothing.
 #
 # To try to fix this situation, we hardcode here that we want Net::SSL. In the
-# _new constructor, we check that Net::SSL was actually used, and die() if it
-# wasn't. We subsequently only include configuration for Net::SSL.
+# constructor, we check that Net::SSL was actually used, and die() if it wasn't.
+# We subsequently only include configuration for Net::SSL.
 BEGIN {
     use Net::HTTPS;
 
     $Net::HTTPS::SSL_SOCKET_CLASS = "Net::SSL";
 }
 
-use LWP::UserAgent;
- at Sys::VirtV2V::Transfer::ESX::UA::ISA = qw(LWP::UserAgent);
-
-our %handles;
-
 sub new {
     my $class = shift;
+    my ($username, $password, $noverify) = @_;
 
-    my ($server, $username, $password, $target, $noverify) = @_;
-
-    my $self = $class->SUPER::new(
-        agent => 'virt-v2v/'.$Sys::VirtV2V::VERSION,
-        protocols_allowed => [ 'https' ]
-    );
+    my $self = {};
+    bless($self, $class);
 
-    # Older versions of LWP::UserAgent don't support show_progress
-    $self->show_progress(1) if ($self->can('show_progress'));
-
-    $self->{_v2v_server}   = $server;
-    $self->{_v2v_target}   = $target;
-    $self->{_v2v_username} = $username;
-    $self->{_v2v_password} = $password;
-    $self->{_v2v_noverify} = $noverify;
+    $self->{noverify} = $noverify;
+    $self->{agent}    = 'virt-v2v/'.$Sys::VirtV2V::VERSION;
+    $self->{auth}     = 'Basic '.encode_base64("$username:$password");
 
     if ($noverify) {
         # Unset HTTPS_CA_DIR if it is already set
@@ -79,213 +65,211 @@ sub new {
         $ENV{HTTPS_CA_DIR} = "" unless (exists($ENV{HTTPS_CA_DIR}));
     }
 
-    die("Invalid configuration of Net::HTTPS")
+    die('Invalid configuration of Net::HTTPS')
         unless(Net::HTTPS->isa('Net::SSL'));
 
     return $self;
 }
 
-sub get_volume
+sub _request
 {
     my $self = shift;
+    my ($method, $uri) = @_;
 
-    my ($path) = @_;
-
-    # Need to turn this:
-    #  [yellow:storage1] win2k3r2-32/win2k3r2-32.vmdk
-    # into this:
-    #  https://yellow.rhev.marston/folder/win2k3r2-32/win2k3r2-32-flat.vmdk? \
-    #  dcPath=ha-datacenter&dsName=yellow:storage1
-
-    $path =~ /^\[(.*)\]\s+(.*)\.vmdk$/
-        or die("Failed to parse ESX path: $path");
-    my $datastore = $1;
-    my $vmdk = $2;
-
-    my $url = _get_vol_url($self->{_v2v_server}, $vmdk, $datastore);
+    my $base = URI->new($uri->scheme.'://'.$uri->host);
+    my $conn = new Net::HTTPS(Host => $uri->host,
+                              MaxLineLength => 0)
+        or die(user_message(__x("Failed to connect to {host}: {error}",
+                                host => $uri->host,
+                                error => $@)));
 
-    # Replace / with _ so the vmdk name can be used as a volume name
-    my $volname = $vmdk;
-    $volname =~ s,/,_,g;
-    $self->{_v2v_volname} = $volname;
+    $conn->write_request($method => '/'.$uri->rel($base),
+                         'User-Agent' => $self->{agent},
+                         'Authorization' => $self->{auth})
+        or die(user_message(__x("Failed to send request to {host}: {error}",
+                                host => $uri->host,
+                                error => $@)));
 
-    my $target = $self->{_v2v_target};
-    if ($target->volume_exists($volname)) {
-        warn user_message(__x("WARNING: storage volume {name} already exists ".
-                              "on the target. NOT fetching it again. Delete ".
-                              "the volume and retry to download again.",
-                              name => $volname));
-        return $target->get_volume($volname);
-    }
+    my ($code, $msg, %h) = $conn->read_response_headers();
+    die([$code, $msg]) unless ($code == 200);
 
-    # Head request to get the size and create the volume
-    # We could do this with a single GET request. The problem with this is that
-    # you have to create the volume before writing to it. If the volume creation
-    # takes a very long time, the transfer may fail in the mean time.
-    my $retried = 0;
-    SIZE: for(;;) {
-        my $r = $self->head($url);
-        if ($r->is_success) {
-            $self->verify_certificate($r) unless ($self->{_v2v_noverify});
-            $self->create_volume($r);
-            last SIZE;
-        }
-
-        # If a disk is actually a snapshot image it will have '-00000n'
-        # appended to its name, e.g.:
-        #  [yellow:storage1] RHEL4-X/RHEL4-X-000003.vmdk
-        # The flat storage is still called RHEL4-X-flat, however.
-        # If we got a 404 and the vmdk name looks like it might be a snapshot,
-        # try again without the snapshot suffix.
-        # XXX: We're in flaky heuristic territory here. When the libvirt ESX
-        # driver implements the volume apis we should look for this information
-        # there instead.
-        elsif ($r->code == 404 && $retried == 0) {
-            $retried = 1;
-            if ($vmdk =~ /^(.*)-\d+$/) {
-                $vmdk = $1;
-                $url = _get_vol_url($self->{_v2v_server}, $vmdk, $datastore);
-            }
-        }
+    $self->_verify_certificate($conn, $uri->host) unless ($self->{noverify});
 
-        else {
-            $self->report_error($r);
-        }
-    }
-
-    $self->{_v2v_received} = 0;
-    my $r = $self->get($url,
-                    ':content_cb' => sub { $self->handle_data(@_); },
-                    ':read_size_hint' => 64 * 1024);
-
-    if ($r->is_success) {
-        # It reports success even if one of the callbacks died
-        my $died = $r->header('X-Died');
-        die($died) if (defined($died));
-
-        $self->verify_certificate($r) unless ($self->{_v2v_noverify});
-
-        # It reports success even if we didn't receive the whole file
-        die(user_message(__x("Didn't receive full volume. Received {received} ".
-                             "of {total} bytes.",
-                             received => $self->{_v2v_received},
-                             total => $self->{_v2v_volsize})))
-            unless ($self->{_v2v_received} == $self->{_v2v_volsize});
-
-        my $vol = $self->{_v2v_vol};
-        $vol->close();
-        return $vol;
-    }
-
-    $self->report_error($r);
+    return ($conn, \%h);
 }
 
-sub _get_vol_url
+sub get_content_length
 {
-    my ($server, $vmdk, $datastore) = @_;
+    my $self = shift;
+    my ($uri) = @_;
 
-    my $url = URI->new("https://".$server);
-    $url->path("/folder/$vmdk-flat.vmdk");
-    $url->query_form(dcPath => "ha-datacenter", dsName => $datastore);
+    my ($conn, $h) = $self->_request('HEAD', $uri);
 
-    return $url;
+    my $length = $h->{'Content-Length'};
+    die(user_message(__x("ESX Server didn't return content length ".
+                         "for {uri}",
+                         uri => $uri)))
+        unless (defined($length));
+
+    return $length;
 }
 
-sub report_error
+sub request_content
 {
     my $self = shift;
-    my ($r) = @_;
+    my ($uri) = @_;
 
-    if ($r->code == 401) {
-        die(user_message(__x("Authentication error connecting to ".
-                             "{server}. Used credentials for {username} ".
-                             "from .netrc.",
-                             server => $self->{_v2v_server},
-                             username => $self->{_v2v_username})))
-    }
+    my ($conn) = $self->_request('GET', $uri);
 
-    die(user_message(__x("Failed to connect to ESX server: {error}",
-                         error => $r->status_line)));
+    $self->{conn} = $conn;
+    $self->{hostname} = $uri->host;
 }
 
-sub get_basic_credentials
+sub read_content
 {
     my $self = shift;
+    my ($size) = @_;
+
+    my $conn = $self->{conn};
+    die("read_content called without request_content") unless (defined($conn));
+
+    my $buf;
+    my $rv;
+    do {
+        $rv = $conn->read_entity_body($buf, $size);
+    } while (defined($rv) && $rv == -1);
+    # We want to clean up and exit immediately on signals, and we don't set
+    # nonblocking on any socket, so EINTR and EAGAIN don't need to be handled
+    # here
 
-    my ($realm, $uri, $isproxy) = @_; # Not interested in any of these things
-                                      # because we only ever contact a single
-                                      # server in a single context
+    die(user_message(__x("Error reading data from {host}",
+                         host => $self->{hostname}))) unless (defined($rv));
 
-    return ($self->{_v2v_username}, $self->{_v2v_password});
+    return $buf;
 }
 
-sub handle_data
+sub _verify_certificate
 {
     my $self = shift;
+    my ($conn, $hostname) = @_;
 
-    my ($data, $response) = @_;
+    my $cert = $conn->get_peer_certificate();
 
-    # Verify the certificate of the get request the first time we're called
-    if ($self->{_v2v_received} == 0) {
-        $self->verify_certificate($response) unless ($self->{_v2v_noverify});
+    my $cn;
+    foreach my $i (split(/\//, $cert->subject_name)) {
+        next unless(length($i) > 0);
+        my ($key, $value) = split(/=/, $i);
+        $cn = lc($value) if (lc($key) eq 'cn');
+    }
+    die(user_message(__x("SSL Certificate Subject from {host} doesn't contain ".
+                         "a CN", host => $hostname))) unless (defined($cn));
+
+    $hostname = lc($hostname);
+    die(user_message(__x("Server {server} presented an SSL certificate ".
+                         "for {commonname}",
+                         server => $hostname,
+                         commonname => $cn)))
+        unless ($hostname eq $cn or $hostname !~ /\Q.$cn\E$/);
+
+    my $not_before = _parse_nottime($cert->not_before);
+    my $not_after  = _parse_nottime($cert->not_after);
+
+    my $now = DateTime->now;
+    if (DateTime->compare($now, $not_before) < 0) {
+        die(user_message(__x("SSL Certificate presented by {host} will not ".
+                             "be valid until {date}",
+                             host => $hostname,
+                             date => $not_before)));
     }
 
-    $self->{_v2v_received} += length($data);
-    $self->{_v2v_vol}->write($data);
+    if (DateTime->compare($now, $not_after) > 0) {
+        die(user_message(__x("SSL Certificate present by {host} expired on ".
+                             "{date}",
+                             host => $hostname,
+                             date => $not_after)));
+    }
+
+    # This should never happen, because we should instead get an SSL connection
+    # error Net::HTTPS
+    if (!$conn->get_peer_verify()) {
+        die("SSL Certificate not verified");
+    }
 }
 
-sub create_volume
+sub _parse_nottime
 {
-    my $self = shift;
+    my ($date) = @_;
+
+    $date =~ /^\s*(\d{4})-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)\s+(\S+)\s*$/
+        or die("Unrecognised date format: $date");
+
+    return new DateTime(
+        year      => $1,
+        month     => $2,
+        day       => $3,
+        hour      => $4,
+        minute    => $5,
+        second    => $6,
+        time_zone => $7
+    );
+}
 
-    my ($response) = @_;
 
-    my $target = $self->{_v2v_target};
+package Sys::VirtV2V::Transfer::ESX::ReadStream;
 
-    my $name = $self->{_v2v_volname};
-    die("create_volume called, but _v2v_volname is not set")
-        unless (defined($name));
+sub new
+{
+    my $class = shift;
+    my ($uri, $username, $password, $noverify, $error) = @_;
 
-    my $size = $response->content_length();
-    $self->{_v2v_volsize} = $size;
+    my $self = {};
+    bless($self, $class);
+
+    $self->{ua} = new Sys::VirtV2V::Transfer::ESX::UA(
+        $username,
+        $password,
+        $noverify
+    );
 
-    my $vol = $target->create_volume($name, $size);
-    $vol->open();
-    $self->{_v2v_vol} = $vol;
+    eval {
+        $self->{ua}->request_content($uri);
+    };
+    if ($@) {
+        if (ref($@) eq 'ARRAY') {
+            &$error($@->[0], $@->[1]);
+        } else {
+            die($@);
+        }
+    }
+
+    return $self;
 }
 
-sub verify_certificate
+sub read
 {
     my $self = shift;
+    my ($size) = @_;
 
-    my ($r) = @_;
-
-    # No point in trying to verify headers if the request failed anyway
-    return unless ($r->is_success);
-
-    my $subject = $r->header('Client-SSL-Cert-Subject');
-    die(user_message(__"Server response didn't include an SSL subject"))
-        unless ($subject);
+    return $self->{ua}->read_content($size);
+}
 
-    $subject =~ /\/CN=([^\/]*)/
-        or die(user_message(__x("SSL Certification Subject doesn't contain a ".
-                                "common name: {subject}",
-                                subject => $subject)));
-    my $cn = $1;
+sub close
+{
+    # Nothing required
+}
 
-    $self->{_v2v_server} =~ /(^|\.)\Q$cn\E$/
-        or die(user_message(__x("Server {server} presented an SSL certificate ".
-                                "for {commonname}",
-                                server => $self->{_v2v_server},
-                                commonname => $cn)));
+sub DESTROY
+{
+    shift->close();
 }
 
+
 package Sys::VirtV2V::Transfer::ESX;
 
 use Sys::Virt;
+use URI;
 
 use Sys::VirtV2V::Util qw(user_message);
-
 use Locale::TextDomain 'virt-v2v';
 
 =pod
@@ -294,66 +278,198 @@ use Locale::TextDomain 'virt-v2v';
 
 Sys::VirtV2V::Transfer::ESX - Transfer guest storage from an ESX server
 
-=head1 SYNOPSIS
+=head1 METHODS
 
- use Sys::VirtV2V::Transfer::ESX;
+=over
 
- $vol = Sys::VirtV2V::Transfer::ESX->transfer($conn, $path, $target);
+=item new(path, hostname, username, password, noverify, is_sparse)
 
-=head1 DESCRIPTION
+Return a new ESX Transfer object
 
-Sys::VirtV2V::Transfer::ESX retrieves guest storage devices from an ESX server.
+=cut
 
-=head1 METHODS
+sub new
+{
+    my $class = shift;
+    my ($path, $hostname, $username, $password, $noverify, $is_sparse) = @_;
 
-=over
+    die(user_message(__x("Authentication is required to connect to ".
+                         "{server} and no credentials were found in ".
+                         ".netrc.",
+                         server => $hostname)))
+        unless (defined($username));
+
+    my $self = {};
+    bless($self, $class);
+
+    $self->{hostname} = $hostname;
+    $self->{username} = $username;
+    $self->{password} = $password;
+    $self->{noverify} = $noverify;
+
+    my $ua = new Sys::VirtV2V::Transfer::ESX::UA(
+        $username,
+        $password,
+        $noverify
+    );
+
+    # ESX path looks like this:
+    #  [yellow:storage1] win2k3r2-32/win2k3r2-32.vmdk
+
+    # Strip out datastore and vmdk name
+    $path =~ /^\[(.*)\]\s+(.*)\.vmdk$/
+        or die("Failed to parse ESX path: $path");
+    my $datastore = $1;
+    my $vmdk = $2;
+
+    $self->{uri} = _get_vol_uri($hostname, $vmdk, $datastore);
+
+    # Get the size of the volume. At the same time, verify we have the correct
+    # URI.
+    my $retried = 0;
+    for(;;) {
+        eval {
+            $self->{size} = $ua->get_content_length($self->{uri});
+        };
+
+        last if defined($self->{size});
+
+        if ($@) {
+            # Re-throw an unstructured error
+            die ($@) if (ref($@) ne 'ARRAY');
+
+            my $code = $@->[0];
+            my $msg  = $@->[1];
+
+            my $r = $@;
+            # If a disk is actually a snapshot image it will have '-00000n'
+            # appended to its name, e.g.:
+            #   [yellow:storage1] RHEL4-X/RHEL4-X-000003.vmdk
+            # The flat storage is still called RHEL4-X-flat, however. If we got
+            # a 404 and the vmdk name looks like it might be a snapshot, try
+            # again without the snapshot suffix.
+            # XXX: We're in flaky heuristic territory here. When the libvirt ESX
+            # driver implements the volume apis we should look for this
+            # information there instead.
+            if ($code == 404 && !$retried && $vmdk =~ /^(.*)-\d+$/) {
+                $vmdk = $1;
+                $self->{uri} = _get_vol_uri($hostname, $vmdk, $datastore);
+            }
+
+            else {
+                $self->_report_error($code, $msg);
+            }
+
+            $retried = 1;
+        }
+    }
 
-=item transfer(conn, path, target)
+    # Create a libvirt-friendly volume name
+    $self->{name} = $vmdk;
+    $self->{name} =~ s,/,_,g;
 
-Transfer <path> from a remote ESX server. Server and authentication details will
-be taken from <conn>. Storage will be created using <target>.
+    return $self;
+}
+
+# Volume path looks like this:
+#  https://yellow.rhev.marston/folder/win2k3r2-32/win2k3r2-32-flat.vmdk? \
+#  dcPath=ha-datacenter&dsName=yellow:storage1
+sub _get_vol_uri
+{
+    my ($server, $vmdk, $datastore) = @_;
+
+    my $uri = URI->new("https://".$server);
+    $uri->path("/folder/$vmdk-flat.vmdk");
+    $uri->query_form(dcPath => "ha-datacenter", dsName => $datastore);
+
+    return $uri;
+}
+
+=item local_path
+
+ESX cannot return a local path. This function will die().
 
 =cut
 
-sub transfer
+sub local_path
 {
-    my $class = shift;
+    die(user_message(__"virt-v2v cannot write to an ESX connection"));
+}
 
-    my ($conn, $path, $target) = @_;
+=item get_read_stream
 
-    my $uri      = $conn->{uri};
-    my $username = $conn->{username};
-    my $password = $conn->{password};
+Get a read stream for this volume.
 
-    die("URI not defined for connection") unless (defined($uri));
+=cut
 
-    die(user_message(__x("Authentication is required to connect to ".
-                         "{server} and no credentials were found in ".
-                         ".netrc.",
-                         server => $conn->{hostname})))
-        unless (defined($username));
+sub get_read_stream
+{
+    my $self = shift;
+    return new Sys::VirtV2V::Transfer::ESX::ReadStream(
+        $self->{uri},
+        $self->{username},
+        $self->{password},
+        $self->{noverify},
+        sub { $self->_report_error(@_) }
+    );
+}
 
-    # Look for no_verify in the URI
-    my %query = $uri->query_form;
+=item get_write_stream
 
-    my $noverify = 0;
-    $noverify = 1 if (exists($query{no_verify}) && $query{no_verify} eq "1");
+get_write_stream is not implemented for ESX. This function will die with an
+error message if called.
 
-    # Initialise a user agent
-    my $ua = Sys::VirtV2V::Transfer::ESX::UA->new($conn->{hostname},
-                                                  $username,
-                                                  $password,
-                                                  $target,
-                                                  $noverify);
+=cut
+
+sub get_write_stream
+{
+    die(user_message(__"Unable to write to an ESX connection"));
+}
+
+=item esx_get_name
 
-    return $ua->get_volume($path);
+Return a libvirt-friendly name for this ESX path.
+
+=cut
+
+sub esx_get_name
+{
+    return shift->{name};
+}
+
+=item esx_get_size
+
+Return the size of the volume which will be returned.
+
+=cut
+
+sub esx_get_size
+{
+    return shift->{size};
+}
+
+sub _report_error
+{
+    my $self = shift;
+    my ($code, $msg) = @_;
+
+    if ($code == 401) {
+        die(user_message(__x("Authentication error connecting to ".
+                             "{server}. Used credentials for {username} ".
+                             "from .netrc.",
+                             server => $self->{hostname},
+                             username => $self->{username})))
+    }
+
+    die(user_message(__x("Failed to connect to ESX server: {error}",
+                         error => $msg)));
 }
 
 =back
 
 =head1 COPYRIGHT
 
-Copyright (C) 2009,2010 Red Hat Inc.
+Copyright (C) 2010 Red Hat Inc.
 
 =head1 LICENSE
 
@@ -361,7 +477,6 @@ Please see the file COPYING.LIB for the full license.
 
 =head1 SEE ALSO
 
-L<Sys::VirtV2V::Converter(3pm)>,
 L<virt-v2v(1)>,
 L<http://libguestfs.org/>.
 
diff --git a/lib/Sys/VirtV2V/Transfer/Local.pm b/lib/Sys/VirtV2V/Transfer/Local.pm
new file mode 100644
index 0000000..3207c4f
--- /dev/null
+++ b/lib/Sys/VirtV2V/Transfer/Local.pm
@@ -0,0 +1,238 @@
+# Sys::VirtV2V::Transfer::Local
+# Copyright (C) 2010 Red Hat Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+use strict;
+use warnings;
+
+package Sys::VirtV2V::Transfer::Local::ReadStream;
+
+use Sys::VirtV2V::Util qw(user_message);
+use Locale::TextDomain 'virt-v2v';
+
+sub new
+{
+    my $class = shift;
+    my ($path, $is_sparse) = @_;
+
+    my $self = {};
+    bless($self, $class);
+
+    my $fh;
+    open($fh, '<', $path)
+        or die(user_message(__x("Unable to open {path} for reading: {error}",
+                                path => $path,
+                                error => $!)));
+
+    $self->{fh} = $fh;
+    $self->{is_sparse} = $is_sparse;
+
+    return $self;
+}
+
+sub read
+{
+    my $self = shift;
+    my ($size) = @_;
+
+    my $buf;
+    my $in = read($self->{fh}, $buf, $size);
+    $self->_read_error($!) unless (defined($in));
+
+    return "" if ($in == 0);
+    return $buf;
+}
+
+sub close
+{
+    my $self = shift;
+    close($self->{fh}) or $self->_read_error($!);
+}
+
+sub DESTROY
+{
+    my $self = shift;
+    $self->close();
+}
+
+sub _read_error
+{
+    my $self = shift;
+    my ($error) = @_;
+
+    die(user_message(__x("Error reading from {path}: {error}",
+                         path => $self->{path},
+                         error => $error)));
+}
+
+
+package Sys::VirtV2V::Transfer::Local::WriteStream;
+
+use Sys::VirtV2V::Util qw(user_message);
+use Locale::TextDomain 'virt-v2v';
+
+sub new
+{
+    my $class = shift;
+    my ($path, $is_sparse) = @_;
+
+    my $self = {};
+    bless($self, $class);
+
+    my $fh;
+    open($fh, '>', $path)
+        or die(user_message(__x("Unable to open {path} for writing: {error}",
+                                path => $path,
+                                error => $!)));
+
+    $self->{fh} = $fh;
+    $self->{is_sparse} = $is_sparse;
+
+    return $self;
+}
+
+sub write
+{
+    my $self = shift;
+    my ($buf) = @_;
+
+    print { $self->{fh} } $buf or $self->_write_error($!);
+}
+
+sub close
+{
+    my $self = shift;
+    close($self->{fh}) or $self->_write_error($!);
+}
+
+sub _write_error
+{
+    my $self = shift;
+    my ($error) = @_;
+
+    die(user_message(__x("Error writing to {path}: {error}",
+                         path => $self->{path},
+                         error => $error)));
+}
+
+sub DESTROY
+{
+    my $self = shift;
+    $self->close();
+}
+
+
+package Sys::VirtV2V::Transfer::Local;
+
+use POSIX;
+use File::Spec;
+use File::stat;
+
+use Sys::VirtV2V::Util qw(user_message);
+use Locale::TextDomain 'virt-v2v';
+
+=pod
+
+=head1 NAME
+
+Sys::VirtV2V::Transfer::Local - Access local storage
+
+=head1 METHODS
+
+=over
+
+=item new(path, is_sparse)
+
+Create a new Local transfer object.
+
+=cut
+
+sub new
+{
+    my $class = shift;
+    my ($path, $is_sparse) = @_;
+
+    my $self = {};
+    bless($self, $class);
+
+    $self->{path}      = $path;
+    $self->{is_sparse} = $is_sparse;
+
+    return $self;
+}
+
+=item local_path
+
+Return a local path to the file.
+
+=cut
+
+sub local_path
+{
+    return shift->{path};
+}
+
+=item get_read_stream
+
+Get a stream to receive data from a local file.
+
+=cut
+
+sub get_read_stream
+{
+    my $self = shift;
+
+    return new Sys::VirtV2V::Transfer::Local::ReadStream(
+        $self->{path},
+        $self->{is_sparse}
+    );
+}
+
+=item get_write_stream
+
+Get a stream to write data to a local file.
+
+=cut
+
+sub get_write_stream
+{
+    my $self = shift;
+
+    return new Sys::VirtV2V::Transfer::Local::WriteStream(
+        $self->{path},
+        $self->{is_sparse}
+    );
+}
+
+=back
+
+=head1 COPYRIGHT
+
+Copyright (C) 2010 Red Hat Inc.
+
+=head1 LICENSE
+
+Please see the file COPYING.LIB for the full license.
+
+=head1 SEE ALSO
+
+L<Sys::VirtV2V::Converter(3pm)>,
+L<virt-v2v(1)>,
+L<http://libguestfs.org/>.
+
+=cut
+
+1;
diff --git a/lib/Sys/VirtV2V/Transfer/LocalCopy.pm b/lib/Sys/VirtV2V/Transfer/LocalCopy.pm
deleted file mode 100644
index cdff97e..0000000
--- a/lib/Sys/VirtV2V/Transfer/LocalCopy.pm
+++ /dev/null
@@ -1,150 +0,0 @@
-# Sys::VirtV2V::Transfer::LocalCopy
-# Copyright (C) 2010 Red Hat Inc.
-#
-# This library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2 of the License, or (at your option) any later version.
-#
-# This library 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
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-package Sys::VirtV2V::Transfer::LocalCopy;
-
-use POSIX;
-use File::Spec;
-use File::stat;
-
-use Sys::VirtV2V::Util qw(user_message);
-
-use Locale::TextDomain 'virt-v2v';
-
-=pod
-
-=head1 NAME
-
-Sys::VirtV2V::Transfer::LocalCopy - Copy a guest's local storage
-
-=head1 SYNOPSIS
-
- use Sys::VirtV2V::Transfer::LocalCopy;
-
- $vol = Sys::VirtV2V::Transfer::LocalCopy->transfer($conn, $path, $target);
-
-=head1 DESCRIPTION
-
-Sys::VirtV2V::Transfer::LocalCopy retrieves guest storage devices from local
-storage.
-
-=head1 METHODS
-
-=over
-
-=item transfer(conn, path, target)
-
-Transfer <path> from local storage. Storage will be created using <target>.
-
-=cut
-
-sub transfer
-{
-    my $class = shift;
-
-    my ($conn, $path, $target) = @_;
-
-    my (undef, undef, $name) = File::Spec->splitpath($path);
-
-    if ($target->volume_exists($name)) {
-        warn user_message(__x("WARNING: storage volume {name} already exists ".
-                              "on the target. NOT copying it again. Delete ".
-                              "the volume and retry to copy again.",
-                              name => $name));
-        return $target->get_volume($name);
-    }
-
-    my $fh;
-    open($fh, '<', $path)
-        or die(user_message(__x("Unable to open {path} for reading: {error}",
-                                path => $path,
-                                error => $!)));
-
-    my $st = stat($fh)
-        or die(user_message(__x("Unable to stat {path}: {error}",
-                                path => $path,
-                                error => $!)));
-
-    my $size;
-
-    # If it's a block device, use the output of blockdev command
-    if (S_ISBLK($st->mode)) {
-        my $blockdev;
-        open($blockdev, '-|', 'blockdev', '--getsize64', $path)
-            or die("Unable to execute blockdev: $!");
-
-        while (<$blockdev>) {
-            if (defined($size)) {
-                my $error = "blockdev returned multiple output lines:\n$size\n";
-                $error .= $_;
-                while(<$blockdev>) {
-                    $error .= $_;
-                }
-                die($error);
-            }
-            chomp;
-            $size = $_;
-        }
-
-        close($blockdev) or die("blockdev returned an error: $size");
-    }
-
-    # Otherwise use the size of the file directly
-    else {
-        $size = $st->size;
-    }
-
-    my $vol = $target->create_volume($name, $size);
-    $vol->open();
-
-    for (;;) {
-        my $buffer;
-        # Transfer in block chunks
-        my $in = sysread($fh, $buffer, $st->blksize);
-        die(user_message(__x("Error reading data from {path}: {error}",
-                             path => $path,
-                             error => $!))) if (!defined($in));
-
-        last if ($in == 0);
-
-        $vol->write($buffer);
-    }
-
-    $vol->close();
-
-    return $vol;
-}
-
-=back
-
-=head1 COPYRIGHT
-
-Copyright (C) 2010 Red Hat Inc.
-
-=head1 LICENSE
-
-Please see the file COPYING.LIB for the full license.
-
-=head1 SEE ALSO
-
-L<Sys::VirtV2V::Converter(3pm)>,
-L<virt-v2v(1)>,
-L<http://libguestfs.org/>.
-
-=cut
-
-1;
diff --git a/lib/Sys/VirtV2V/Transfer/SSH.pm b/lib/Sys/VirtV2V/Transfer/SSH.pm
index 5f54f0e..39eaa66 100644
--- a/lib/Sys/VirtV2V/Transfer/SSH.pm
+++ b/lib/Sys/VirtV2V/Transfer/SSH.pm
@@ -15,114 +15,21 @@
 # License along with this library; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
-package Sys::VirtV2V::Transfer::SSH;
+use strict;
+use warnings;
 
-use POSIX;
-use File::Spec;
-use File::stat;
+package Sys::VirtV2V::Transfer::SSH::Stream;
 
 use Sys::VirtV2V::Util qw(user_message);
-
 use Locale::TextDomain 'virt-v2v';
 
-=pod
-
-=head1 NAME
-
-Sys::VirtV2V::Transfer::SSH - Copy a remote guest's storage via ssh
-
-=head1 SYNOPSIS
-
- use Sys::VirtV2V::Transfer::SSH;
-
- $vol = Sys::VirtV2V::Transfer::SSH->transfer($conn, $path, $target);
-
-=head1 DESCRIPTION
-
-Sys::VirtV2V::Transfer::SSH retrieves guest storage devices from a remote server
-via SSH.
-
-=head1 METHODS
-
-=over
-
-=item transfer(conn, path, target)
-
-Transfer <path> from the remove server. Storage will be created using <target>.
-
-=cut
-
-sub transfer
+sub new
 {
     my $class = shift;
+    my ($hostname, $username, $command) = @_;
 
-    my ($conn, $path, $target) = @_;
-
-    my (undef, undef, $name) = File::Spec->splitpath($path);
-
-    if ($target->volume_exists($name)) {
-        warn user_message(__x("WARNING: storage volume {name} already exists ".
-                              "on the target. NOT copying it again. Delete ".
-                              "the volume and retry to copy again.",
-                              name => $name));
-        return $target->get_volume($name);
-    }
-
-    my $uri      = $conn->{uri};
-    my $username = $conn->{username};
-    my $password = $conn->{password};
-    my $host     = $conn->{hostname};
-
-    die("URI not defined for connection") unless (defined($uri));
-
-    my ($pid, $size, $fh, $error) =
-        _connect($host, $username, $path);
-
-    my $vol = $target->create_volume($name, $size);
-    $vol->open();
-
-    my $written = 0;
-    for (;;) {
-        my $buffer;
-        # Transfer in 8k chunks
-        my $in = read($fh, $buffer, 8 * 1024);
-        die(user_message(__x("Error reading data from {path}: {error}",
-                             path => $path,
-                             error => $!))) if (!defined($in));
-
-        last if ($in == 0);
-
-        $vol->write($buffer);
-        $written += length($buffer);
-    }
-
-    $vol->close();
-
-    die(user_message(__x("Didn't receive full volume. Received {received} ".
-                         "of {total} bytes.",
-                         received => $written,
-                         total => $size))) unless ($written == $size);
-
-    waitpid($pid, 0) == $pid or die("error reaping child: $!");
-    # If the child returned an error, check for anything on its stderr
-    if ($? != 0) {
-        my $msg = "";
-        while (<$error>) {
-            $msg .= $_;
-        }
-        die(user_message(__x("Unexpected error copying {path} from {host}. ".
-                             "Command output: {output}",
-                             path => $path,
-                             host => $uri->host,
-                             output => $msg)));
-    }
-
-    return $vol;
-}
-
-sub _connect
-{
-    my ($host, $username, $path) = @_;
+    my $self = {};
+    bless($self, $class);
 
     my ($stdin_read, $stdin_write);
     my ($stdout_read, $stdout_write);
@@ -137,22 +44,8 @@ sub _connect
         my @command;
         push(@command, 'ssh');
         push(@command, '-l', $username) if (defined($username));
-        push(@command, $host);
-
-        # Return the size of the remote path on the first line, followed by its
-        # contents.
-        # The bit arithmetic with the output of stat is a translation into shell
-        # of the S_ISBLK macro. If the remote device is a block device, stat
-        # will simply return the size of the block device inode. In this case,
-        # we use the output of blockdev --getsize64 instead.
-        push(@command,
-            "dev=$path; ".
-            'if [[ $(((0x$(stat -L -c %f $dev)&0170000)>>12)) == 6 ]]; then '.
-              'blockdev --getsize64 $dev; '.
-            'else '.
-              'stat -L -c %s $dev; '.
-            'fi; '.
-            'cat $dev');
+        push(@command, $hostname);
+        push(@command, $command);
 
         # Close the ends of the pipes we don't need
         close($stdin_write);
@@ -176,36 +69,271 @@ sub _connect
     close($stdout_write);
     close($stderr_write);
 
-    # Check that we don't get output on stderr before we read the file size
+    $self->{pid}    = $pid;
+    $self->{stdin}  = $stdin_write;
+    $self->{stdout} = $stdout_read;
+    $self->{stderr} = $stderr_read;
+
+    $self->{hostname} = $hostname;
+
+    return $self;
+}
+
+sub close
+{
+    my $self = shift;
+
+    # Nothing to do if it's already closed.
+    return unless (exists($self->{pid}));
+
+    my $pid    = $self->{pid};
+    my $stderr = $self->{stderr};
+
+    # Must close stdin before waitpid, or process will not exit when writing
+    close($self->{stdin});
+    close($self->{stdout});
+
+    waitpid($pid, 0) == $pid or die("error reaping child: $!");
+    # If the child returned an error, check for anything on its stderr
+    if ($? != 0) {
+        my $msg = "";
+        while (<$stderr>) {
+            $msg .= $_;
+        }
+        die(user_message(__x("Unexpected error copying {path} from {host}. ".
+                             "Command output: {output}",
+                             path => $self->{path},
+                             host => $self->{hostname},
+                             output => $msg)));
+    }
+
+    close($self->{stderr});
+
+    delete($self->{pid});
+    delete($self->{stdin});
+    delete($self->{stdout});
+    delete($self->{stderr});
+}
+
+sub DESTROY
+{
+    my $self = shift;
+    $self->close();
+}
+
+sub _check_stderr
+{
+    my $self = shift;
+    my ($rw, $errfun) = @_;
+
     for(;;) {
-        my ($rin, $rout);
+        my ($rin, $rout, $win, $wout);
         $rin = '';
-        vec($rin, fileno($stdout_read), 1) = 1;
-        vec($rin, fileno($stderr_read), 1) = 1;
+        vec($rin, fileno($self->{stderr}), 1) = 1;
+
+        # Waiting to read from stdout
+        if ($rw == 0) {
+            $win = undef;
+            vec($rin, fileno($self->{stdout}), 1) = 1;
+        }
+
+        # Waiting to write to stdin
+        else {
+            $win = '';
+            vec($win, fileno($self->{stdin}), 1) = 1;
+        }
 
-        my $nfound = select($rout=$rin, undef, undef, undef);
+        my $nfound = select($rout=$rin, $wout=$win, undef, undef);
         die("select failed: $!") if ($nfound < 0);
 
-        if (vec($rout, fileno($stderr_read), 1) == 1) {
-            my $stderr = '';
-            while(<$stderr_read>) {
-                $stderr .= $_;
+        my $stderr = $self->{stderr};
+        if (vec($rout, fileno($stderr), 1) == 1) {
+            my $error = '';
+            while(<$stderr>) {
+                $error .= $_;
             }
 
-            die(user_message(__x("Unexpected error getting {path}: ".
-                                 "{output}",
-                                 path => $path, output => $stderr)));
+            &$errfun($error);
         }
 
-        if (vec($rout, fileno($stdout_read), 1) == 1) {
-            last;
-        }
+        last if (vec($rout, fileno($self->{stdout}), 1) == 1 ||
+                 vec($wout, fileno($self->{stdin}), 1) == 1);
     }
+}
+
+package Sys::VirtV2V::Transfer::SSH::ReadStream;
 
-    # First line returned is the output of stat
-    my $size = <$stdout_read>;
+use Sys::VirtV2V::Util qw(user_message);
+use Locale::TextDomain 'virt-v2v';
 
-    return ($pid, $size, $stdout_read, $stderr_read);
+ at Sys::VirtV2V::Transfer::SSH::ReadStream::ISA =
+    qw(Sys::VirtV2V::Transfer::SSH::Stream);
+
+sub new
+{
+    my $class = shift;
+    my ($path, $hostname, $username, $is_sparse) = @_;
+
+    my $self = $class->SUPER::new($hostname, $username, "dd if=$path");
+
+    $self->{path} = $path;
+
+    # Check that the stream becomes readable without anything on stderr
+    $self->SUPER::_check_stderr(0, sub { $self->_read_error(@_) } );
+
+    return $self;
+}
+
+sub read
+{
+    my $self = shift;
+    my ($size) = @_;
+
+    my $buf;
+    my $in = read($self->{stdout}, $buf, $size);
+    $self->_read_error($!) unless (defined($in));
+
+    return "" if ($in == 0);
+    return $buf;
+}
+
+sub _read_error
+{
+    my $self = shift;
+    my ($error) = @_;
+
+    die(user_message(__x("Error reading from {path}: {error}",
+                         path => $self->{path},
+                         error => $error)));
+}
+
+package Sys::VirtV2V::Transfer::SSH::WriteStream;
+
+use Sys::VirtV2V::Util qw(user_message);
+use Locale::TextDomain 'virt-v2v';
+
+ at Sys::VirtV2V::Transfer::SSH::WriteStream::ISA =
+    qw(Sys::VirtV2V::Transfer::SSH::Stream);
+
+sub new
+{
+    my $class = shift;
+    my ($path, $hostname, $username, $is_sparse) = @_;
+
+    my $self = $class->SUPER::new($hostname, $username, "dd of=$path");
+
+    $self->{path} = $path;
+
+    # Check that the stream becomes writable without anything on stderr
+    $self->SUPER::_check_stderr(1, sub { $self->_write_error(@_) });
+
+    return $self;
+}
+
+sub write
+{
+    my $self = shift;
+    my ($buf) = @_;
+
+    print { $self->{stdin} } $buf or $self->_write_error($!);
+}
+
+sub _write_error
+{
+    my $self = shift;
+    my ($error) = @_;
+
+    die(user_message(__x("Error writing data to {path}: {error}",
+                         path => $self->{path},
+                         error => $error)));
+}
+
+package Sys::VirtV2V::Transfer::SSH;
+
+use POSIX;
+use File::Spec;
+use File::stat;
+
+use Sys::VirtV2V::Util qw(user_message);
+use Locale::TextDomain 'virt-v2v';
+
+=pod
+
+=head1 NAME
+
+Sys::VirtV2V::Transfer::SSH - Transfer data over an SSH connection
+
+=head1 METHODS
+
+=over
+
+=item new(path, hostname, username, is_sparse)
+
+Create a new SSH transfer object.
+
+=cut
+
+sub new
+{
+    my $class = shift;
+    my ($path, $hostname, $username, $is_sparse) = @_;
+
+    my $self = {};
+    bless($self, $class);
+
+    $self->{path}      = $path;
+    $self->{hostname}  = $hostname;
+    $self->{username}  = $username;
+    $self->{is_sparse} = $is_sparse;
+
+    return $self;
+}
+
+=item local_path
+
+SSH cannot currently return a local path. This function will die().
+
+=cut
+
+sub local_path
+{
+    die(user_message(__"virt-v2v cannot yet write to an SSH connection"));
+}
+
+=item get_read_stream
+
+Get a stream to read from a file over an SSH connection.
+
+=cut
+
+sub get_read_stream
+{
+    my $self = shift;
+
+    return new Sys::VirtV2V::Transfer::SSH::ReadStream(
+        $self->{path},
+        $self->{hostname},
+        $self->{username},
+        $self->{is_sparse}
+    );
+}
+
+=item get_write_stream
+
+Get a stream to write to a file over an SSH connection.
+
+=cut
+
+sub get_write_stream
+{
+    my $self = shift;
+
+    return new Sys::VirtV2V::Transfer::SSH::WriteStream(
+        $self->{path},
+        $self->{hostname},
+        $self->{username},
+        $self->{is_sparse}
+    );
 }
 
 =back
diff --git a/lib/Sys/VirtV2V/Util.pm b/lib/Sys/VirtV2V/Util.pm
index 699899a..4aab8af 100644
--- a/lib/Sys/VirtV2V/Util.pm
+++ b/lib/Sys/VirtV2V/Util.pm
@@ -20,13 +20,16 @@ package Sys::VirtV2V::Util;
 use strict;
 use warnings;
 
+use Sys::Virt;
+use XML::DOM;
+
 use Locale::TextDomain 'virt-v2v';
 
 require Exporter;
 use vars qw(@EXPORT_OK @ISA);
 
 @ISA = qw(Exporter);
- at EXPORT_OK = qw(augeas_error user_message);
+ at EXPORT_OK = qw(augeas_error user_message parse_libvirt_volinfo);
 
 =pod
 
@@ -128,6 +131,42 @@ sub user_message
     return __x("virt-v2v: {message}\n", message => $msg);
 }
 
+=item parse_libvirt_volinfo(vol)
+
+Return name, format, size, is_sparse, is_block for a given a libvirt volume.
+
+=cut
+
+sub parse_libvirt_volinfo
+{
+    my ($vol) = @_;
+
+    my $voldom = new XML::DOM::Parser->parse($vol->get_xml_description());
+
+    my ($name, $format, $size, $is_sparse, $is_block);
+
+    ($name) = $voldom->findnodes('/volume/name/text()');
+    $name = $name->getData();
+
+    ($format) = $voldom->findnodes('/volume/target/format/@type');
+    $format = $format->getValue();
+
+    my $info = $vol->get_info();
+
+    $size = $info->{capacity};
+
+    my $allocation = $info->{allocation};
+    if ($allocation < $size) {
+        $is_sparse = 1;
+    } else {
+        $is_sparse = 0;
+    }
+
+    $is_block = $info->{type} == Sys::Virt::StorageVol::TYPE_BLOCK ? 1 : 0;
+
+    return ($name, $format, $size, $is_sparse, $is_block);
+}
+
 =back
 
 =head1 COPYRIGHT
diff --git a/v2v/virt-v2v.pl b/v2v/virt-v2v.pl
index 93bfcd5..c6a2e55 100755
--- a/v2v/virt-v2v.pl
+++ b/v2v/virt-v2v.pl
@@ -32,10 +32,10 @@ use Sys::Guestfs::Lib qw(get_partitions inspect_all_partitions
 use Sys::VirtV2V;
 use Sys::VirtV2V::Config;
 use Sys::VirtV2V::Converter;
-use Sys::VirtV2V::Connection::LibVirt;
-use Sys::VirtV2V::Connection::LibVirtXML;
-use Sys::VirtV2V::Target::LibVirt;
-use Sys::VirtV2V::Target::RHEV;
+use Sys::VirtV2V::Connection::LibVirtSource;
+use Sys::VirtV2V::Connection::LibVirtTarget;
+use Sys::VirtV2V::Connection::LibVirtXMLSource;
+use Sys::VirtV2V::Connection::RHEVTarget;
 use Sys::VirtV2V::ExecHelper;
 use Sys::VirtV2V::GuestfsHandle;
 use Sys::VirtV2V::GuestOS;
@@ -282,7 +282,8 @@ if ($output_method eq "libvirt") {
                 -exitval => 1 })
         unless (defined($output_pool));
 
-    $target = new Sys::VirtV2V::Target::LibVirt($output_uri, $output_pool);
+    $target = new Sys::VirtV2V::Connection::LibVirtTarget($output_uri,
+                                                          $output_pool);
 }
 
 elsif ($output_method eq "rhev") {
@@ -291,7 +292,7 @@ elsif ($output_method eq "rhev") {
                 -exitval => 1 })
         unless (defined($output_storage_domain));
 
-    $target = new Sys::VirtV2V::Target::RHEV($output_storage_domain);
+    $target = new Sys::VirtV2V::Connection::RHEVTarget($output_storage_domain);
 }
 
 else {
@@ -299,48 +300,41 @@ else {
                          output => $output_method)));
 }
 
-# Get an appropriate Connection
-my $conn;
-eval {
-    if ($input_method eq "libvirtxml") {
-        my $path = shift(@ARGV) or
-            pod2usage({ -message => user_message(__"You must specify a filename"),
-                        -exitval => 1 });
-
-        # Warn if we were given more than 1 argument
-        if(scalar(@_) > 0) {
-            print STDERR user_message
-                (__x("WARNING: {modulename} only takes a single filename.",
-                     modulename => 'libvirtxml'));
-        }
-
-        $conn = Sys::VirtV2V::Connection::LibVirtXML->new($path, $target);
+# Get an appropriate Source
+my $source;
+if ($input_method eq "libvirtxml") {
+    my $path = shift(@ARGV) or
+        pod2usage({ -message => user_message(__"You must specify a filename"),
+                    -exitval => 1 });
+
+    # Warn if we were given more than 1 argument
+    if(scalar(@_) > 0) {
+        warn user_message
+            (__x("WARNING: {modulename} only takes a single filename.",
+                 modulename => 'libvirtxml'));
     }
 
-    elsif ($input_method eq "libvirt") {
-        my $name = shift(@ARGV) or
-            pod2usage({ -message => user_message(__"You must specify a guest"),
-                        -exitval => 1 });
+    $source = Sys::VirtV2V::Connection::LibVirtXMLSource->new($path);
+}
 
-        $conn = Sys::VirtV2V::Connection::LibVirt->new($input_uri, $name,
-                                                       $target);
+elsif ($input_method eq "libvirt") {
+    my $name = shift(@ARGV) or
+        pod2usage({ -message => user_message(__"You must specify a guest"),
+                    -exitval => 1 });
 
-        # Warn if we were given more than 1 argument
-        if(scalar(@_) > 0) {
-            print STDERR user_message
-                (__x("WARNING: {modulename} only takes a single domain name.",
-                     modulename => 'libvirt'));
-        }
-    }
+    $source = Sys::VirtV2V::Connection::LibVirtSource->new($input_uri, $name);
 
-    else {
-        print STDERR user_message __x("{input} is not a valid input method",
-                                      input => $input_method);
-        exit(1);
+    # Warn if we were given more than 1 argument
+    if(scalar(@_) > 0) {
+        warn user_message
+            (__x("WARNING: {modulename} only takes a single domain name.",
+                 modulename => 'libvirt'));
     }
-};
-if ($@) {
-    print STDERR $@;
+}
+
+else {
+    warn user_message(__x("{input} is not a valid input method",
+                                  input => $input_method));
     exit(1);
 }
 
@@ -348,12 +342,20 @@ if ($@) {
 ###############################################################################
 ## Start of processing
 
+# Check that the guest doesn't already exist on the target
+die(user_message(__x("Domain {name} already exists on the target.",
+                     name => $source->get_name)))
+    if ($target->guest_exists($source->get_name()));
+
+# Copy source storage to target
+$source->copy_storage($target);
+
 # Get a libvirt configuration for the guest
-my $dom = $conn->get_dom();
+my $dom = $source->get_dom();
 exit(1) unless(defined($dom));
 
 # Get a list of the guest's transfered storage devices
-my $storage = $conn->get_storage_paths();
+my $storage = $source->get_storage_paths();
 
 # Create the transfer iso if required
 my $transferiso;
@@ -375,7 +377,7 @@ eval {
     # Modify the guest and its metadata
     $guestcaps = Sys::VirtV2V::Converter->convert($g, $guestos,
                                                   $config, $dom, $os,
-                                                  $conn->get_storage_devices());
+                                                  $source->get_storage_devices());
 };
 
 # If any of the above commands result in failure, we need to ensure that the
-- 
1.7.2.3




More information about the Libguestfs mailing list