[Libguestfs] [PATCH 3/8] ESX: Import guests from VMware's ESX server

Matthew Booth mbooth at redhat.com
Mon Feb 1 17:23:31 UTC 2010


This change adds the ability to import a guest and its storage from VMware's ESX
server using the LibVirt connection.

An example command line:

virt-v2v -ic 'esx://yellow.marston/?no_verify=1' -op transfer RHEL5-64

This will import the guest RHEL5-64 from esx server yellow.marston, copying its
storage to a local pool called transfer.

Sys::VirtV2V::Connection is refactored to be a superclass. Subclasses are now
created explicitly by virt-v2v.pl rather than using a generic instantiate
mechanism.

Sys::VirtV2V::Connection::LibVirt knows explicitly about ESX, and will use the
new Sys::VirtV2V::Transfer::ESX to fetch its storage.

virt-v2v.pl is updated to reflect the other changes.
---
 MANIFEST                                 |    1 +
 lib/Sys/VirtV2V/Connection.pm            |  160 +++++++++-----
 lib/Sys/VirtV2V/Connection/LibVirt.pm    |  185 ++++++++++-------
 lib/Sys/VirtV2V/Connection/LibVirtXML.pm |   99 ++-------
 lib/Sys/VirtV2V/Transfer/ESX.pm          |  344 ++++++++++++++++++++++++++++++
 v2v/virt-v2v.pl                          |  140 ++++++++----
 6 files changed, 676 insertions(+), 253 deletions(-)
 create mode 100644 lib/Sys/VirtV2V/Transfer/ESX.pm

diff --git a/MANIFEST b/MANIFEST
index 7bfad76..3d6bf00 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -12,6 +12,7 @@ lib/Sys/VirtV2V/Connection.pm
 lib/Sys/VirtV2V/Connection/LibVirt.pm
 lib/Sys/VirtV2V/Connection/LibVirtXML.pm
 lib/Sys/VirtV2V/UserMessage.pm
+lib/Sys/VirtV2V/Transfer/ESX.pm
 MANIFEST			This list of files
 MANIFEST.SKIP
 META.yml
diff --git a/lib/Sys/VirtV2V/Connection.pm b/lib/Sys/VirtV2V/Connection.pm
index 31fbdb3..6a967d0 100644
--- a/lib/Sys/VirtV2V/Connection.pm
+++ b/lib/Sys/VirtV2V/Connection.pm
@@ -20,113 +20,163 @@ package Sys::VirtV2V::Connection;
 use strict;
 use warnings;
 
-use Module::Pluggable sub_name => 'modules',
-                      search_path => ['Sys::VirtV2V::Connection'],
-                      require => 1;
+use Sys::Virt;
 
-use Carp;
+use Locale::TextDomain 'virt-v2v';
 
 =pod
 
 =head1 NAME
 
-Sys::VirtV2V::Connection - Read a variety of guest metadata formats
+Sys::VirtV2V::Connection - Obtain domain metadata
 
 =head1 SYNOPSIS
 
- use Sys::VirtV2V::Connection;
+ use Sys::VirtV2V::Connection::LibVirt;
 
- $reader = Sys::VirtV2V::Connection->instantiate("libvirtxml", $uri,
-                                                     $config, @args);
- exit 1 unless($mdr->is_configured());
- $dom = $reader->get_dom();
+ $conn = Sys::VirtV2V::Connection::LibVirt->new($uri, $name, $pool);
+ $dom = $conn->get_dom();
+ @storage = $conn->get_local_storage();
 
 =head1 DESCRIPTION
 
-Sys::VirtV2V::Connection reads the metadata of a, possibly foreign,
-guest. It provides the DOM representation of an equivalent libvirt XML
-representation.
+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::Connection is an interface to various backends, each of
-which implement a consistent API. Sys::VirtV2V::Connection itself only
-implements methods to access backends.
+ Sys::VirtV2V::Connection::LibVirt
+ Sys::VirtV2V::Connection::LibVirtXML
 
 =head1 METHODS
 
 =over
 
-=item instantiate(name, $uri, $config, @args)
+=item get_local_storage
 
-=over
-
-=item name
+Return a list of the domain's storage devices. The returned list contains local
+paths.
 
-The name of the module to instantiate.
+=cut
 
-=item uri
+sub get_local_storage
+{
+    my $self = shift;
 
-A URI describing the target connection.
+    return @{$self->{storage}};
+}
 
-=item config
+=item get_dom()
 
-A parsed virt-v2v configuration file.
+Returns an XML::DOM::Document describing a libvirt configuration equivalent to
+the input.
 
