root / lib / ovf.py @ 99381e3b
History | View | Annotate | Download (26.9 kB)
1 |
#!/usr/bin/python
|
---|---|
2 |
#
|
3 |
|
4 |
# Copyright (C) 2011 Google Inc.
|
5 |
#
|
6 |
# This program is free software; you can redistribute it and/or modify
|
7 |
# it under the terms of the GNU General Public License as published by
|
8 |
# the Free Software Foundation; either version 2 of the License, or
|
9 |
# (at your option) any later version.
|
10 |
#
|
11 |
# This program is distributed in the hope that it will be useful, but
|
12 |
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
14 |
# General Public License for more details.
|
15 |
#
|
16 |
# You should have received a copy of the GNU General Public License
|
17 |
# along with this program; if not, write to the Free Software
|
18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
19 |
# 02110-1301, USA.
|
20 |
|
21 |
|
22 |
"""Converter tools between ovf and ganeti config file
|
23 |
|
24 |
"""
|
25 |
|
26 |
# pylint: disable=F0401, E1101
|
27 |
|
28 |
# F0401 because ElementTree is not default for python 2.4
|
29 |
# E1101 makes no sense - pylint assumes that ElementTree object is a tuple
|
30 |
|
31 |
|
32 |
import errno |
33 |
import logging |
34 |
import os |
35 |
import os.path |
36 |
import re |
37 |
import shutil |
38 |
import tarfile |
39 |
import tempfile |
40 |
import xml.parsers.expat |
41 |
try:
|
42 |
import xml.etree.ElementTree as ET |
43 |
except ImportError: |
44 |
import elementtree.ElementTree as ET |
45 |
|
46 |
from ganeti import constants |
47 |
from ganeti import errors |
48 |
from ganeti import utils |
49 |
|
50 |
|
51 |
# Schemas used in OVF format
|
52 |
GANETI_SCHEMA = "http://ganeti"
|
53 |
OVF_SCHEMA = "http://schemas.dmtf.org/ovf/envelope/1"
|
54 |
RASD_SCHEMA = ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/"
|
55 |
"CIM_ResourceAllocationSettingData")
|
56 |
|
57 |
# File extensions in OVF package
|
58 |
OVA_EXT = ".ova"
|
59 |
OVF_EXT = ".ovf"
|
60 |
MF_EXT = ".mf"
|
61 |
CERT_EXT = ".cert"
|
62 |
COMPRESSION_EXT = ".gz"
|
63 |
FILE_EXTENSIONS = [ |
64 |
OVF_EXT, |
65 |
MF_EXT, |
66 |
CERT_EXT, |
67 |
] |
68 |
|
69 |
COMPRESSION_TYPE = "gzip"
|
70 |
COMPRESS = "compression"
|
71 |
DECOMPRESS = "decompression"
|
72 |
ALLOWED_ACTIONS = [COMPRESS, DECOMPRESS] |
73 |
|
74 |
|
75 |
def LinkFile(old_path, prefix=None, suffix=None, directory=None): |
76 |
"""Create link with a given prefix and suffix.
|
77 |
|
78 |
This is a wrapper over os.link. It tries to create a hard link for given file,
|
79 |
but instead of rising error when file exists, the function changes the name
|
80 |
a little bit.
|
81 |
|
82 |
@type old_path:string
|
83 |
@param old_path: path to the file that is to be linked
|
84 |
@type prefix: string
|
85 |
@param prefix: prefix of filename for the link
|
86 |
@type suffix: string
|
87 |
@param suffix: suffix of the filename for the link
|
88 |
@type directory: string
|
89 |
@param directory: directory of the link
|
90 |
|
91 |
@raise errors.OpPrereqError: when error on linking is different than
|
92 |
"File exists"
|
93 |
|
94 |
"""
|
95 |
assert(prefix is not None or suffix is not None) |
96 |
if directory is None: |
97 |
directory = os.getcwd() |
98 |
new_path = utils.PathJoin(directory, "%s%s" % (prefix, suffix))
|
99 |
counter = 1
|
100 |
while True: |
101 |
try:
|
102 |
os.link(old_path, new_path) |
103 |
break
|
104 |
except OSError, err: |
105 |
if err.errno == errno.EEXIST:
|
106 |
new_path = utils.PathJoin(directory, |
107 |
"%s_%s%s" % (prefix, counter, suffix))
|
108 |
counter += 1
|
109 |
else:
|
110 |
raise errors.OpPrereqError("Error moving the file %s to %s location:" |
111 |
" %s" % (old_path, new_path, err))
|
112 |
return new_path
|
113 |
|
114 |
|
115 |
class OVFReader(object): |
116 |
"""Reader class for OVF files.
|
117 |
|
118 |
@type files_list: list
|
119 |
@ivar files_list: list of files in the OVF package
|
120 |
@type tree: ET.ElementTree
|
121 |
@ivar tree: XML tree of the .ovf file
|
122 |
@type schema_name: string
|
123 |
@ivar schema_name: name of the .ovf file
|
124 |
@type input_dir: string
|
125 |
@ivar input_dir: directory in which the .ovf file resides
|
126 |
|
127 |
"""
|
128 |
def __init__(self, input_path): |
129 |
"""Initialiaze the reader - load the .ovf file to XML parser.
|
130 |
|
131 |
It is assumed that names of manifesto (.mf), certificate (.cert) and ovf
|
132 |
files are the same. In order to account any other files as part of the ovf
|
133 |
package, they have to be explicitly mentioned in the Resources section
|
134 |
of the .ovf file.
|
135 |
|
136 |
@type input_path: string
|
137 |
@param input_path: absolute path to the .ovf file
|
138 |
|
139 |
@raise errors.OpPrereqError: when .ovf file is not a proper XML file or some
|
140 |
of the files mentioned in Resources section do not exist
|
141 |
|
142 |
"""
|
143 |
self.tree = ET.ElementTree()
|
144 |
try:
|
145 |
self.tree.parse(input_path)
|
146 |
except xml.parsers.expat.ExpatError, err:
|
147 |
raise errors.OpPrereqError("Error while reading %s file: %s" % |
148 |
(OVF_EXT, err)) |
149 |
|
150 |
# Create a list of all files in the OVF package
|
151 |
(input_dir, input_file) = os.path.split(input_path) |
152 |
(input_name, _) = os.path.splitext(input_file) |
153 |
files_directory = utils.ListVisibleFiles(input_dir) |
154 |
files_list = [] |
155 |
for file_name in files_directory: |
156 |
(name, extension) = os.path.splitext(file_name) |
157 |
if extension in FILE_EXTENSIONS and name == input_name: |
158 |
files_list.append(file_name) |
159 |
files_list += self._GetAttributes("{%s}References/{%s}File" % |
160 |
(OVF_SCHEMA, OVF_SCHEMA), |
161 |
"{%s}href" % OVF_SCHEMA)
|
162 |
for file_name in files_list: |
163 |
file_path = utils.PathJoin(input_dir, file_name) |
164 |
if not os.path.exists(file_path): |
165 |
raise errors.OpPrereqError("File does not exist: %s" % file_path) |
166 |
logging.info("Files in the OVF package: %s", " ".join(files_list)) |
167 |
self.files_list = files_list
|
168 |
self.input_dir = input_dir
|
169 |
self.schema_name = input_name
|
170 |
|
171 |
def _GetAttributes(self, path, attribute): |
172 |
"""Get specified attribute from all nodes accessible using given path.
|
173 |
|
174 |
Function follows the path from root node to the desired tags using path,
|
175 |
then reads the apropriate attribute values.
|
176 |
|
177 |
@type path: string
|
178 |
@param path: path of nodes to visit
|
179 |
@type attribute: string
|
180 |
@param attribute: attribute for which we gather the information
|
181 |
@rtype: list
|
182 |
@return: for each accessible tag with the attribute value set, value of the
|
183 |
attribute
|
184 |
|
185 |
"""
|
186 |
current_list = self.tree.findall(path)
|
187 |
results = [x.get(attribute) for x in current_list] |
188 |
return filter(None, results) |
189 |
|
190 |
def _GetElementMatchingAttr(self, path, match_attr): |
191 |
"""Searches for element on a path that matches certain attribute value.
|
192 |
|
193 |
Function follows the path from root node to the desired tags using path,
|
194 |
then searches for the first one matching the attribute value.
|
195 |
|
196 |
@type path: string
|
197 |
@param path: path of nodes to visit
|
198 |
@type match_attr: tuple
|
199 |
@param match_attr: pair (attribute, value) for which we search
|
200 |
@rtype: ET.ElementTree or None
|
201 |
@return: first element matching match_attr or None if nothing matches
|
202 |
|
203 |
"""
|
204 |
potential_elements = self.tree.findall(path)
|
205 |
(attr, val) = match_attr |
206 |
for elem in potential_elements: |
207 |
if elem.get(attr) == val:
|
208 |
return elem
|
209 |
return None |
210 |
|
211 |
@staticmethod
|
212 |
def _GetDictParameters(root, schema): |
213 |
"""Reads text in all children and creates the dictionary from the contents.
|
214 |
|
215 |
@type root: ET.ElementTree or None
|
216 |
@param root: father of the nodes we want to collect data about
|
217 |
@type schema: string
|
218 |
@param schema: schema name to be removed from the tag
|
219 |
@rtype: dict
|
220 |
@return: dictionary containing tags and their text contents, tags have their
|
221 |
schema fragment removed or empty dictionary, when root is None
|
222 |
|
223 |
"""
|
224 |
if not root: |
225 |
return {}
|
226 |
results = {} |
227 |
for element in list(root): |
228 |
pref_len = len("{%s}" % schema) |
229 |
assert(schema in element.tag) |
230 |
tag = element.tag[pref_len:] |
231 |
results[tag] = element.text |
232 |
return results
|
233 |
|
234 |
def VerifyManifest(self): |
235 |
"""Verifies manifest for the OVF package, if one is given.
|
236 |
|
237 |
@raise errors.OpPrereqError: if SHA1 checksums do not match
|
238 |
|
239 |
"""
|
240 |
if "%s%s" % (self.schema_name, MF_EXT) in self.files_list: |
241 |
logging.warning("Verifying SHA1 checksums, this may take a while")
|
242 |
manifest_filename = "%s%s" % (self.schema_name, MF_EXT) |
243 |
manifest_path = utils.PathJoin(self.input_dir, manifest_filename)
|
244 |
manifest_content = utils.ReadFile(manifest_path).splitlines() |
245 |
manifest_files = {} |
246 |
regexp = r"SHA1\((\S+)\)= (\S+)"
|
247 |
for line in manifest_content: |
248 |
match = re.match(regexp, line) |
249 |
if match:
|
250 |
file_name = match.group(1)
|
251 |
sha1_sum = match.group(2)
|
252 |
manifest_files[file_name] = sha1_sum |
253 |
files_with_paths = [utils.PathJoin(self.input_dir, file_name)
|
254 |
for file_name in self.files_list] |
255 |
sha1_sums = utils.FingerprintFiles(files_with_paths) |
256 |
for file_name, value in manifest_files.iteritems(): |
257 |
if sha1_sums.get(utils.PathJoin(self.input_dir, file_name)) != value: |
258 |
raise errors.OpPrereqError("SHA1 checksum of %s does not match the" |
259 |
" value in manifest file" % file_name)
|
260 |
logging.info("SHA1 checksums verified")
|
261 |
|
262 |
def GetInstanceName(self): |
263 |
"""Provides information about instance name.
|
264 |
|
265 |
@rtype: string
|
266 |
@return: instance name string
|
267 |
|
268 |
"""
|
269 |
find_name = "{%s}VirtualSystem/{%s}Name" % (OVF_SCHEMA, OVF_SCHEMA)
|
270 |
return self.tree.findtext(find_name) |
271 |
|
272 |
def GetDiskTemplate(self): |
273 |
"""Returns disk template from .ovf file
|
274 |
|
275 |
@rtype: string or None
|
276 |
@return: name of the template
|
277 |
"""
|
278 |
find_template = ("{%s}GanetiSection/{%s}DiskTemplate" %
|
279 |
(GANETI_SCHEMA, GANETI_SCHEMA)) |
280 |
return self.tree.findtext(find_template) |
281 |
|
282 |
def GetDisksNames(self): |
283 |
"""Provides list of file names for the disks used by the instance.
|
284 |
|
285 |
@rtype: list
|
286 |
@return: list of file names, as referenced in .ovf file
|
287 |
|
288 |
"""
|
289 |
results = [] |
290 |
disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA)
|
291 |
disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA) |
292 |
for disk in disk_ids: |
293 |
disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA)
|
294 |
disk_match = ("{%s}id" % OVF_SCHEMA, disk)
|
295 |
disk_elem = self._GetElementMatchingAttr(disk_search, disk_match)
|
296 |
if disk_elem is None: |
297 |
raise errors.OpPrereqError("%s file corrupted - disk %s not found in" |
298 |
" references" % (OVF_EXT, disk))
|
299 |
disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA)
|
300 |
disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA)
|
301 |
results.append((disk_name, disk_compression)) |
302 |
return results
|
303 |
|
304 |
|
305 |
class Converter(object): |
306 |
"""Converter class for OVF packages.
|
307 |
|
308 |
Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is
|
309 |
to provide a common interface for the two.
|
310 |
|
311 |
@type options: optparse.Values
|
312 |
@ivar options: options parsed from the command line
|
313 |
@type output_dir: string
|
314 |
@ivar output_dir: directory to which the results of conversion shall be
|
315 |
written
|
316 |
@type temp_file_manager: L{utils.TemporaryFileManager}
|
317 |
@ivar temp_file_manager: container for temporary files created during
|
318 |
conversion
|
319 |
@type temp_dir: string
|
320 |
@ivar temp_dir: temporary directory created then we deal with OVA
|
321 |
|
322 |
"""
|
323 |
def __init__(self, input_path, options): |
324 |
"""Initialize the converter.
|
325 |
|
326 |
@type input_path: string
|
327 |
@param input_path: path to the Converter input file
|
328 |
@type options: optparse.Values
|
329 |
@param options: command line options
|
330 |
|
331 |
@raise errors.OpPrereqError: if file does not exist
|
332 |
|
333 |
"""
|
334 |
input_path = os.path.abspath(input_path) |
335 |
if not os.path.isfile(input_path): |
336 |
raise errors.OpPrereqError("File does not exist: %s" % input_path) |
337 |
self.options = options
|
338 |
self.temp_file_manager = utils.TemporaryFileManager()
|
339 |
self.temp_dir = None |
340 |
self.output_dir = None |
341 |
self._ReadInputData(input_path)
|
342 |
|
343 |
def _ReadInputData(self, input_path): |
344 |
"""Reads the data on which the conversion will take place.
|
345 |
|
346 |
@type input_path: string
|
347 |
@param input_path: absolute path to the Converter input file
|
348 |
|
349 |
"""
|
350 |
raise NotImplementedError() |
351 |
|
352 |
def _CompressDisk(self, disk_path, compression, action): |
353 |
"""Performs (de)compression on the disk and returns the new path
|
354 |
|
355 |
@type disk_path: string
|
356 |
@param disk_path: path to the disk
|
357 |
@type compression: string
|
358 |
@param compression: compression type
|
359 |
@type action: string
|
360 |
@param action: whether the action is compression or decompression
|
361 |
@rtype: string
|
362 |
@return: new disk path after (de)compression
|
363 |
|
364 |
@raise errors.OpPrereqError: disk (de)compression failed or "compression"
|
365 |
is not supported
|
366 |
|
367 |
"""
|
368 |
assert(action in ALLOWED_ACTIONS) |
369 |
# For now we only support gzip, as it is used in ovftool
|
370 |
if compression != COMPRESSION_TYPE:
|
371 |
raise errors.OpPrereqError("Unsupported compression type: %s" |
372 |
% compression) |
373 |
disk_file = os.path.basename(disk_path) |
374 |
if action == DECOMPRESS:
|
375 |
(disk_name, _) = os.path.splitext(disk_file) |
376 |
prefix = disk_name |
377 |
elif action == COMPRESS:
|
378 |
prefix = disk_file |
379 |
new_path = utils.GetClosedTempfile(suffix=COMPRESSION_EXT, prefix=prefix, |
380 |
dir=self.output_dir)
|
381 |
self.temp_file_manager.Add(new_path)
|
382 |
args = ["gzip", "-c", disk_path] |
383 |
run_result = utils.RunCmd(args, output=new_path) |
384 |
if run_result.failed:
|
385 |
raise errors.OpPrereqError("Disk %s failed with output: %s" |
386 |
% (action, run_result.stderr)) |
387 |
logging.info("The %s of the disk is completed", action)
|
388 |
return (COMPRESSION_EXT, new_path)
|
389 |
|
390 |
def _ConvertDisk(self, disk_format, disk_path): |
391 |
"""Performes conversion to specified format.
|
392 |
|
393 |
@type disk_format: string
|
394 |
@param disk_format: format to which the disk should be converted
|
395 |
@type disk_path: string
|
396 |
@param disk_path: path to the disk that should be converted
|
397 |
@rtype: string
|
398 |
@return path to the output disk
|
399 |
|
400 |
@raise errors.OpPrereqError: convertion of the disk failed
|
401 |
|
402 |
"""
|
403 |
disk_file = os.path.basename(disk_path) |
404 |
(disk_name, disk_extension) = os.path.splitext(disk_file) |
405 |
if disk_extension != disk_format:
|
406 |
logging.warning("Conversion of disk image to %s format, this may take"
|
407 |
" a while", disk_format)
|
408 |
|
409 |
new_disk_path = utils.GetClosedTempfile(suffix=".%s" % disk_format,
|
410 |
prefix=disk_name, dir=self.output_dir)
|
411 |
self.temp_file_manager.Add(new_disk_path)
|
412 |
args = [ |
413 |
"qemu-img",
|
414 |
"convert",
|
415 |
"-O",
|
416 |
disk_format, |
417 |
disk_path, |
418 |
new_disk_path, |
419 |
] |
420 |
run_result = utils.RunCmd(args, cwd=os.getcwd()) |
421 |
if run_result.failed:
|
422 |
raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was" |
423 |
": %s" % (disk_format, run_result.stderr))
|
424 |
return (".%s" % disk_format, new_disk_path) |
425 |
|
426 |
@staticmethod
|
427 |
def _GetDiskQemuInfo(disk_path, regexp): |
428 |
"""Figures out some information of the disk using qemu-img.
|
429 |
|
430 |
@type disk_path: string
|
431 |
@param disk_path: path to the disk we want to know the format of
|
432 |
@type regexp: string
|
433 |
@param regexp: string that has to be matched, it has to contain one group
|
434 |
@rtype: string
|
435 |
@return: disk format
|
436 |
|
437 |
@raise errors.OpPrereqError: format information cannot be retrieved
|
438 |
|
439 |
"""
|
440 |
args = ["qemu-img", "info", disk_path] |
441 |
run_result = utils.RunCmd(args, cwd=os.getcwd()) |
442 |
if run_result.failed:
|
443 |
raise errors.OpPrereqError("Gathering info about the disk using qemu-img" |
444 |
" failed, output was: %s" % run_result.stderr)
|
445 |
result = run_result.output |
446 |
regexp = r"%s" % regexp
|
447 |
match = re.search(regexp, result) |
448 |
if match:
|
449 |
disk_format = match.group(1)
|
450 |
else:
|
451 |
raise errors.OpPrereqError("No file information matching %s found in:" |
452 |
" %s" % (regexp, result))
|
453 |
return disk_format
|
454 |
|
455 |
def Parse(self): |
456 |
"""Parses the data and creates a structure containing all required info.
|
457 |
|
458 |
"""
|
459 |
raise NotImplementedError() |
460 |
|
461 |
def Save(self): |
462 |
"""Saves the gathered configuration in an apropriate format.
|
463 |
|
464 |
"""
|
465 |
raise NotImplementedError() |
466 |
|
467 |
def Cleanup(self): |
468 |
"""Cleans the temporary directory, if one was created.
|
469 |
|
470 |
"""
|
471 |
self.temp_file_manager.Cleanup()
|
472 |
if self.temp_dir: |
473 |
shutil.rmtree(self.temp_dir)
|
474 |
self.temp_dir = None |
475 |
|
476 |
|
477 |
class OVFImporter(Converter): |
478 |
"""Converter from OVF to Ganeti config file.
|
479 |
|
480 |
@type input_dir: string
|
481 |
@ivar input_dir: directory in which the .ovf file resides
|
482 |
@type output_dir: string
|
483 |
@ivar output_dir: directory to which the results of conversion shall be
|
484 |
written
|
485 |
@type input_path: string
|
486 |
@ivar input_path: complete path to the .ovf file
|
487 |
@type ovf_reader: L{OVFReader}
|
488 |
@ivar ovf_reader: OVF reader instance collects data from .ovf file
|
489 |
@type results_name: string
|
490 |
@ivar results_name: name of imported instance
|
491 |
@type results_template: string
|
492 |
@ivar results_template: disk template read from .ovf file or command line
|
493 |
arguments
|
494 |
@type results_disk: dict
|
495 |
@ivar results_disk: disk information gathered from .ovf file or command line
|
496 |
arguments
|
497 |
|
498 |
"""
|
499 |
def _ReadInputData(self, input_path): |
500 |
"""Reads the data on which the conversion will take place.
|
501 |
|
502 |
@type input_path: string
|
503 |
@param input_path: absolute path to the .ovf or .ova input file
|
504 |
|
505 |
@raise errors.OpPrereqError: if input file is neither .ovf nor .ova
|
506 |
|
507 |
"""
|
508 |
(input_dir, input_file) = os.path.split(input_path) |
509 |
(_, input_extension) = os.path.splitext(input_file) |
510 |
|
511 |
if input_extension == OVF_EXT:
|
512 |
logging.info("%s file extension found, no unpacking necessary", OVF_EXT)
|
513 |
self.input_dir = input_dir
|
514 |
self.input_path = input_path
|
515 |
self.temp_dir = None |
516 |
elif input_extension == OVA_EXT:
|
517 |
logging.info("%s file extension found, proceeding to unpacking", OVA_EXT)
|
518 |
self._UnpackOVA(input_path)
|
519 |
else:
|
520 |
raise errors.OpPrereqError("Unknown file extension; expected %s or %s" |
521 |
" file" % (OVA_EXT, OVF_EXT))
|
522 |
assert ((input_extension == OVA_EXT and self.temp_dir) or |
523 |
(input_extension == OVF_EXT and not self.temp_dir)) |
524 |
assert self.input_dir in self.input_path |
525 |
|
526 |
if self.options.output_dir: |
527 |
self.output_dir = os.path.abspath(self.options.output_dir) |
528 |
if (os.path.commonprefix([constants.EXPORT_DIR, self.output_dir]) != |
529 |
constants.EXPORT_DIR): |
530 |
logging.warning("Export path is not under %s directory, import to"
|
531 |
" Ganeti using gnt-backup may fail",
|
532 |
constants.EXPORT_DIR) |
533 |
else:
|
534 |
self.output_dir = constants.EXPORT_DIR
|
535 |
|
536 |
self.ovf_reader = OVFReader(self.input_path) |
537 |
self.ovf_reader.VerifyManifest()
|
538 |
|
539 |
def _UnpackOVA(self, input_path): |
540 |
"""Unpacks the .ova package into temporary directory.
|
541 |
|
542 |
@type input_path: string
|
543 |
@param input_path: path to the .ova package file
|
544 |
|
545 |
@raise errors.OpPrereqError: if file is not a proper tarball, one of the
|
546 |
files in the archive seem malicious (e.g. path starts with '../') or
|
547 |
.ova package does not contain .ovf file
|
548 |
|
549 |
"""
|
550 |
input_name = None
|
551 |
if not tarfile.is_tarfile(input_path): |
552 |
raise errors.OpPrereqError("The provided %s file is not a proper tar" |
553 |
" archive", OVA_EXT)
|
554 |
ova_content = tarfile.open(input_path) |
555 |
temp_dir = tempfile.mkdtemp() |
556 |
self.temp_dir = temp_dir
|
557 |
for file_name in ova_content.getnames(): |
558 |
file_normname = os.path.normpath(file_name) |
559 |
try:
|
560 |
utils.PathJoin(temp_dir, file_normname) |
561 |
except ValueError, err: |
562 |
raise errors.OpPrereqError("File %s inside %s package is not safe" % |
563 |
(file_name, OVA_EXT)) |
564 |
if file_name.endswith(OVF_EXT):
|
565 |
input_name = file_name |
566 |
if not input_name: |
567 |
raise errors.OpPrereqError("No %s file in %s package found" % |
568 |
(OVF_EXT, OVA_EXT)) |
569 |
logging.warning("Unpacking the %s archive, this may take a while",
|
570 |
input_path) |
571 |
self.input_dir = temp_dir
|
572 |
self.input_path = utils.PathJoin(self.temp_dir, input_name) |
573 |
try:
|
574 |
try:
|
575 |
extract = ova_content.extractall |
576 |
except AttributeError: |
577 |
# This is a prehistorical case of using python < 2.5
|
578 |
for member in ova_content.getmembers(): |
579 |
ova_content.extract(member, path=self.temp_dir)
|
580 |
else:
|
581 |
extract(self.temp_dir)
|
582 |
except tarfile.TarError, err:
|
583 |
raise errors.OpPrereqError("Error while extracting %s archive: %s" % |
584 |
(OVA_EXT, err)) |
585 |
logging.info("OVA package extracted to %s directory", self.temp_dir) |
586 |
|
587 |
def Parse(self): |
588 |
"""Parses the data and creates a structure containing all required info.
|
589 |
|
590 |
The method reads the information given either as a command line option or as
|
591 |
a part of the OVF description.
|
592 |
|
593 |
@raise errors.OpPrereqError: if some required part of the description of
|
594 |
virtual instance is missing or unable to create output directory
|
595 |
|
596 |
"""
|
597 |
self.results_name = self._GetInfo("instance name", self.options.name, |
598 |
self._ParseNameOptions, self.ovf_reader.GetInstanceName) |
599 |
if not self.results_name: |
600 |
raise errors.OpPrereqError("Name of instance not provided") |
601 |
|
602 |
self.output_dir = utils.PathJoin(self.output_dir, self.results_name) |
603 |
try:
|
604 |
utils.Makedirs(self.output_dir)
|
605 |
except OSError, err: |
606 |
raise errors.OpPrereqError("Failed to create directory %s: %s" % |
607 |
(self.output_dir, err))
|
608 |
|
609 |
self.results_template = self._GetInfo("disk template", |
610 |
self.options.disk_template, self._ParseTemplateOptions, |
611 |
self.ovf_reader.GetDiskTemplate)
|
612 |
if not self.results_template: |
613 |
logging.info("Disk template not given")
|
614 |
|
615 |
self.results_disk = self._GetInfo("disk", self.options.disks, |
616 |
self._ParseDiskOptions, self._GetDiskInfo, |
617 |
ignore_test=self.results_template == constants.DT_DISKLESS)
|
618 |
|
619 |
@staticmethod
|
620 |
def _GetInfo(name, cmd_arg, cmd_function, nocmd_function, |
621 |
ignore_test=False):
|
622 |
"""Get information about some section - e.g. disk, network, hypervisor.
|
623 |
|
624 |
@type name: string
|
625 |
@param name: name of the section
|
626 |
@type cmd_arg: dict
|
627 |
@param cmd_arg: command line argument specific for section 'name'
|
628 |
@type cmd_function: callable
|
629 |
@param cmd_function: function to call if 'cmd_args' exists
|
630 |
@type nocmd_function: callable
|
631 |
@param nocmd_function: function to call if 'cmd_args' is not there
|
632 |
|
633 |
"""
|
634 |
if ignore_test:
|
635 |
logging.info("Information for %s will be ignored", name)
|
636 |
return {}
|
637 |
if cmd_arg:
|
638 |
logging.info("Information for %s will be parsed from command line", name)
|
639 |
results = cmd_function() |
640 |
else:
|
641 |
logging.info("Information for %s will be parsed from %s file",
|
642 |
name, OVF_EXT) |
643 |
results = nocmd_function() |
644 |
logging.info("Options for %s were succesfully read", name)
|
645 |
return results
|
646 |
|
647 |
def _ParseNameOptions(self): |
648 |
"""Returns name if one was given in command line.
|
649 |
|
650 |
@rtype: string
|
651 |
@return: name of an instance
|
652 |
|
653 |
"""
|
654 |
return self.options.name |
655 |
|
656 |
def _ParseTemplateOptions(self): |
657 |
"""Returns disk template if one was given in command line.
|
658 |
|
659 |
@rtype: string
|
660 |
@return: disk template name
|
661 |
|
662 |
"""
|
663 |
return self.options.disk_template |
664 |
|
665 |
def _ParseDiskOptions(self): |
666 |
"""Parses disk options given in a command line.
|
667 |
|
668 |
@rtype: dict
|
669 |
@return: dictionary of disk-related options
|
670 |
|
671 |
@raise errors.OpPrereqError: disk description does not contain size
|
672 |
information or size information is invalid or creation failed
|
673 |
|
674 |
"""
|
675 |
assert self.options.disks |
676 |
results = {} |
677 |
for (disk_id, disk_desc) in self.options.disks: |
678 |
results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id |
679 |
if disk_desc.get("size"): |
680 |
try:
|
681 |
disk_size = utils.ParseUnit(disk_desc["size"])
|
682 |
except ValueError: |
683 |
raise errors.OpPrereqError("Invalid disk size for disk %s: %s" % |
684 |
(disk_id, disk_desc["size"]))
|
685 |
new_path = utils.PathJoin(self.output_dir, str(disk_id)) |
686 |
args = [ |
687 |
"qemu-img",
|
688 |
"create",
|
689 |
"-f",
|
690 |
"raw",
|
691 |
new_path, |
692 |
disk_size, |
693 |
] |
694 |
run_result = utils.RunCmd(args) |
695 |
if run_result.failed:
|
696 |
raise errors.OpPrereqError("Creation of disk %s failed, output was:" |
697 |
" %s" % (new_path, run_result.stderr))
|
698 |
results["disk%s_size" % disk_id] = str(disk_size) |
699 |
results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id |
700 |
else:
|
701 |
raise errors.OpPrereqError("Disks created for import must have their" |
702 |
" size specified")
|
703 |
results["disk_count"] = str(len(self.options.disks)) |
704 |
return results
|
705 |
|
706 |
def _GetDiskInfo(self): |
707 |
"""Gathers information about disks used by instance, perfomes conversion.
|
708 |
|
709 |
@rtype: dict
|
710 |
@return: dictionary of disk-related options
|
711 |
|
712 |
@raise errors.OpPrereqError: disk is not in the same directory as .ovf file
|
713 |
|
714 |
"""
|
715 |
results = {} |
716 |
disks_list = self.ovf_reader.GetDisksNames()
|
717 |
for (counter, (disk_name, disk_compression)) in enumerate(disks_list): |
718 |
if os.path.dirname(disk_name):
|
719 |
raise errors.OpPrereqError("Disks are not allowed to have absolute" |
720 |
" paths or paths outside main OVF directory")
|
721 |
disk, _ = os.path.splitext(disk_name) |
722 |
disk_path = utils.PathJoin(self.input_dir, disk_name)
|
723 |
if disk_compression:
|
724 |
_, disk_path = self._CompressDisk(disk_path, disk_compression,
|
725 |
DECOMPRESS) |
726 |
disk, _ = os.path.splitext(disk) |
727 |
if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw": |
728 |
logging.info("Conversion to raw format is required")
|
729 |
ext, new_disk_path = self._ConvertDisk("raw", disk_path) |
730 |
|
731 |
final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext, |
732 |
directory=self.output_dir)
|
733 |
final_name = os.path.basename(final_disk_path) |
734 |
disk_size = os.path.getsize(final_disk_path) / (1024 * 1024) |
735 |
results["disk%s_dump" % counter] = final_name
|
736 |
results["disk%s_size" % counter] = str(disk_size) |
737 |
results["disk%s_ivname" % counter] = "disk/%s" % str(counter) |
738 |
if disks_list:
|
739 |
results["disk_count"] = str(len(disks_list)) |
740 |
return results
|
741 |
|
742 |
def Save(self): |
743 |
"""Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
|
744 |
|
745 |
@raise errors.OpPrereqError: when saving to config file failed
|
746 |
|
747 |
"""
|
748 |
logging.info("Conversion was succesfull, saving %s in %s directory",
|
749 |
constants.EXPORT_CONF_FILE, self.output_dir)
|
750 |
results = { |
751 |
constants.INISECT_INS: {}, |
752 |
constants.INISECT_BEP: {}, |
753 |
constants.INISECT_EXP: {}, |
754 |
constants.INISECT_OSP: {}, |
755 |
constants.INISECT_HYP: {}, |
756 |
} |
757 |
|
758 |
results[constants.INISECT_INS].update(self.results_disk)
|
759 |
results[constants.INISECT_INS]["name"] = self.results_name |
760 |
if self.results_template: |
761 |
results[constants.INISECT_INS]["disk_template"] = self.results_template |
762 |
|
763 |
output_file_name = utils.PathJoin(self.output_dir,
|
764 |
constants.EXPORT_CONF_FILE) |
765 |
|
766 |
output = [] |
767 |
for section, options in results.iteritems(): |
768 |
output.append("[%s]" % section)
|
769 |
for name, value in options.iteritems(): |
770 |
if value is None: |
771 |
value = ""
|
772 |
output.append("%s = %s" % (name, value))
|
773 |
output.append("")
|
774 |
output_contents = "\n".join(output)
|
775 |
|
776 |
try:
|
777 |
utils.WriteFile(output_file_name, data=output_contents) |
778 |
except errors.ProgrammerError, err:
|
779 |
raise errors.OpPrereqError("Saving the config file failed: %s" % err) |
780 |
|
781 |
self.Cleanup()
|
782 |
|
783 |
|
784 |
class OVFExporter(Converter): |
785 |
def _ReadInputData(self, input_path): |
786 |
pass
|
787 |
|
788 |
def Parse(self): |
789 |
pass
|
790 |
|
791 |
def Save(self): |
792 |
pass
|