]>
Commit | Line | Data |
---|---|---|
d16287d8 EK |
1 | # Open Virtualization Format import routines |
2 | # https://www.dmtf.org/standards/ovf | |
3 | package PVE::QemuServer::OVF; | |
4 | ||
5 | use strict; | |
6 | use warnings; | |
7 | ||
8 | use XML::LibXML; | |
9 | use File::Spec; | |
10 | use File::Basename; | |
11 | use Data::Dumper; | |
12 | use Cwd 'realpath'; | |
13 | ||
14 | use PVE::Tools; | |
15 | use PVE::Storage; | |
16 | ||
17 | # map OVF resources types to descriptive strings | |
18 | # this will allow us to explore the xml tree without using magic numbers | |
19 | # http://schemas.dmtf.org/wbem/cim-html/2/CIM_ResourceAllocationSettingData.html | |
20 | my @resources = ( | |
21 | { id => 1, dtmf_name => 'Other' }, | |
22 | { id => 2, dtmf_name => 'Computer System' }, | |
23 | { id => 3, dtmf_name => 'Processor' }, | |
24 | { id => 4, dtmf_name => 'Memory' }, | |
25 | { id => 5, dtmf_name => 'IDE Controller', pve_type => 'ide' }, | |
26 | { id => 6, dtmf_name => 'Parallel SCSI HBA', pve_type => 'scsi' }, | |
27 | { id => 7, dtmf_name => 'FC HBA' }, | |
28 | { id => 8, dtmf_name => 'iSCSI HBA' }, | |
29 | { id => 9, dtmf_name => 'IB HCA' }, | |
30 | { id => 10, dtmf_name => 'Ethernet Adapter' }, | |
31 | { id => 11, dtmf_name => 'Other Network Adapter' }, | |
32 | { id => 12, dtmf_name => 'I/O Slot' }, | |
33 | { id => 13, dtmf_name => 'I/O Device' }, | |
34 | { id => 14, dtmf_name => 'Floppy Drive' }, | |
35 | { id => 15, dtmf_name => 'CD Drive' }, | |
36 | { id => 16, dtmf_name => 'DVD drive' }, | |
37 | { id => 17, dtmf_name => 'Disk Drive' }, | |
38 | { id => 18, dtmf_name => 'Tape Drive' }, | |
39 | { id => 19, dtmf_name => 'Storage Extent' }, | |
40 | { id => 20, dtmf_name => 'Other storage device', pve_type => 'sata'}, | |
41 | { id => 21, dtmf_name => 'Serial port' }, | |
42 | { id => 22, dtmf_name => 'Parallel port' }, | |
43 | { id => 23, dtmf_name => 'USB Controller' }, | |
44 | { id => 24, dtmf_name => 'Graphics controller' }, | |
45 | { id => 25, dtmf_name => 'IEEE 1394 Controller' }, | |
46 | { id => 26, dtmf_name => 'Partitionable Unit' }, | |
47 | { id => 27, dtmf_name => 'Base Partitionable Unit' }, | |
48 | { id => 28, dtmf_name => 'Power' }, | |
49 | { id => 29, dtmf_name => 'Cooling Capacity' }, | |
50 | { id => 30, dtmf_name => 'Ethernet Switch Port' }, | |
51 | { id => 31, dtmf_name => 'Logical Disk' }, | |
52 | { id => 32, dtmf_name => 'Storage Volume' }, | |
53 | { id => 33, dtmf_name => 'Ethernet Connection' }, | |
54 | { id => 34, dtmf_name => 'DMTF reserved' }, | |
55 | { id => 35, dtmf_name => 'Vendor Reserved'} | |
56 | ); | |
57 | ||
58 | sub find_by { | |
59 | my ($key, $param) = @_; | |
60 | foreach my $resource (@resources) { | |
61 | if ($resource->{$key} eq $param) { | |
62 | return ($resource); | |
63 | } | |
64 | } | |
65 | return undef; | |
66 | } | |
67 | ||
68 | sub dtmf_name_to_id { | |
69 | my ($dtmf_name) = @_; | |
70 | my $found = find_by('dtmf_name', $dtmf_name); | |
71 | if ($found) { | |
72 | return $found->{id}; | |
73 | } else { | |
74 | return undef; | |
75 | } | |
76 | } | |
77 | ||
78 | sub id_to_pve { | |
79 | my ($id) = @_; | |
80 | my $resource = find_by('id', $id); | |
81 | if ($resource) { | |
82 | return $resource->{pve_type}; | |
83 | } else { | |
84 | return undef; | |
85 | } | |
86 | } | |
87 | ||
88 | # returns two references, $qm which holds qm.conf style key/values, and \@disks | |
89 | sub parse_ovf { | |
90 | my ($ovf, $debug) = @_; | |
91 | ||
92 | my $dom = XML::LibXML->load_xml(location => $ovf, no_blanks => 1); | |
93 | ||
94 | # register the xml namespaces in a xpath context object | |
95 | # 'ovf' is the default namespace so it will prepended to each xml element | |
96 | my $xpc = XML::LibXML::XPathContext->new($dom); | |
97 | $xpc->registerNs('ovf', 'http://schemas.dmtf.org/ovf/envelope/1'); | |
98 | $xpc->registerNs('rasd', 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData'); | |
99 | $xpc->registerNs('vssd', 'http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData'); | |
100 | ||
101 | ||
102 | # hash to save qm.conf parameters | |
103 | my $qm; | |
104 | ||
105 | #array to save a disk list | |
106 | my @disks; | |
107 | ||
108 | # easy xpath | |
109 | # walk down the dom until we find the matching XML element | |
110 | my $xpath_find_name = "/ovf:Envelope/ovf:VirtualSystem/ovf:Name"; | |
111 | my $ovf_name = $xpc->findvalue($xpath_find_name); | |
112 | ||
113 | if ($ovf_name) { | |
114 | ($qm->{name} = $ovf_name) =~ s/[^a-zA-Z0-9\-]//g; # PVE::QemuServer::confdesc requires a valid DNS name | |
115 | } else { | |
116 | warn "warning: unable to parse the VM name in this OVF manifest, generating a default value\n"; | |
117 | } | |
118 | ||
119 | # middle level xpath | |
120 | # element[child] search the elements which have this [child] | |
121 | my $processor_id = dtmf_name_to_id('Processor'); | |
122 | my $xpath_find_vcpu_count = "/ovf:Envelope/ovf:VirtualSystem/ovf:VirtualHardwareSection/ovf:Item[rasd:ResourceType=${processor_id}]/rasd:VirtualQuantity"; | |
123 | $qm->{'cores'} = $xpc->findvalue($xpath_find_vcpu_count); | |
124 | ||
125 | my $memory_id = dtmf_name_to_id('Memory'); | |
126 | my $xpath_find_memory = ("/ovf:Envelope/ovf:VirtualSystem/ovf:VirtualHardwareSection/ovf:Item[rasd:ResourceType=${memory_id}]/rasd:VirtualQuantity"); | |
127 | $qm->{'memory'} = $xpc->findvalue($xpath_find_memory); | |
128 | ||
129 | # middle level xpath | |
130 | # here we expect multiple results, so we do not read the element value with | |
131 | # findvalue() but store multiple elements with findnodes() | |
132 | my $disk_id = dtmf_name_to_id('Disk Drive'); | |
133 | my $xpath_find_disks="/ovf:Envelope/ovf:VirtualSystem/ovf:VirtualHardwareSection/ovf:Item[rasd:ResourceType=${disk_id}]"; | |
134 | my @disk_items = $xpc->findnodes($xpath_find_disks); | |
135 | ||
136 | # disks metadata is split in four different xml elements: | |
137 | # * as an Item node of type DiskDrive in the VirtualHardwareSection | |
138 | # * as an Disk node in the DiskSection | |
139 | # * as a File node in the References section | |
140 | # * each Item node also holds a reference to its owning controller | |
141 | # | |
142 | # we iterate over the list of Item nodes of type disk drive, and for each item, | |
143 | # find the corresponding Disk node, and File node and owning controller | |
144 | # when all the nodes has been found out, we copy the relevant information to | |
145 | # a $pve_disk hash ref, which we push to @disks; | |
146 | ||
147 | foreach my $item_node (@disk_items) { | |
148 | ||
149 | my $disk_node; | |
150 | my $file_node; | |
151 | my $controller_node; | |
152 | my $pve_disk; | |
153 | ||
154 | print "disk item:\n", $item_node->toString(1), "\n" if $debug; | |
155 | ||
156 | # from Item, find corresponding Disk node | |
157 | # here the dot means the search should start from the current element in dom | |
158 | my $host_resource = $item_node->findvalue('./rasd:HostResource'); | |
159 | my $disk_section_path; | |
160 | my $disk_id; | |
161 | ||
162 | # RFC 3986 "2.3. Unreserved Characters" | |
163 | my $valid_uripath_chars = qr/[[:alnum:]]|[\-\._~]/; | |
164 | ||
165 | if ($host_resource =~ m|^ovf:/(${valid_uripath_chars}+)/(${valid_uripath_chars}+)$|) { | |
166 | $disk_section_path = $1; | |
167 | $disk_id = $2; | |
168 | } else { | |
169 | warn "invalid host ressource $host_resource, skipping\n"; | |
170 | next; | |
171 | } | |
172 | printf "disk section path: $disk_section_path and disk id: $disk_id\n" if $debug; | |
173 | ||
174 | # tricky xpath | |
175 | # @ means we filter the result query based on a the value of an item attribute ( @ = attribute) | |
176 | # @ needs to be escaped to prevent Perl double quote interpolation | |
177 | my $xpath_find_fileref = sprintf("/ovf:Envelope/ovf:DiskSection/\ | |
178 | ovf:Disk[\@ovf:diskId='%s']/\@ovf:fileRef", $disk_id); | |
179 | my $fileref = $xpc->findvalue($xpath_find_fileref); | |
180 | ||
181 | my $valid_url_chars = qr@${valid_uripath_chars}|/@; | |
182 | if (!$fileref || $fileref !~ m/^${valid_url_chars}+$/) { | |
183 | warn "invalid host ressource $host_resource, skipping\n"; | |
184 | next; | |
185 | } | |
186 | ||
187 | # from Disk Node, find corresponding filepath | |
188 | my $xpath_find_filepath = sprintf("/ovf:Envelope/ovf:References/ovf:File[\@ovf:id='%s']/\@ovf:href", $fileref); | |
189 | my $filepath = $xpc->findvalue($xpath_find_filepath); | |
190 | if (!$filepath) { | |
191 | warn "invalid file reference $fileref, skipping\n"; | |
192 | next; | |
193 | } | |
194 | print "file path: $filepath\n" if $debug; | |
195 | ||
196 | # from Item, find owning Controller type | |
197 | my $controller_id = $item_node->findvalue('./rasd:Parent'); | |
198 | my $xpath_find_parent_type = sprintf("/ovf:Envelope/ovf:VirtualSystem/ovf:VirtualHardwareSection/\ | |
199 | ovf:Item[rasd:InstanceID='%s']/rasd:ResourceType", $controller_id); | |
200 | my $controller_type = $xpc->findvalue($xpath_find_parent_type); | |
201 | if (!$controller_type) { | |
202 | warn "invalid or missing controller: $controller_type, skipping\n"; | |
203 | next; | |
204 | } | |
205 | print "owning controller type: $controller_type\n" if $debug; | |
206 | ||
207 | # extract corresponding Controller node details | |
208 | my $adress_on_controller = $item_node->findvalue('./rasd:AddressOnParent'); | |
209 | my $pve_disk_address = id_to_pve($controller_type) . $adress_on_controller; | |
210 | ||
211 | # resolve symlinks and relative path components | |
212 | # and die if the diskimage is not somewhere under the $ovf path | |
213 | my $ovf_dir = realpath(dirname(File::Spec->rel2abs($ovf))); | |
214 | my $backing_file_path = realpath(join ('/', $ovf_dir, $filepath)); | |
215 | if ($backing_file_path !~ /^\Q${ovf_dir}\E/) { | |
216 | die "error parsing $filepath, are you using a symlink ?"; | |
217 | } | |
218 | ||
219 | my $virtual_size; | |
220 | if ( !($virtual_size = PVE::Storage::file_size_info($backing_file_path)) ) { | |
221 | die "error parsing $backing_file_path, size seems to be $virtual_size"; | |
222 | } | |
223 | ||
224 | $pve_disk = { | |
225 | disk_address => $pve_disk_address, | |
226 | backing_file => $backing_file_path, | |
227 | virtual_size => $virtual_size | |
228 | }; | |
229 | push @disks, $pve_disk; | |
230 | ||
231 | } | |
232 | ||
233 | return {qm => $qm, disks => \@disks}; | |
234 | } | |
235 | ||
236 | 1; |