-=item args
+Returns undef and displays an error if there was an error
 
-Backend-specific arguments describing where its data is located.
+=cut
 
-=back
+sub get_dom
+{
+    my $self = shift;
 
-Instantiate a backend instance with the given name.
+    return $self->{dom};
+}
 
-=cut
 
-sub instantiate
+# Iterate over returned storage. Transfer it and update DOM as necessary. To be
+# called by subclasses.
+sub _storage_iterate
 {
-    my $class = shift;
+    my $self = shift;
+
+    my ($transfer, $pool) = @_;
+
+    my $dom = $self->get_dom();
 
-    my ($name, $uri, $config, @args) = @_;
+    # Create a hash of guest devices to their paths
+    my @storage;
+    foreach my $disk ($dom->findnodes('/domain/devices/disk')) {
+        my ($source_e) = $disk->findnodes('source');
 
-    defined($name) or carp("instantiate called without name argument");
-    defined($uri)  or carp("instantiate called without uri argument");
-    defined($config) or carp("instantiate called without config argument");
+        my ($source) = $source_e->findnodes('@file | @dev');
+        defined($source) or die("source element has neither dev nor file: \n".
+                                $dom.toString());
 
-    foreach my $module ($class->modules()) {
-        if($module->get_name() eq $name) {
-            return $module->_new($uri, $config->{$name}, @args);
+        my ($target) = $disk->findnodes('target/@dev');
+        defined($target) or die("disk does not have a target device: \n".
+                                $dom.toString());
+
+        # If the disk is a floppy or a cdrom, blank its source
+        my $device = $disk->getAttribute('device');
+        if ($device eq 'floppy' || $device eq 'cdrom') {
+            $source_e->setAttribute($source->getName(), '');
         }
-    }
 
-    return undef;
-}
+        else {
+            my $path = $source->getValue();
 
-=back
+            if (defined($transfer)) {
+                # Die if transfer required and no output pool
+                die (user_message(__"No output pool was specified"))
+                    unless (defined($pool));
 
-=head1 BACKEND INTERFACE
+                # Fetch the remote storage
+                my $vol = $transfer->transfer($self, $path, $pool);
 
-=over
+                # Parse the XML description of the returned volume
+                my $voldom =
+                    new XML::DOM::Parser->parse($vol->get_xml_description());
 
-=item CLASS->get_name()
+                # Find any existing driver element.
+                my ($driver) = $disk->findnodes('driver');
 
-Return the module's name.
+                # Create a new driver element if none exists
+                unless (defined($driver)) {
+                    $driver =
+                        $disk->getOwnerDocument()->createElement("driver");
+                    $disk->appendChild($driver);
+                }
+                $driver->setAttribute('name', 'qemu');
 
-=item is_configured()
+                # Get the volume format for passing to the qemu driver
+                my ($format) =
+                    $voldom->findnodes('/volume/target/format/@type');
 
-Return 1 if the module has been suffiently configured to proceed.
-Return 0 and display an error message otherwise.
+                $format = $format->getValue() if (defined($format));
 
-=item get_dom()
+                # Auto-detect if no format is specified explicitly
+                $format ||= 'auto';
 
-Returns an XML::DOM::Document describing a libvirt configuration equivalent to
-the input.
+                $driver->setAttribute('type', $format);
 
-Returns undef and displays an error if there was an error
+                # Remove the @file or @dev attribute before adding a new one
+                $source_e->removeAttributeNode($source);
+
+                $path = $vol->get_path();
+
+                # Set @file or @dev as appropriate
+                if ($vol->get_info()->{type} ==
+                    Sys::Virt::StorageVol::TYPE_FILE)
+                {
+                    $disk->setAttribute('type', 'file');
+                    $source_e->setAttribute('file', $path);
+                } else {
+                    $disk->setAttribute('type', 'block');
+                    $source_e->setAttribute('dev', $path);
+                }
+            }
+
+            push(@storage, $path);
+        }
+    }
+
+    $self->{storage} = \@storage;
+}
 
 =back
 
 =head1 COPYRIGHT
 
-Copyright (C) 2009 Red Hat Inc.
+Copyright (C) 2009,2010 Red Hat Inc.
 
 =head1 LICENSE
 
diff --git a/lib/Sys/VirtV2V/Connection/LibVirt.pm b/lib/Sys/VirtV2V/Connection/LibVirt.pm
index d3aa80d..59e4913 100644
--- a/lib/Sys/VirtV2V/Connection/LibVirt.pm
+++ b/lib/Sys/VirtV2V/Connection/LibVirt.pm
@@ -20,10 +20,17 @@ package Sys::VirtV2V::Connection::LibVirt;
 use strict;
 use warnings;
 
