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