Revision 99381e3b lib/ovf.py
b/lib/ovf.py | ||
---|---|---|
29 | 29 |
# E1101 makes no sense - pylint assumes that ElementTree object is a tuple |
30 | 30 |
|
31 | 31 |
|
32 |
import errno |
|
32 | 33 |
import logging |
34 |
import os |
|
33 | 35 |
import os.path |
34 | 36 |
import re |
35 | 37 |
import shutil |
... | ... | |
57 | 59 |
OVF_EXT = ".ovf" |
58 | 60 |
MF_EXT = ".mf" |
59 | 61 |
CERT_EXT = ".cert" |
62 |
COMPRESSION_EXT = ".gz" |
|
60 | 63 |
FILE_EXTENSIONS = [ |
61 | 64 |
OVF_EXT, |
62 | 65 |
MF_EXT, |
63 | 66 |
CERT_EXT, |
64 | 67 |
] |
65 | 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 |
|
|
66 | 114 |
|
67 | 115 |
class OVFReader(object): |
68 | 116 |
"""Reader class for OVF files. |
... | ... | |
139 | 187 |
results = [x.get(attribute) for x in current_list] |
140 | 188 |
return filter(None, results) |
141 | 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 |
|
|
142 | 234 |
def VerifyManifest(self): |
143 | 235 |
"""Verifies manifest for the OVF package, if one is given. |
144 | 236 |
|
... | ... | |
167 | 259 |
" value in manifest file" % file_name) |
168 | 260 |
logging.info("SHA1 checksums verified") |
169 | 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 |
|
|
170 | 304 |
|
171 | 305 |
class Converter(object): |
172 | 306 |
"""Converter class for OVF packages. |
... | ... | |
215 | 349 |
""" |
216 | 350 |
raise NotImplementedError() |
217 | 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 |
|
|
218 | 455 |
def Parse(self): |
219 | 456 |
"""Parses the data and creates a structure containing all required info. |
220 | 457 |
|
... | ... | |
249 | 486 |
@ivar input_path: complete path to the .ovf file |
250 | 487 |
@type ovf_reader: L{OVFReader} |
251 | 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 |
|
252 | 497 |
|
253 | 498 |
""" |
254 | 499 |
def _ReadInputData(self, input_path): |
... | ... | |
340 | 585 |
logging.info("OVA package extracted to %s directory", self.temp_dir) |
341 | 586 |
|
342 | 587 |
def Parse(self): |
343 |
pass |
|
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 |
|
344 | 741 |
|
345 | 742 |
def Save(self): |
346 | 743 |
"""Saves all the gathered information in a constant.EXPORT_CONF_FILE file. |
... | ... | |
370 | 767 |
for section, options in results.iteritems(): |
371 | 768 |
output.append("[%s]" % section) |
372 | 769 |
for name, value in options.iteritems(): |
770 |
if value is None: |
|
771 |
value = "" |
|
373 | 772 |
output.append("%s = %s" % (name, value)) |
374 | 773 |
output.append("") |
375 | 774 |
output_contents = "\n".join(output) |
Also available in: Unified diff