+use Sys::VirtV2V::Connection;
+our @ISA = ("Sys::VirtV2V::Connection");
+
+use Net::Netrc;
+use URI;
 use XML::DOM;
 
 use Sys::Virt;
 
+use Sys::VirtV2V;
+use Sys::VirtV2V::Transfer::ESX;
 use Sys::VirtV2V::UserMessage qw(user_message);
 
 use Locale::TextDomain 'virt-v2v';
@@ -36,107 +43,133 @@ Sys::VirtV2V::Connection::LibVirt - Read libvirt metadata from libvirtd
 
 =head1 SYNOPSIS
 
- use Sys::VirtV2V::Connection;
+ use Sys::VirtV2V::Connection::LibVirt;
 
- $reader = Sys::VirtV2V::Connection->instantiate
-    ("libvirt", "xen+ssh://xenserver.example.com/", $config, @args);
- $dom = $reader->get_dom();
+ $conn = Sys::VirtV2V::Connection::LibVirt->new
+    ("xen+ssh://xenserver.example.com/", $name, $pool);
+ $dom = $conn->get_dom();
 
 =head1 DESCRIPTION
 
-Sys::VirtV2V::Connection::LibVirt is a backend for
+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
 
-See BACKEND INTERFACE in L<Sys::VirtV2V::Connection> for a detailed
-description of its exported methods.
-
 =over
 
-=cut
+=item new(uri, name, pool)
 
-use constant NAME => "libvirt";
+Create a new Sys::VirtV2V::Connection::LibVirt. Domain I<name> will be
+downloaded from I<uri>. Remote storage will be copied to a new volume, which
+will be create in <pool>.
+
+=cut
 
-sub _new
+sub new
 {
     my $class = shift;
 
-    my ($uri, $config, @args) = @_;
+    my ($uri, $name, $pool) = @_;
 
     my $self = {};
 
     bless($self, $class);
 
-    my @vmm_params = (auth => 1);
-    push(@vmm_params, url => $uri) if defined($uri);
-    my $vmm = Sys::Virt->new(@vmm_params);
+    $self->{uri} = URI->new($uri);
+    $self->{name} = $name;
 
-    $self->{vmm} = $vmm;
-    $self->_handle_args(@args);
+    # Parse uri authority for hostname and username
+    $self->{uri}->authority() =~ /^(?:([^:]*)(?::([^@]*))?@)?(.*)$/
+        or die(user_message(__x("Unable to parse URI authority: {auth}",
+                                auth => $self->{uri}->authority())));
 
-    return $self;
-}
+    my $username = $self->{username} = $1;
+    my $hostname = $self->{hostname} = $3;
 
-sub _handle_args
-{
-    my $self = shift;
+    print STDERR user_message(__("WARNING: Specifying a password in the ".
+                                 "connection URI is not supported. It has ".
+                                 "been ignored.")) if (defined($2));
 
-    # The first argument is the name of a libvirt domain
-    $self->{name} = shift;
+    # Look for credentials in .netrc if the URI contains a hostname
+    if (defined($hostname)) {
+        if (defined($username)) {
+            my $mach = Net::Netrc->lookup($hostname, $username);
+            $self->{password} = $mach->password if (defined($mach));
+        }
 
-    # 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 => NAME));
-    }
-}
+        else {
+            my $mach = Net::Netrc->lookup($hostname);
 
-=item Sys::VirtV2V::Connection::LibVirtXML->get_name()
+            if (defined($mach)) {
+                $self->{username} = $mach->login;
+                $self->{password} = $mach->password;
+            }
+        }
+    }
 
-See BACKEND INTERFACE in L<Sys::VirtV2V::Connection> for details.
+    my $sourcevmm;
+    eval {
+        $sourcevmm = Sys::Virt->new(
+            uri => $uri,
+            readonly => 1,
+            auth => 1,
+            credlist => [
+                Sys::Virt::CRED_AUTHNAME,
+                Sys::Virt::CRED_PASSPHRASE
+            ],
+            callback => sub {
+                my $creds = shift;
+
+                foreach my $cred (@$creds) {
+                    if ($cred->{type} == Sys::Virt::CRED_AUTHNAME) {
+                        $cred->{result} = $self->{username};
+                    }
+
+                    elsif ($cred->{type} == Sys::Virt::CRED_PASSPHRASE) {
+                        $cred->{result} = $self->{password};
+                    }
+
+                    else { die($cred->{type}, "\n"); }
+                }
+            }
+        );
+    };
+    die(user_message(__x("Failed to connect to {uri}: {error}",
+                         uri => $uri,
+                         error => $@->stringify()))) if ($@);
 
