[pve-devel] [PATCH qemu-server v3 3/4] Add a new command line option 'ovfimport', to create VMs from an OVF manifest

Emmanuel Kasper e.kasper at proxmox.com
Wed Mar 29 16:53:14 CEST 2017


Currently the following extracted paramaters are used to create a VM:
     * VM name
     * Memory
     * Number of cores
---
 PVE/CLI/qm.pm         |  47 +++++++++++++++
 PVE/QemuServer/OVF.pm | 161 +++++++++++++++++++++++++-------------------------
 control.in            |   2 +-
 3 files changed, 130 insertions(+), 80 deletions(-)

diff --git a/PVE/CLI/qm.pm b/PVE/CLI/qm.pm
index 44439dd..12d705c 100755
--- a/PVE/CLI/qm.pm
+++ b/PVE/CLI/qm.pm
@@ -17,10 +17,12 @@ use PVE::SafeSyslog;
 use PVE::INotify;
 use PVE::RPCEnvironment;
 use PVE::QemuServer;
+use PVE::QemuServer::OVF;
 use PVE::API2::Qemu;
 use JSON;
 use PVE::JSONSchema qw(get_standard_option);
 use Term::ReadLine;
+use Data::Dumper;
 
 use PVE::CLIHandler;
 
@@ -432,6 +434,48 @@ __PACKAGE__->register_method ({
 	return undef;
     }});
 
