From d16287d8d27f088ef590a88c35bcd0916038dfaa Mon Sep 17 00:00:00 2001 From: Emmanuel Kasper Date: Wed, 13 Sep 2017 16:10:23 +0200 Subject: [PATCH] Initial support for importing OVF virtual machines Following OVF parameters will be extracted: * VM name * Memory * Number of cores * disks and their associated controllers --- PVE/QemuServer/Makefile | 1 + PVE/QemuServer/OVF.pm | 236 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 PVE/QemuServer/OVF.pm diff --git a/PVE/QemuServer/Makefile b/PVE/QemuServer/Makefile index f75f2e65..49f65f3a 100644 --- a/PVE/QemuServer/Makefile +++ b/PVE/QemuServer/Makefile @@ -4,3 +4,4 @@ install: install -D -m 0644 USB.pm ${DESTDIR}${PERLDIR}/PVE/QemuServer/USB.pm install -D -m 0644 Memory.pm ${DESTDIR}${PERLDIR}/PVE/QemuServer/Memory.pm install -D -m 0644 ImportDisk.pm ${DESTDIR}${PERLDIR}/PVE/QemuServer/ImportDisk.pm + install -D -m 0644 OVF.pm ${DESTDIR}${PERLDIR}/PVE/QemuServer/OVF.pm diff --git a/PVE/QemuServer/OVF.pm b/PVE/QemuServer/OVF.pm new file mode 100644 index 00000000..7ee4dc85 --- /dev/null +++ b/PVE/QemuServer/OVF.pm @@ -0,0 +1,236 @@ +# Open Virtualization Format import routines +# https://www.dmtf.org/standards/ovf +package PVE::QemuServer::OVF; + +use strict; +use warnings; + +use XML::LibXML; +use File::Spec; +use File::Basename; +use Data::Dumper; +use Cwd 'realpath'; + +use PVE::Tools; +use PVE::Storage; + +# 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 = ( + { id => 1, dtmf_name => 'Other' }, + { id => 2, dtmf_name => 'Computer System' }, + { id => 3, dtmf_name => 'Processor' }, + { id => 4, dtmf_name => 'Memory' }, + { id => 5, dtmf_name => 'IDE Controller', pve_type => 'ide' }, + { id => 6, dtmf_name => 'Parallel SCSI HBA', pve_type => 'scsi' }, + { id => 7, dtmf_name => 'FC HBA' }, + { id => 8, dtmf_name => 'iSCSI HBA' }, + { id => 9, dtmf_name => 'IB HCA' }, + { id => 10, dtmf_name => 'Ethernet Adapter' }, + { id => 11, dtmf_name => 'Other Network Adapter' }, + { id => 12, dtmf_name => 'I/O Slot' }, + { id => 13, dtmf_name => 'I/O Device' }, + { id => 14, dtmf_name => 'Floppy Drive' }, + { id => 15, dtmf_name => 'CD Drive' }, + { id => 16, dtmf_name => 'DVD drive' }, + { 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'}, + { id => 21, dtmf_name => 'Serial port' }, + { id => 22, dtmf_name => 'Parallel port' }, + { id => 23, dtmf_name => 'USB Controller' }, + { id => 24, dtmf_name => 'Graphics controller' }, + { id => 25, dtmf_name => 'IEEE 1394 Controller' }, + { id => 26, dtmf_name => 'Partitionable Unit' }, + { id => 27, dtmf_name => 'Base Partitionable Unit' }, + { id => 28, dtmf_name => 'Power' }, + { id => 29, dtmf_name => 'Cooling Capacity' }, + { id => 30, dtmf_name => 'Ethernet Switch Port' }, + { id => 31, dtmf_name => 'Logical Disk' }, + { id => 32, dtmf_name => 'Storage Volume' }, + { id => 33, dtmf_name => 'Ethernet Connection' }, + { id => 34, dtmf_name => 'DMTF reserved' }, + { id => 35, dtmf_name => 'Vendor Reserved'} +); + +sub find_by { + my ($key, $param) = @_; + foreach my $resource (@resources) { + if ($resource->{$key} eq $param) { + return ($resource); + } + } + return undef; +} + +sub dtmf_name_to_id { + my ($dtmf_name) = @_; + my $found = find_by('dtmf_name', $dtmf_name); + if ($found) { + return $found->{id}; + } else { + return undef; + } +} + +sub id_to_pve { + my ($id) = @_; + my $resource = find_by('id', $id); + if ($resource) { + return $resource->{pve_type}; + } else { + return undef; + } +} + +# returns two references, $qm which holds qm.conf style key/values, and \@disks +sub parse_ovf { + my ($ovf, $debug) = @_; + + 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; + + #array to save a disk list + my @disks; + + # 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"; + } + + # 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 $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 $xpath_find_disks="/ovf:Envelope/ovf:VirtualSystem/ovf:VirtualHardwareSection/ovf:Item[rasd:ResourceType=${disk_id}]"; + my @disk_items = $xpc->findnodes($xpath_find_disks); + + # 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 + # * each Item node also holds a reference to its owning controller + # + # 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 copy the relevant information to + # a $pve_disk hash ref, which we push to @disks; + + foreach my $item_node (@disk_items) { + + my $disk_node; + my $file_node; + my $controller_node; + my $pve_disk; + + print "disk item:\n", $item_node->toString(1), "\n" if $debug; + + # from Item, find corresponding Disk node + # 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; + + # RFC 3986 "2.3. Unreserved Characters" + my $valid_uripath_chars = qr/[[:alnum:]]|[\-\._~]/; + + if ($host_resource =~ m|^ovf:/(${valid_uripath_chars}+)/(${valid_uripath_chars}+)$|) { + $disk_section_path = $1; + $disk_id = $2; + } else { + warn "invalid host ressource $host_resource, skipping\n"; + next; + } + printf "disk section path: $disk_section_path and disk id: $disk_id\n" if $debug; + + # 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); + + my $valid_url_chars = qr@${valid_uripath_chars}|/@; + if (!$fileref || $fileref !~ m/^${valid_url_chars}+$/) { + warn "invalid host ressource $host_resource, skipping\n"; + next; + } + + # 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 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 "owning controller type: $controller_type\n" if $debug; + + # extract corresponding Controller node details + my $adress_on_controller = $item_node->findvalue('./rasd:AddressOnParent'); + my $pve_disk_address = id_to_pve($controller_type) . $adress_on_controller; + + # resolve symlinks and relative path components + # and die if the diskimage is not somewhere under the $ovf path + my $ovf_dir = realpath(dirname(File::Spec->rel2abs($ovf))); + my $backing_file_path = realpath(join ('/', $ovf_dir, $filepath)); + if ($backing_file_path !~ /^\Q${ovf_dir}\E/) { + die "error parsing $filepath, are you using a symlink ?"; + } + + my $virtual_size; + if ( !($virtual_size = PVE::Storage::file_size_info($backing_file_path)) ) { + die "error parsing $backing_file_path, size seems to be $virtual_size"; + } + + $pve_disk = { + disk_address => $pve_disk_address, + backing_file => $backing_file_path, + virtual_size => $virtual_size + }; + push @disks, $pve_disk; + + } + + return {qm => $qm, disks => \@disks}; +} + +1;