-=cut
+    $self->{sourcevmm} = $sourcevmm;
 
-sub get_name
-{
-    my $class = shift;
+    $self->_check_shutdown();
 
-    return NAME;
-}
+    $self->_get_dom();
 
-=item is_configured()
+    my $transfer;
+    if ($self->{uri}->scheme eq "esx") {
+        $transfer = "Sys::VirtV2V::Transfer::ESX";
+    }
 
-See BACKEND INTERFACE in L<Sys::VirtV2V::Connection> for details.
+    $self->_storage_iterate($transfer, $pool);
 
-=cut
+    return $self;
+}
 
-sub is_configured
+sub _check_shutdown
 {
     my $self = shift;
 
     my $vmm = $self->{vmm};
-    my $name = $self->{name};
-
-    # Check the given domain exists
-    my $domain = _get_domain($vmm, $name);
-
-    # Don't continue if it isn't
-    return 0 unless(defined($domain));
+    my $domain = $self->_get_domain();
 
     # Check the domain is shutdown
-    unless ($domain->get_info()->{state} == Sys::Virt::Domain::STATE_SHUTOFF) {
-        print STDERR user_message
-            (__x("Guest {name} is currently {state}. ".
-                 "It must be shut down first.",
-                 state => _state_string($domain->get_info()->{state}),
-                 name => $name));
-        return 0;
-    }
-
-    return 1;
+    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
@@ -164,30 +197,30 @@ sub _state_string
 
 sub _get_domain
 {
-    my ($vmm, $name) = @_;
+    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 ($@);
 
-    # Warn and exit if we didn't find it
-    unless($domain) {
-        print STDERR user_message
-            (__x("{name} isn't a valid guest name", name => $name));
-    }
+    # Check we found it
+    die(user_message(__x("{name} isn't a valid guest name", name => $name)))
+        unless($domain);
+
+    $self->{domain} = $domain;
 
     return $domain;
 }
 
-=item get_dom()
-
-See BACKEND INTERFACE in L<Sys::VirtV2V::Connection> for details.
-
-=cut
-
-sub get_dom
+sub _get_dom
 {
     my $self = shift;
 
@@ -195,13 +228,15 @@ sub get_dom
     my $name = $self->{name};
 
     # Lookup the domain
-    my $domain = _get_domain($vmm, $name);
+    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();
-    return new XML::DOM::Parser->parse($xml);
+
+    my $dom = new XML::DOM::Parser->parse($xml);
+    $self->{dom} = $dom;
 }
 
 =back
diff --git a/lib/Sys/VirtV2V/Connection/LibVirtXML.pm b/lib/Sys/VirtV2V/Connection/LibVirtXML.pm
index c05aa0f..6867a9b 100644
--- a/lib/Sys/VirtV2V/Connection/LibVirtXML.pm
+++ b/lib/Sys/VirtV2V/Connection/LibVirtXML.pm
@@ -20,6 +20,8 @@ package Sys::VirtV2V::Connection::LibVirtXML;
 use strict;
 use warnings;
 
+our @ISA = ("Sys::VirtV2V::Connection");
+
 use XML::DOM;
 use XML::DOM::XPath;
 
@@ -35,34 +37,33 @@ Sys::VirtV2V::Connection::LibVirtXML - Read libvirt XML from a file
 
 =head1 SYNOPSIS
 
- use Sys::VirtV2V::Connection;
+ use Sys::VirtV2V::Connection::LibVirtXML;
 
- $reader = Sys::VirtV2V::Connection->instantiate("libvirtxml", undef,
-                                                     $config, @args);
- $dom = $reader->get_dom();
+ $conn = Sys::VirtV2V::Connection::LibVirtXML->new($config, $path);
+ $dom = $conn->get_dom();
 
 =head1 DESCRIPTION
 
-Sys::VirtV2V::Connection::LibVirtXML is a backend for
+Sys::VirtV2V::Connection::LibVirtXML is an implementation of
 Sys::VirtV2V::Connection which reads libvirt XML guest descriptions from a
 file.
 
 =head1 METHODS
 
-See BACKEND INTERFACE in L<Sys::VirtV2V::Connection> for a detailed
-description of its exported methods.
-
 =over
 
-=cut
+=item new(config, path)
 
-use constant NAME => "libvirtxml";
+Create a new LibVirtXML connection. Configuration for transforming the metadata
+is taken from I<config>, and the metadata itself is read from I<path>.
+
+=cut
 
-sub _new
+sub new
 {
     my $class = shift;
 
-    my ($uri, $config, @args) = @_;
+    my ($config, $path) = @_;
 
     my %obj = ();
     my $self = \%obj;
@@ -87,77 +88,22 @@ sub _new
             }
 
             else {
-                print STDERR user_message
-                    (__x("WARNING: unknown configuration directive ".
-                         "{directive} in {name} section.",
-                         directive => $directive, name => NAME));
-                $self->{invalidconfig} = 1;
+                die(__x("WARNING: unknown configuration directive ".
+                        "{directive} in {name} section.",
+                        directive => $directive, name => 'libvirtxml'));
             }
         }
     }
 