+__PACKAGE__->register_method ({
+    name => 'ovfimport',
+    path => 'ovfimport',
+    description => "Create a new VM using parameters read from an OVF manifest",
+    parameters => {
+	additionalProperties => 0,
+	properties => {
+	    vmid => get_standard_option('pve-vmid', { completion => \&PVE::QemuServer::complete_vmid }),
+	    dryrun => {
+		type => 'boolean',
+		description => 'Print a text representation of the extracted OVF parameters, but do not create a VM',
+		optional => 1,
+		},
+	    manifest => {
+		type => 'string'
+		}
+	},
+    },
+    returns => {
+	type => 'string',
+    },
+    code => sub {
+	my ($param) = @_;
+	my $vmid = $param->{vmid};
+	my $ovf_file = PVE::Tools::extract_param($param, 'manifest');
+	my $dryrun = PVE::Tools::extract_param($param, 'dryrun');
+
+	my $parsed = PVE::QemuServer::OVF::parse_ovf($ovf_file);
+
+	if ($dryrun) {
+	    print Dumper($parsed);
+	    exit(0);
+	}
+
+	$param->{name} = $parsed->{qm}->{name} if defined($parsed->{qm}->{name});
+	$param->{memory} = $parsed->{qm}->{memory} if defined($parsed->{qm}->{memory});
+	$param->{cores} = $parsed->{qm}->{cores} if defined($parsed->{qm}->{cores});
+	$param->{node} = $nodename;
+
+	PVE::API2::Qemu->create_vm($param);
+    }
+});
 
 my $print_agent_result = sub {
     my ($data) = @_;
@@ -587,6 +631,9 @@ our $cmddef = {
     nbdstop => [ __PACKAGE__, 'nbdstop', ['vmid']],
 
     terminal => [ __PACKAGE__, 'terminal', ['vmid']],
+
+    ovfimport => [ __PACKAGE__, 'ovfimport', ['vmid']],
+
 };
 
 1;
diff --git a/PVE/QemuServer/OVF.pm b/PVE/QemuServer/OVF.pm
index bdca880..5f8e3ca 100644
--- a/PVE/QemuServer/OVF.pm
+++ b/PVE/QemuServer/OVF.pm
@@ -5,13 +5,15 @@ package PVE::QemuServer::OVF;
 use strict;
 use warnings;
 
-use XML::Simple;
-use PVE::Tools;
+use XML::LibXML;
 use File::Spec;
 use File::Basename;
 use Data::Dumper;
 
-# map OVF resource types to descriptive strings
+use PVE::Tools;
+use PVE::Storage::Plugin;
+
+# map OVF resources types to descriptive strings
 # this will allow us to explore the xml tree without using magic numbers
 # http://schemas.dmtf.org/wbem/cim-html/2/CIM_ResourceAllocationSettingData.html
 my @resources = (
@@ -34,7 +36,7 @@ my @resources = (
     { id => 17, dtmf_name => 'Disk Drive' },
     { id => 18, dtmf_name => 'Tape Drive' },
     { id => 19, dtmf_name => 'Storage Extent' },
-    { id => 20, dtmf_name => 'Other storage device', pve_type => 'sata' }, # yes
+    { id => 20, dtmf_name => 'Other storage device', pve_type => 'sata'},
     { id => 21, dtmf_name => 'Serial port' },
     { id => 22, dtmf_name => 'Parallel port' },
     { id => 23, dtmf_name => 'USB Controller' },
@@ -86,11 +88,15 @@ sub id_to_pve {
 sub parse_ovf {
     my ($ovf, $debug) = @_;
 
-    my $parser = XML::Simple->new();
-    my $xml = PVE::Tools::file_get_contents($ovf);
-    my $tree;
-    eval { $tree = $parser->XMLin($xml) };
-    die $@ if $@;
+    my $dom = XML::LibXML->load_xml(location => $ovf, no_blanks => 1);
+
+    # register the xml namespaces in a xpath context object
+    # 'ovf' is the default namespace so it will prepended to each xml element
+    my $xpc = XML::LibXML::XPathContext->new($dom);
+    $xpc->registerNs('ovf', 'http://schemas.dmtf.org/ovf/envelope/1');
+    $xpc->registerNs('rasd', 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData');
+    $xpc->registerNs('vssd', 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData');
+
 
     # hash to save qm.conf parameters
     my $qm;
@@ -98,36 +104,35 @@ sub parse_ovf {
     #array to save a disk list
     my @disks;
 
-    my $ovf_name = $tree->{VirtualSystem}->{Name};
-    $ovf_name =~ s/[^a-zA-Z0-9\-]//g; # PVE::QemuServer::confdesc requires a valid DNS name
-    if (!($qm->{name} = $ovf_name)) {
+    # easy xpath
+    # walk down the dom until we find the matching XML element
+    my $xpath_find_name = "/ovf:Envelope/ovf:VirtualSystem/ovf:Name";
+    my $ovf_name = $xpc->findvalue($xpath_find_name);
+
+    if ($ovf_name) {
+	($qm->{name} = $ovf_name) =~ s/[^a-zA-Z0-9\-]//g; # PVE::QemuServer::confdesc requires a valid DNS name
+    } else {
 	warn "warning: unable to parse the VM name in this OVF manifest, generating a default value\n";
     }
 
-    my $scsi_id = dtmf_name_to_id('Parallel SCSI HBA');
-    my $ide_id = dtmf_name_to_id('IDE Controller');
-    my $sata_id = dtmf_name_to_id('Other storage device');
+    # middle level xpath
+    # element[child] search the elements which have this [child]
+    my $processor_id = dtmf_name_to_id('Processor');
+    my $xpath_find_vcpu_count = "/ovf:Envelope/ovf:VirtualSystem/ovf:VirtualHardwareSection/ovf:Item[rasd:ResourceType=${processor_id}]/rasd:VirtualQuantity";
+    $qm->{'cores'} = $xpc->findvalue($xpath_find_vcpu_count);
 
-    my @virtual_hardware_items = @{ $tree->{VirtualSystem}->{VirtualHardwareSection}->{Item} };
-    my @controllers_items = grep { $_->{'rasd:ResourceType'} =~ /^(?:\Q$scsi_id\E|\Q$ide_id\E|\Q$sata_id\E)$/ } @virtual_hardware_items;
+    my $memory_id = dtmf_name_to_id('Memory');
+    my $xpath_find_memory = ("/ovf:Envelope/ovf:VirtualSystem/ovf:VirtualHardwareSection/ovf:Item[rasd:ResourceType=${memory_id}]/rasd:VirtualQuantity");
+    $qm->{'memory'} = $xpc->findvalue($xpath_find_memory);
 
+    # middle level xpath
+    # here we expect multiple results, so we do not read the element value with
+    # findvalue() but store multiple elements with findnodes()
     my $disk_id = dtmf_name_to_id('Disk Drive');
-    my @disk_items = grep { $_->{'rasd:ResourceType'} =~ /\Q$disk_id\E/ } @virtual_hardware_items;
+    my $xpath_find_disks="/ovf:Envelope/ovf:VirtualSystem/ovf:VirtualHardwareSection/ovf:Item[rasd:ResourceType=${disk_id}]";
+    my @disk_items = $xpc->findnodes($xpath_find_disks);
 
-    foreach my $item_node (@virtual_hardware_items) {
-	if ($item_node->{'rasd:ResourceType'} == dtmf_name_to_id('Processor')) {
-	    if (!($qm->{'cores'} = $item_node->{'rasd:VirtualQuantity'})) {
-		warn "warning: unable to count the number of cores in this OVF manifest\n";
-	    }
-	}
-	elsif ($item_node->{'rasd:ResourceType'} == dtmf_name_to_id('Memory')) {
-	    if (!($qm->{'memory'} = $item_node->{'rasd:VirtualQuantity'})) {
-		warn "warning: unable to parse memory in this OVF manifest n";
-	    }
-	}
-    }
-
-    # disks metadata is split in four different xml nodes:
+    # disks metadata is split in four different xml elements:
     # * as an Item node of type DiskDrive in the VirtualHardwareSection
     # * as an Disk node in the DiskSection
     # * as a File node in the References section
@@ -135,8 +140,8 @@ sub parse_ovf {
     #
     # we iterate over the list of Item nodes of type disk drive, and for each item,
     # find the corresponding Disk node, and File node and owning controller
-    # when all the nodes has been found out, we extract the relevant information to
-    # the $pve_disk hash ref, which we push to @disks;
+    # when all the nodes has been found out, we copy the relevant information to
+    # a $pve_disk hash ref, which we push to @disks;
 
     foreach my $item_node (@disk_items) {
 	
@@ -145,8 +150,11 @@ sub parse_ovf {
 	my $controller_node;
 	my $pve_disk;
 
+	print "disk item:\n", $item_node->toString(1), "\n" if $debug;
+
 	# from Item, find corresponding Disk node
-	my $host_resource = $item_node->{'rasd:HostResource'};
+	# here the dot means the search should start from the current element in dom
+	my $host_resource = $item_node->findvalue('./rasd:HostResource');
 	my $disk_section_path;
 	my $disk_id;
 	if ($host_resource =~ m|^ovf:/(.+)/(.+)|) {
@@ -156,59 +164,54 @@ sub parse_ovf {
 	   warn "invalid host ressource $host_resource, skipping\n";
 	   next;
 	}
-	print "disk section path and disk id: ", Dumper($disk_section_path, $disk_id) if $debug;
+	printf "disk section path: $disk_section_path and disk id: $disk_id\n" if $debug;
 	
-	#single disk VM
-	if (ref($tree->{DiskSection}->{Disk}) eq 'HASH') {
-	    if ($tree->{DiskSection}->{Disk}->{'ovf:diskId'} eq $disk_id) {
-		$disk_node = $tree->{DiskSection}->{'Disk'};
-	    }
-	} else { # multiple disks VM
-	    foreach my $entry (@{ $tree->{DiskSection}->{Disk} }) {
-		if ($entry->{'ovf:diskId'} eq $disk_id) {
-		    $disk_node = $entry;
-		    last;
-		}
-	    }
-	}
-	print "disk node: ", Dumper($disk_node) if $debug;
-
-	# from Disk Node, find corresponing File node
-	# single disk VM
-	if (ref($tree->{References}->{File}) eq 'HASH') {
-	    my $entry = $tree->{References}->{File};
-	    if ($entry->{'ovf:id'} eq $disk_node->{'ovf:fileRef'}) {
-		$file_node = $entry;
-	    }
+	# tricky xpath
+	# @ means we filter the result query based on a the value of an item attribute ( @ = attribute)
+	# @ needs to be escaped to prevent Perl double quote interpolation
+	my $xpath_find_fileref = sprintf("/ovf:Envelope/ovf:DiskSection/\
+ovf:Disk[\@ovf:diskId='%s']/\@ovf:fileRef", $disk_id);
+	my $fileref = $xpc->findvalue($xpath_find_fileref);
+	if (!$fileref) {
+	    warn "invalid host ressource $host_resource, skipping\n";
+	    next;
 	}
-	else { # multiple disks,
-	    foreach my $entry (@{ $tree->{References}->{File} }) {
-		if ($entry->{'ovf:id'} eq $disk_node->{'ovf:fileRef'}) {
-		    $file_node = $entry;
-		}
-	    }
+	
+	# from Disk Node, find corresponding filepath
+	my $xpath_find_filepath = sprintf("/ovf:Envelope/ovf:References/ovf:File[\@ovf:id='%s']/\@ovf:href", $fileref);
+	my $filepath = $xpc->findvalue($xpath_find_filepath);
+	if (!$filepath) {
+	    warn "invalid file reference $fileref, skipping\n";
+	    next;
 	}
-	print "file node: ", Dumper($file_node) if $debug;
-
-	# from Item, find corresponding Controller node
-	foreach my $controller (@controllers_items) {
-	    if ($controller->{'rasd:InstanceID'} eq $item_node->{'rasd:Parent'}) {
-		$controller_node = $controller;
-	    }
+	print "file path: $filepath\n" if $debug;
+
+	# from Item, find owning Controller type
+	my $controller_id = $item_node->findvalue('./rasd:Parent');
+	my $xpath_find_parent_type = sprintf("/ovf:Envelope/ovf:VirtualSystem/ovf:VirtualHardwareSection/\
+ovf:Item[rasd:InstanceID='%s']/rasd:ResourceType", $controller_id);
+	my $controller_type = $xpc->findvalue($xpath_find_parent_type);
+	if (!$controller_type) {
+	    warn "invalid or missing controller: $controller_type, skipping\n";
+	    next;
 	}
-	print "controller node: ", Dumper($controller_node) if $debug;
+	print "owning controller type: $controller_type\n" if $debug;
 
 	# extract corresponding Controller node details
-	my $controller_id = $controller_node->{'rasd:ResourceType'};
-	my $adress_on_controller = $item_node->{'rasd:AddressOnParent'};
-	my $pve_disk_address = id_to_pve($controller_id) . $adress_on_controller;
-	
-	my $backing_file = $file_node->{'ovf:href'};
-	my $backing_file_abs_path = join ('/', dirname(File::Spec->rel2abs($ovf)), $backing_file);
+	my $adress_on_controller = $item_node->findvalue('./rasd:AddressOnParent');
+	my $pve_disk_address = id_to_pve($controller_type) . $adress_on_controller;
+
+	my $backing_file_abs_path = join ('/', dirname(File::Spec->rel2abs($ovf)), $filepath);
+
+	my $virtual_size;
+	if ( !($virtual_size = PVE::Storage::Plugin::file_size_info($backing_file_abs_path)) ) {
+	    die "error parsing $backing_file_abs_path, size seems to be $virtual_size";
+	}
 
 	$pve_disk = {
 	    disk_address => $pve_disk_address,
-	    backing_file => $backing_file_abs_path
+	    backing_file => $backing_file_abs_path,
+	    virtual_size => $virtual_size
 	};
 	push @disks, $pve_disk;
 
diff --git a/control.in b/control.in
index d0601ac..7834e31 100644
--- a/control.in
+++ b/control.in
@@ -3,7 +3,7 @@ Version: @@VERSION@@-@@PKGRELEASE@@
 Section: admin
 Priority: optional
 Architecture: @@ARCH@@
-Depends: libc6 (>= 2.7-18), perl (>= 5.10.0-19), libterm-readline-gnu-perl, pve-qemu-kvm (>= 2.2-1), libpve-access-control, libpve-storage-perl, pve-cluster, libjson-perl, libjson-xs-perl, libio-multiplex-perl, libnet-ssleay-perl, socat, pve-firewall, libuuid-perl, pve-ha-manager, dbus, libpve-common-perl (>= 4.0-92), libpve-guest-common-perl
+Depends: libc6 (>= 2.7-18), perl (>= 5.10.0-19), libterm-readline-gnu-perl, pve-qemu-kvm (>= 2.2-1), libpve-access-control, libpve-storage-perl, pve-cluster, libjson-perl, libjson-xs-perl, libio-multiplex-perl, libnet-ssleay-perl, libxml-libxml-perl, socat, pve-firewall, libuuid-perl, pve-ha-manager, dbus, libpve-common-perl (>= 4.0-92), libpve-guest-common-perl
 Maintainer: Proxmox Support Team <support at proxmox.com>
 Description: Qemu Server Tools
  This package contains the Qemu Server tools used by Proxmox VE
-- 
2.1.4





More information about the pve-devel mailing list