-    $self->_handle_args(@args);
-
-    return $self;
-}
-
-sub _handle_args
-{
-    my $self = shift;
-
-    # The first argument is the libvirt xml file's path
-    $self->{path} = shift;
-
-    # 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 => NAME));
-    }
-}
-
-=item Sys::VirtV2V::Connection::LibVirtXML->get_name()
-
-See BACKEND INTERFACE in L<Sys::VirtV2V::Connection> for details.
-
-=cut
-
-sub get_name
-{
-    my $class = shift;
-
-    return NAME;
-}
-
-=item is_configured()
-
-See BACKEND INTERFACE in L<Sys::VirtV2V::Connection> for details.
-
-=cut
-
-sub is_configured
-{
-    my $self = shift;
-
-    if(!defined($self->{path})) {
-        print STDERR user_message
-            (__x("You must specify a filename when using {modulename}",
-                 modulename => NAME));
-        return 0;
-    }
+    $self->_get_dom($path);
 
-    return 0 if(exists($self->{invalidconfig}));
+    # No transfer methods defined yet
+    $self->_storage_iterate(undef, undef);
 
-    return 1;
+    return $self;
 }
 
-=item get_dom()
-
-See BACKEND INTERFACE in L<Sys::VirtV2V::Connection> for details.
-
-=cut
-
-sub get_dom
+sub _get_dom
 {
     my $self = shift;
 
@@ -212,7 +158,7 @@ sub get_dom
 
 =head1 COPYRIGHT
 
-Copyright (C) 2009 Red Hat Inc.
+Copyright (C) 2009,2010 Red Hat Inc.
 
 =head1 LICENSE
 
@@ -220,7 +166,6 @@ Please see the file COPYING.LIB for the full license.
 
 =head1 SEE ALSO
 
-L<Sys::VirtV2V::Connection(3)>,
 L<virt-v2v(1)>,
 L<v2v-snapshot(1)>,
 L<http://libguestfs.org/>.
diff --git a/lib/Sys/VirtV2V/Transfer/ESX.pm b/lib/Sys/VirtV2V/Transfer/ESX.pm
new file mode 100644
index 0000000..353c10d
--- /dev/null
+++ b/lib/Sys/VirtV2V/Transfer/ESX.pm
@@ -0,0 +1,344 @@
+# Sys::VirtV2V::Transfer::ESX
+# 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::ESX::UA;
+
+use strict;
+use warnings;
+
+use Sys::Virt::Error;
+
+use Sys::VirtV2V;
+
+use Sys::VirtV2V::UserMessage qw(user_message);
+
+use Locale::TextDomain 'virt-v2v';
+
+# This is a gross hack to bring sanity to Net::HTTPS's SSL handling. Net::HTTPS
+# can use either Net::SSL or IO::Socket::SSL, depending on which is available at
+# runtime. It does not expose which of these it is using, or provide any common
+# interface for configuring them. Neither of these libraries will verify a peer
+# certificate by default. The configuration required to ensure certificates are
+# verified is custom to the driver in use. If the wrong driver is configured, it
+# 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.
+BEGIN {
+    use Net::HTTPS;
+
+    $Net::HTTPS::SSL_SOCKET_CLASS = "Net::SSL";
+}
+
+use LWP::UserAgent;
+our @ISA = ("LWP::UserAgent");
+
+our %handles;
+
+sub new {
+    my $class = shift;
+
+    my ($server, $username, $password, $pool, $verify) = @_;
+
+    my $self = $class->SUPER::new(
+        agent => 'virt-v2v/'.$Sys::VirtV2V::VERSION,
+        protocols_allowed => [ 'https' ]
+    );
+    $self->show_progress(1);
+
+    $self->add_handler(response_header => sub {
+        my ($response, $self, $h) = @_;
+
+        if ($response->is_success) {
+            $self->verify_certificate($response) if ($verify);
+            $self->create_volume($response);
+        }
+    });
+
+    $self->{_v2v_server}   = $server;
+    $self->{_v2v_pool}     = $pool;
+    $self->{_v2v_username} = $username;
+    $self->{_v2v_password} = $password;
+
+    if ($verify) {
+        # Leave HTTPS_CA_DIR alone if it is already set
+        # Setting HTTPS_CA_DIR to the empty string results in it using the
+        # compiled-in default paths
+        $ENV{HTTPS_CA_DIR} = "" unless (exists($ENV{HTTPS_CA_DIR}));
+    } else {
+        # Unset HTTPS_CA_DIR if it is already set
+        delete($ENV{HTTPS_CA_DIR});
+    }
+
+    die("Invalid configuration of Net::HTTPS")
+        unless(Net::HTTPS->isa('Net::SSL'));
+
+    return $self;
+}
+
+sub get_volume
+{
+    my $self = shift;
+
+    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 = URI->new("https://".$self->{_v2v_server});
+    $url->path("/folder/$vmdk-flat.vmdk");
+    $url->query_form(dcPath => "ha-datacenter", dsName => $datastore);
+
+    # Replace / with _ so the vmdk name can be used as a volume name
+    $self->{_v2v_volname} = $vmdk;
+    $self->{_v2v_volname} =~ s,/,_,g;
+
+    # Check to see if this volume already exists
+    eval {
+        my $pool = $self->{_v2v_pool};
+        $self->{_v2v_vol} = $pool->get_volume_by_name($self->{_v2v_volname});
+    };
+
+    # The above command should generate VIR_ERR_NO_STORAGE_VOL because the
+    # volume doesn't exist
+    unless($@ && $@->code == Sys::Virt::Error::ERR_NO_STORAGE_VOL) {
+        unless ($@) {
+            print STDERR user_message(__x("WARNING: storage volume {name} ".
+                                          "already exists in the target ".
+                                          "pool. NOT fetching it again. ".
+                                          "Delete the volume and retry to ".
+                                          "download again.",
+                                          name => $self->{_v2v_volname}));
+            return $self->{_v2v_vol};
+        }
+
+        # We got an error, but not the one we expected
+        die(user_message(__x("Unexpected error accessing storage pool: ",
+                             "{error}", error => $@->stringify())));
+    }
+
+    my $r = $self->SUPER::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));
+
+        # Close the volume file descriptor
+        close($self->{_v2v_volfh});
+        return $self->{_v2v_vol};
+    }
+
+    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})))
+    }
+
+    die(user_message(__x("Failed to connect to ESX server: {error}",
+                         error => $r->status_line)));
+}
+
+sub get_basic_credentials
+{
+    my $self = shift;
+
+    my ($realm, $uri, $isproxy) = @_; # Not interested in any of these things
+                                      # because we only ever contact a single
+                                      # server in a single context
+
+    return ($self->{_v2v_username}, $self->{_v2v_password});
+}
+
+sub handle_data
+{
+    my $self = shift;
+
+    my ($data, $response) = @_;
+
+    my $volfh = $self->{_v2v_volfh};
+
+    syswrite($volfh, $data)
+        or die(user_message(__x("Error writing to {path}: {error}",
+                                path => $self->{_v2v_volpath},
+                                error => $!)));
+}
+
+sub create_volume
+{
+    my $self = shift;
+
+    my ($response) = @_;
+
+    my $pool = $self->{_v2v_pool};
+
+    # Create a volume in the target storage pool of the correct size
+    my $name = $self->{_v2v_volname};
+    die("create_volume called, but _v2v_volname is not set")
+        unless (defined($name));
+
+    my $size = $response->content_length();
+
+    my $vol_xml = "
+        <volume>
+            <name>$name</name>
+            <capacity>$size</capacity>
+        </volume>
+    ";
+
+    my $volume;
+    eval {
+        $volume = $pool->create_volume($vol_xml);
+    };
+    die(user_message(__x("Failed to create storage volume: {error}",
+                         error => $@->stringify()))) if ($@);
+    $self->{_v2v_vol} = $volume;
+
+    # Open the volume for writing
+    open(my $volfh, '>', $volume->get_path())
+        or die(user_message(__x("Error opening storage volume {path} ".
+                                "for writing: {error}", error => $!)));
+
+    $self->{_v2v_volfh} = $volfh;
+}
+
+sub verify_certificate
+{
+    my $self = shift;
+
+    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);
+
+    $subject =~ /\/CN=([^\/]*)/
+        or die(user_message(__x("SSL Certification Subject doesn't contain a ".
+                                "common name: {subject}",
+                                subject => $subject)));
+    my $cn = $1;
+
+    $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)));
+}
+
+package Sys::VirtV2V::Transfer::ESX;
+
+use Sys::Virt;
+
+use Sys::VirtV2V::UserMessage qw(user_message);
+
+use Locale::TextDomain 'virt-v2v';
+
+=pod
+
+=head1 NAME
+
+Sys::VirtV2V::Transfer::ESX - Transfer guest storage from an ESX server
+
+=head1 SYNOPSIS
+
+ use Sys::VirtV2V::Transfer::ESX;
+
+ $vol = Sys::VirtV2V::Transfer::ESX->transfer($conn, $path, $pool);
+
+=head1 DESCRIPTION
+
+Sys::VirtV2V::Transfer::ESX retrieves guest storage devices from an ESX server.
+
+=head1 METHODS
+
+=over
+
+=item transfer(conn, path, pool)
+
+Transfer <path> from a remote ESX server. Server and authentication details will
+be taken from <conn>. Storage will be copied to a new volume created in <pool>.
+
+=cut
+
+sub transfer
+{
+    my $class = shift;
+
+    my ($conn, $path, $pool) = @_;
+
+    my $uri      = $conn->{uri};
+    my $username = $conn->{username};
+    my $password = $conn->{password};
+
+    die("URI not defined for connection")      unless (defined($uri));
+
+    die(user_message(__x("Authentication is required to connect to ".
+                         "{server} and no credentials were found in ".
+                         ".netrc.",
+                         server => $conn->{hostname})))
+        unless (defined($username));
+
+    # Look for no_verify in the URI
+    my %query = $uri->query_form;
+
+    my $noverify = 0;
+    $noverify = 1 if (exists($query{no_verify}) && $query{no_verify} eq "1");
+
+    # Initialise a user agent
+    my $ua = Sys::VirtV2V::Transfer::ESX::UA->new($conn->{hostname},
+                                                  $username,
+                                                  $password,
+                                                  $pool);
+
+    return $ua->get_volume($path);
+}
+
+=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<virt-v2v(1)>,
+L<v2v-snapshot(1)>,
+L<http://libguestfs.org/>.
+
+=cut
+
+1;
diff --git a/v2v/virt-v2v.pl b/v2v/virt-v2v.pl
index 82f16ce..b097ccd 100755
--- a/v2v/virt-v2v.pl
+++ b/v2v/virt-v2v.pl
@@ -33,7 +33,8 @@ use Sys::Guestfs::Lib qw(open_guest get_partitions inspect_all_partitions
 use Sys::VirtV2V;
 use Sys::VirtV2V::GuestOS;
 use Sys::VirtV2V::Converter;
-use Sys::VirtV2V::Connection;
+use Sys::VirtV2V::Connection::LibVirt;
+use Sys::VirtV2V::Connection::LibVirtXML;
 use Sys::VirtV2V::UserMessage qw(user_message);
 
 =encoding utf8
@@ -133,12 +134,20 @@ Guest argument is the path to an XML file describing a libvirt domain.
 
 =cut
 
-my $input_uri;
+my $input_uri = "qemu:///system";
 
 =item B<-ic URI>
 
 Specifies the connection to use when using the libvirt input method. If omitted,
-then we connect to the default libvirt hypervisor.
+this defaults to qemu:///system.
+
+=cut
+
+my $input_transport;
+
+=item B<-it method>
+
+Species the transport method used to obtain raw storage from the source guest.
 
 =cut
 
@@ -151,9 +160,18 @@ ommitted, this defaults to qemu:///system.
 
 =cut
 
+my $output_pool;
+
+=item B<-op pool>
+
+Specifies the pool which will be used to create new storage for the converted
+guest.
+
+=cut
+
 my $config_file;
 
-=item B<-f file>, B<--config file>
+=item B<-f file>
 
 Load the virt-v2v configuration from I<file>. There is no default.
 
@@ -189,6 +207,7 @@ GetOptions ("help|?"      => sub {
             "i=s"         => \$input_method,
             "ic=s"        => \$input_uri,
             "oc=s"        => \$output_uri,
+            "op=s"        => \$output_pool,
             "f|config=s"  => \$config_file
 ) or pod2usage(2);
 
@@ -208,33 +227,89 @@ if(defined($config_file)) {
     }
 }
 
+# Connect to target libvirt
+my $vmm = Sys::Virt->new(
+    auth => 1,
+    uri => $output_uri
+);
+
 # Get an appropriate Connection
-my $mdr = Sys::VirtV2V::Connection->instantiate($input_method, $input_uri,
-                                                    $config, @ARGV);
-if(!defined($mdr)) {
-    print STDERR user_message __x("{input} is not a valid input method",
-                                  input => $input_method);
+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($config, $path);
+    }
+
+    elsif ($input_method eq "libvirt") {
+        my $name = shift(@ARGV) or
+            pod2usage({ -message => user_message(__"You must specify a guest"),
+                        -exitval => 1 });
+
+        # Get a handle to the output pool if one is defined
+        my $pool;
+        if (defined($output_pool)) {
+            eval {
+                $pool = $vmm->get_storage_pool_by_name($output_pool);
+            };
+
+            if ($@) {
+                print STDERR user_message
+                    (__x("Output pool {poolname} is not a valid local ".
+                         "storage pool",
+                         poolname => $output_pool));
+                exit(1);
+            }
+        }
+
+        $conn = Sys::VirtV2V::Connection::LibVirt->new($input_uri, $name,
+                                                       $pool);
+
+        # 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'));
+        }
+    }
+
+    else {
+        print STDERR user_message __x("{input} is not a valid input method",
+                                      input => $input_method);
+        exit(1);
+    }
+};
+if ($@) {
+    print STDERR $@;
     exit(1);
 }
 
-# Check Connection is properly initialised
-exit 1 unless($mdr->is_configured());
-
 # Configure GuestOS ([files] and [deps] sections)
 Sys::VirtV2V::GuestOS->configure($config);
 
+
 ###############################################################################
 ## Start of processing
 
 # Get a libvirt configuration for the guest
-my $dom = $mdr->get_dom();
+my $dom = $conn->get_dom();
 exit(1) unless(defined($dom));
 
-# Get a list of the guest's storage devices
-my @devices = get_guest_devices($dom);
+# Get a list of the guest's transfered storage devices
+my @storage = $conn->get_local_storage();
 
-# Open a libguestfs handle on the guest's devices
-my $g = get_guestfs_handle(@devices);
+# Open a libguestfs handle on the guest's storage devices
+my $g = get_guestfs_handle(@storage);
 
 # Inspect the guest
 my $os = inspect_guest($g);
@@ -242,11 +317,6 @@ my $os = inspect_guest($g);
 # Instantiate a GuestOS instance to manipulate the guest
 my $guestos = Sys::VirtV2V::GuestOS->instantiate($g, $os);
 
-# Connect to target libvirt
-my @vmm_params = (auth => 1);
-push(@vmm_params, uri => $output_uri) if(defined($output_uri));
-my $vmm = Sys::Virt->new(@vmm_params);
-
 # Modify the guest and its metadata for the target hypervisor
 Sys::VirtV2V::Converter->convert($vmm, $guestos, $dom, $os);
 
@@ -255,15 +325,14 @@ $g->sync();
 
 $vmm->define_domain($dom->toString());
 
+exit(0);
 
 ###############################################################################
 ## Helper functions
 
 sub get_guestfs_handle
 {
-    my @params = \@_; # Initialise parameters with list of devices
-
-    my $g = open_guest(@params, rw => 1);
+    my $g = open_guest(\@_, rw => 1);
 
     # Mount the transfer iso if GuestOS needs it
     my $transferiso = Sys::VirtV2V::GuestOS->get_transfer_iso();
@@ -328,27 +397,6 @@ sub inspect_guest
     return $os;
 }
 
-sub get_guest_devices
-{
-    my $dom = shift;
-
-    my @devices;
-    foreach my $source ($dom->findnodes('/domain/devices/disk/source')) {
-        my $attrs = $source->getAttributes();
-
-        # Get either dev or file, whichever is defined
-        my $attr = $attrs->getNamedItem("dev");
-        $attr = $attrs->getNamedItem("file") if(!defined($attr));
-
-        defined($attr) or die("source element has neither dev nor file: ".
-                              $source.toString());
-
-        push(@devices, $attr->getValue());
-    }
-
-    return @devices;
-}
-
 =head1 PREPARING TO RUN VIRT-V2V
 
 =head2 Backup the guest
-- 
1.6.6




More information about the Libguestfs mailing list