4 # Copyright (C) 2011 Google Inc.
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.
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.
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
22 """Converter tools between ovf and ganeti config file
26 # pylint: disable=F0401, E1101
28 # F0401 because ElementTree is not default for python 2.4
29 # E1101 makes no sense - pylint assumes that ElementTree object is a tuple
40 import xml.parsers.expat
42 import xml.etree.ElementTree as ET
44 import elementtree.ElementTree as ET
46 from ganeti import constants
47 from ganeti import errors
48 from ganeti import utils
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")
57 # File extensions in OVF package
62 COMPRESSION_EXT = ".gz"
69 COMPRESSION_TYPE = "gzip"
70 COMPRESS = "compression"
71 DECOMPRESS = "decompression"
72 ALLOWED_ACTIONS = [COMPRESS, DECOMPRESS]
75 def LinkFile(old_path, prefix=None, suffix=None, directory=None):
76 """Create link with a given prefix and suffix.
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
83 @param old_path: path to the file that is to be linked
85 @param prefix: prefix of filename for the link
87 @param suffix: suffix of the filename for the link
88 @type directory: string
89 @param directory: directory of the link
91 @raise errors.OpPrereqError: when error on linking is different than
95 assert(prefix is not None or suffix is not None)
97 directory = os.getcwd()
98 new_path = utils.PathJoin(directory, "%s%s" % (prefix, suffix))
102 os.link(old_path, new_path)
105 if err.errno == errno.EEXIST:
106 new_path = utils.PathJoin(directory,
107 "%s_%s%s" % (prefix, counter, suffix))
110 raise errors.OpPrereqError("Error moving the file %s to %s location:"
111 " %s" % (old_path, new_path, err))
115 class OVFReader(object):
116 """Reader class for OVF files.
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
128 def __init__(self, input_path):
129 """Initialiaze the reader - load the .ovf file to XML parser.
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
136 @type input_path: string
137 @param input_path: absolute path to the .ovf file
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
143 self.tree = ET.ElementTree()
145 self.tree.parse(input_path)
146 except xml.parsers.expat.ExpatError, err:
147 raise errors.OpPrereqError("Error while reading %s file: %s" %
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)
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
171 def _GetAttributes(self, path, attribute):
172 """Get specified attribute from all nodes accessible using given path.
174 Function follows the path from root node to the desired tags using path,
175 then reads the apropriate attribute values.
178 @param path: path of nodes to visit
179 @type attribute: string
180 @param attribute: attribute for which we gather the information
182 @return: for each accessible tag with the attribute value set, value of the
186 current_list = self.tree.findall(path)
187 results = [x.get(attribute) for x in current_list]
188 return filter(None, results)
190 def _GetElementMatchingAttr(self, path, match_attr):
191 """Searches for element on a path that matches certain attribute value.
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.
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
204 potential_elements = self.tree.findall(path)
205 (attr, val) = match_attr
206 for elem in potential_elements:
207 if elem.get(attr) == val:
211 def _GetElementMatchingText(self, path, match_text):
212 """Searches for element on a path that matches certain text value.
214 Function follows the path from root node to the desired tags using path,
215 then searches for the first one matching the text value.
218 @param path: path of nodes to visit
219 @type match_text: tuple
220 @param match_text: pair (node, text) for which we search
221 @rtype: ET.ElementTree or None
222 @return: first element matching match_text or None if nothing matches
225 potential_elements = self.tree.findall(path)
226 (node, text) = match_text
227 for elem in potential_elements:
228 if elem.findtext(node) == text:
233 def _GetDictParameters(root, schema):
234 """Reads text in all children and creates the dictionary from the contents.
236 @type root: ET.ElementTree or None
237 @param root: father of the nodes we want to collect data about
239 @param schema: schema name to be removed from the tag
241 @return: dictionary containing tags and their text contents, tags have their
242 schema fragment removed or empty dictionary, when root is None
248 for element in list(root):
249 pref_len = len("{%s}" % schema)
250 assert(schema in element.tag)
251 tag = element.tag[pref_len:]
252 results[tag] = element.text
255 def VerifyManifest(self):
256 """Verifies manifest for the OVF package, if one is given.
258 @raise errors.OpPrereqError: if SHA1 checksums do not match
261 if "%s%s" % (self.schema_name, MF_EXT) in self.files_list:
262 logging.warning("Verifying SHA1 checksums, this may take a while")
263 manifest_filename = "%s%s" % (self.schema_name, MF_EXT)
264 manifest_path = utils.PathJoin(self.input_dir, manifest_filename)
265 manifest_content = utils.ReadFile(manifest_path).splitlines()
267 regexp = r"SHA1\((\S+)\)= (\S+)"
268 for line in manifest_content:
269 match = re.match(regexp, line)
271 file_name = match.group(1)
272 sha1_sum = match.group(2)
273 manifest_files[file_name] = sha1_sum
274 files_with_paths = [utils.PathJoin(self.input_dir, file_name)
275 for file_name in self.files_list]
276 sha1_sums = utils.FingerprintFiles(files_with_paths)
277 for file_name, value in manifest_files.iteritems():
278 if sha1_sums.get(utils.PathJoin(self.input_dir, file_name)) != value:
279 raise errors.OpPrereqError("SHA1 checksum of %s does not match the"
280 " value in manifest file" % file_name)
281 logging.info("SHA1 checksums verified")
283 def GetInstanceName(self):
284 """Provides information about instance name.
287 @return: instance name string
290 find_name = "{%s}VirtualSystem/{%s}Name" % (OVF_SCHEMA, OVF_SCHEMA)
291 return self.tree.findtext(find_name)
293 def GetDiskTemplate(self):
294 """Returns disk template from .ovf file
296 @rtype: string or None
297 @return: name of the template
299 find_template = ("{%s}GanetiSection/{%s}DiskTemplate" %
300 (GANETI_SCHEMA, GANETI_SCHEMA))
301 return self.tree.findtext(find_template)
303 def GetNetworkData(self):
304 """Provides data about the network in the OVF instance.
306 The method gathers the data about networks used by OVF instance. It assumes
307 that 'name' tag means something - in essence, if it contains one of the
308 words 'bridged' or 'routed' then that will be the mode of this network in
309 Ganeti. The information about the network can be either in GanetiSection or
310 VirtualHardwareSection.
313 @return: dictionary containing all the network information
317 networks_search = ("{%s}NetworkSection/{%s}Network" %
318 (OVF_SCHEMA, OVF_SCHEMA))
319 network_names = self._GetAttributes(networks_search,
320 "{%s}name" % OVF_SCHEMA)
321 required = ["ip", "mac", "link", "mode"]
322 for (counter, network_name) in enumerate(network_names):
323 network_search = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item"
324 % (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
325 ganeti_search = ("{%s}GanetiSection/{%s}Network/{%s}Nic" %
326 (GANETI_SCHEMA, GANETI_SCHEMA, GANETI_SCHEMA))
327 network_match = ("{%s}Connection" % RASD_SCHEMA, network_name)
328 ganeti_match = ("{%s}name" % OVF_SCHEMA, network_name)
329 network_data = self._GetElementMatchingText(network_search, network_match)
330 network_ganeti_data = self._GetElementMatchingAttr(ganeti_search,
334 if network_ganeti_data:
335 ganeti_data["mode"] = network_ganeti_data.findtext("{%s}Mode" %
337 ganeti_data["mac"] = network_ganeti_data.findtext("{%s}MACAddress" %
339 ganeti_data["ip"] = network_ganeti_data.findtext("{%s}IPAddress" %
341 ganeti_data["link"] = network_ganeti_data.findtext("{%s}Link" %
345 mac_data = network_data.findtext("{%s}Address" % RASD_SCHEMA)
347 network_name = network_name.lower()
349 # First, some not Ganeti-specific information is collected
350 if constants.NIC_MODE_BRIDGED in network_name:
351 results["nic%s_mode" % counter] = "bridged"
352 elif constants.NIC_MODE_ROUTED in network_name:
353 results["nic%s_mode" % counter] = "routed"
354 results["nic%s_mac" % counter] = mac_data
356 # GanetiSection data overrides 'manually' collected data
357 for name, value in ganeti_data.iteritems():
358 results["nic%s_%s" % (counter, name)] = value
360 # Bridged network has no IP - unless specifically stated otherwise
361 if (results.get("nic%s_mode" % counter) == "bridged" and
362 not results.get("nic%s_ip" % counter)):
363 results["nic%s_ip" % counter] = constants.VALUE_NONE
365 for option in required:
366 if not results.get("nic%s_%s" % (counter, option)):
367 results["nic%s_%s" % (counter, option)] = constants.VALUE_AUTO
370 results["nic_count"] = str(len(network_names))
373 def GetDisksNames(self):
374 """Provides list of file names for the disks used by the instance.
377 @return: list of file names, as referenced in .ovf file
381 disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA)
382 disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA)
383 for disk in disk_ids:
384 disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA)
385 disk_match = ("{%s}id" % OVF_SCHEMA, disk)
386 disk_elem = self._GetElementMatchingAttr(disk_search, disk_match)
387 if disk_elem is None:
388 raise errors.OpPrereqError("%s file corrupted - disk %s not found in"
389 " references" % (OVF_EXT, disk))
390 disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA)
391 disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA)
392 results.append((disk_name, disk_compression))
396 class Converter(object):
397 """Converter class for OVF packages.
399 Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is
400 to provide a common interface for the two.
402 @type options: optparse.Values
403 @ivar options: options parsed from the command line
404 @type output_dir: string
405 @ivar output_dir: directory to which the results of conversion shall be
407 @type temp_file_manager: L{utils.TemporaryFileManager}
408 @ivar temp_file_manager: container for temporary files created during
410 @type temp_dir: string
411 @ivar temp_dir: temporary directory created then we deal with OVA
414 def __init__(self, input_path, options):
415 """Initialize the converter.
417 @type input_path: string
418 @param input_path: path to the Converter input file
419 @type options: optparse.Values
420 @param options: command line options
422 @raise errors.OpPrereqError: if file does not exist
425 input_path = os.path.abspath(input_path)
426 if not os.path.isfile(input_path):
427 raise errors.OpPrereqError("File does not exist: %s" % input_path)
428 self.options = options
429 self.temp_file_manager = utils.TemporaryFileManager()
431 self.output_dir = None
432 self._ReadInputData(input_path)
434 def _ReadInputData(self, input_path):
435 """Reads the data on which the conversion will take place.
437 @type input_path: string
438 @param input_path: absolute path to the Converter input file
441 raise NotImplementedError()
443 def _CompressDisk(self, disk_path, compression, action):
444 """Performs (de)compression on the disk and returns the new path
446 @type disk_path: string
447 @param disk_path: path to the disk
448 @type compression: string
449 @param compression: compression type
451 @param action: whether the action is compression or decompression
453 @return: new disk path after (de)compression
455 @raise errors.OpPrereqError: disk (de)compression failed or "compression"
459 assert(action in ALLOWED_ACTIONS)
460 # For now we only support gzip, as it is used in ovftool
461 if compression != COMPRESSION_TYPE:
462 raise errors.OpPrereqError("Unsupported compression type: %s"
464 disk_file = os.path.basename(disk_path)
465 if action == DECOMPRESS:
466 (disk_name, _) = os.path.splitext(disk_file)
468 elif action == COMPRESS:
470 new_path = utils.GetClosedTempfile(suffix=COMPRESSION_EXT, prefix=prefix,
472 self.temp_file_manager.Add(new_path)
473 args = ["gzip", "-c", disk_path]
474 run_result = utils.RunCmd(args, output=new_path)
475 if run_result.failed:
476 raise errors.OpPrereqError("Disk %s failed with output: %s"
477 % (action, run_result.stderr))
478 logging.info("The %s of the disk is completed", action)
479 return (COMPRESSION_EXT, new_path)
481 def _ConvertDisk(self, disk_format, disk_path):
482 """Performes conversion to specified format.
484 @type disk_format: string
485 @param disk_format: format to which the disk should be converted
486 @type disk_path: string
487 @param disk_path: path to the disk that should be converted
489 @return path to the output disk
491 @raise errors.OpPrereqError: convertion of the disk failed
494 disk_file = os.path.basename(disk_path)
495 (disk_name, disk_extension) = os.path.splitext(disk_file)
496 if disk_extension != disk_format:
497 logging.warning("Conversion of disk image to %s format, this may take"
498 " a while", disk_format)
500 new_disk_path = utils.GetClosedTempfile(suffix=".%s" % disk_format,
501 prefix=disk_name, dir=self.output_dir)
502 self.temp_file_manager.Add(new_disk_path)
511 run_result = utils.RunCmd(args, cwd=os.getcwd())
512 if run_result.failed:
513 raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was"
514 ": %s" % (disk_format, run_result.stderr))
515 return (".%s" % disk_format, new_disk_path)
518 def _GetDiskQemuInfo(disk_path, regexp):
519 """Figures out some information of the disk using qemu-img.
521 @type disk_path: string
522 @param disk_path: path to the disk we want to know the format of
524 @param regexp: string that has to be matched, it has to contain one group
528 @raise errors.OpPrereqError: format information cannot be retrieved
531 args = ["qemu-img", "info", disk_path]
532 run_result = utils.RunCmd(args, cwd=os.getcwd())
533 if run_result.failed:
534 raise errors.OpPrereqError("Gathering info about the disk using qemu-img"
535 " failed, output was: %s" % run_result.stderr)
536 result = run_result.output
537 regexp = r"%s" % regexp
538 match = re.search(regexp, result)
540 disk_format = match.group(1)
542 raise errors.OpPrereqError("No file information matching %s found in:"
543 " %s" % (regexp, result))
547 """Parses the data and creates a structure containing all required info.
550 raise NotImplementedError()
553 """Saves the gathered configuration in an apropriate format.
556 raise NotImplementedError()
559 """Cleans the temporary directory, if one was created.
562 self.temp_file_manager.Cleanup()
564 shutil.rmtree(self.temp_dir)
568 class OVFImporter(Converter):
569 """Converter from OVF to Ganeti config file.
571 @type input_dir: string
572 @ivar input_dir: directory in which the .ovf file resides
573 @type output_dir: string
574 @ivar output_dir: directory to which the results of conversion shall be
576 @type input_path: string
577 @ivar input_path: complete path to the .ovf file
578 @type ovf_reader: L{OVFReader}
579 @ivar ovf_reader: OVF reader instance collects data from .ovf file
580 @type results_name: string
581 @ivar results_name: name of imported instance
582 @type results_template: string
583 @ivar results_template: disk template read from .ovf file or command line
585 @type results_network: dict
586 @ivar results_network: network information gathered from .ovf file or command
588 @type results_disk: dict
589 @ivar results_disk: disk information gathered from .ovf file or command line
593 def _ReadInputData(self, input_path):
594 """Reads the data on which the conversion will take place.
596 @type input_path: string
597 @param input_path: absolute path to the .ovf or .ova input file
599 @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
602 (input_dir, input_file) = os.path.split(input_path)
603 (_, input_extension) = os.path.splitext(input_file)
605 if input_extension == OVF_EXT:
606 logging.info("%s file extension found, no unpacking necessary", OVF_EXT)
607 self.input_dir = input_dir
608 self.input_path = input_path
610 elif input_extension == OVA_EXT:
611 logging.info("%s file extension found, proceeding to unpacking", OVA_EXT)
612 self._UnpackOVA(input_path)
614 raise errors.OpPrereqError("Unknown file extension; expected %s or %s"
615 " file" % (OVA_EXT, OVF_EXT))
616 assert ((input_extension == OVA_EXT and self.temp_dir) or
617 (input_extension == OVF_EXT and not self.temp_dir))
618 assert self.input_dir in self.input_path
620 if self.options.output_dir:
621 self.output_dir = os.path.abspath(self.options.output_dir)
622 if (os.path.commonprefix([constants.EXPORT_DIR, self.output_dir]) !=
623 constants.EXPORT_DIR):
624 logging.warning("Export path is not under %s directory, import to"
625 " Ganeti using gnt-backup may fail",
626 constants.EXPORT_DIR)
628 self.output_dir = constants.EXPORT_DIR
630 self.ovf_reader = OVFReader(self.input_path)
631 self.ovf_reader.VerifyManifest()
633 def _UnpackOVA(self, input_path):
634 """Unpacks the .ova package into temporary directory.
636 @type input_path: string
637 @param input_path: path to the .ova package file
639 @raise errors.OpPrereqError: if file is not a proper tarball, one of the
640 files in the archive seem malicious (e.g. path starts with '../') or
641 .ova package does not contain .ovf file
645 if not tarfile.is_tarfile(input_path):
646 raise errors.OpPrereqError("The provided %s file is not a proper tar"
648 ova_content = tarfile.open(input_path)
649 temp_dir = tempfile.mkdtemp()
650 self.temp_dir = temp_dir
651 for file_name in ova_content.getnames():
652 file_normname = os.path.normpath(file_name)
654 utils.PathJoin(temp_dir, file_normname)
655 except ValueError, err:
656 raise errors.OpPrereqError("File %s inside %s package is not safe" %
657 (file_name, OVA_EXT))
658 if file_name.endswith(OVF_EXT):
659 input_name = file_name
661 raise errors.OpPrereqError("No %s file in %s package found" %
663 logging.warning("Unpacking the %s archive, this may take a while",
665 self.input_dir = temp_dir
666 self.input_path = utils.PathJoin(self.temp_dir, input_name)
669 extract = ova_content.extractall
670 except AttributeError:
671 # This is a prehistorical case of using python < 2.5
672 for member in ova_content.getmembers():
673 ova_content.extract(member, path=self.temp_dir)
675 extract(self.temp_dir)
676 except tarfile.TarError, err:
677 raise errors.OpPrereqError("Error while extracting %s archive: %s" %
679 logging.info("OVA package extracted to %s directory", self.temp_dir)
682 """Parses the data and creates a structure containing all required info.
684 The method reads the information given either as a command line option or as
685 a part of the OVF description.
687 @raise errors.OpPrereqError: if some required part of the description of
688 virtual instance is missing or unable to create output directory
691 self.results_name = self._GetInfo("instance name", self.options.name,
692 self._ParseNameOptions, self.ovf_reader.GetInstanceName)
693 if not self.results_name:
694 raise errors.OpPrereqError("Name of instance not provided")
696 self.output_dir = utils.PathJoin(self.output_dir, self.results_name)
698 utils.Makedirs(self.output_dir)
700 raise errors.OpPrereqError("Failed to create directory %s: %s" %
701 (self.output_dir, err))
703 self.results_template = self._GetInfo("disk template",
704 self.options.disk_template, self._ParseTemplateOptions,
705 self.ovf_reader.GetDiskTemplate)
706 if not self.results_template:
707 logging.info("Disk template not given")
709 self.results_network = self._GetInfo("network", self.options.nics,
710 self._ParseNicOptions, self.ovf_reader.GetNetworkData,
711 ignore_test=self.options.no_nics)
713 self.results_disk = self._GetInfo("disk", self.options.disks,
714 self._ParseDiskOptions, self._GetDiskInfo,
715 ignore_test=self.results_template == constants.DT_DISKLESS)
717 if not self.results_disk and not self.results_network:
718 raise errors.OpPrereqError("Either disk specification or network"
719 " description must be present")
722 def _GetInfo(name, cmd_arg, cmd_function, nocmd_function,
724 """Get information about some section - e.g. disk, network, hypervisor.
727 @param name: name of the section
729 @param cmd_arg: command line argument specific for section 'name'
730 @type cmd_function: callable
731 @param cmd_function: function to call if 'cmd_args' exists
732 @type nocmd_function: callable
733 @param nocmd_function: function to call if 'cmd_args' is not there
737 logging.info("Information for %s will be ignored", name)
740 logging.info("Information for %s will be parsed from command line", name)
741 results = cmd_function()
743 logging.info("Information for %s will be parsed from %s file",
745 results = nocmd_function()
746 logging.info("Options for %s were succesfully read", name)
749 def _ParseNameOptions(self):
750 """Returns name if one was given in command line.
753 @return: name of an instance
756 return self.options.name
758 def _ParseTemplateOptions(self):
759 """Returns disk template if one was given in command line.
762 @return: disk template name
765 return self.options.disk_template
767 def _ParseNicOptions(self):
768 """Parses network options given in a command line or as a dictionary.
771 @return: dictionary of network-related options
774 assert self.options.nics
776 for (nic_id, nic_desc) in self.options.nics:
777 results["nic%s_mode" % nic_id] = \
778 nic_desc.get("mode", constants.VALUE_AUTO)
779 results["nic%s_mac" % nic_id] = nic_desc.get("mac", constants.VALUE_AUTO)
780 results["nic%s_link" % nic_id] = \
781 nic_desc.get("link", constants.VALUE_AUTO)
782 if nic_desc.get("mode") == "bridged":
783 results["nic%s_ip" % nic_id] = constants.VALUE_NONE
785 results["nic%s_ip" % nic_id] = constants.VALUE_AUTO
786 results["nic_count"] = str(len(self.options.nics))
789 def _ParseDiskOptions(self):
790 """Parses disk options given in a command line.
793 @return: dictionary of disk-related options
795 @raise errors.OpPrereqError: disk description does not contain size
796 information or size information is invalid or creation failed
799 assert self.options.disks
801 for (disk_id, disk_desc) in self.options.disks:
802 results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id
803 if disk_desc.get("size"):
805 disk_size = utils.ParseUnit(disk_desc["size"])
807 raise errors.OpPrereqError("Invalid disk size for disk %s: %s" %
808 (disk_id, disk_desc["size"]))
809 new_path = utils.PathJoin(self.output_dir, str(disk_id))
818 run_result = utils.RunCmd(args)
819 if run_result.failed:
820 raise errors.OpPrereqError("Creation of disk %s failed, output was:"
821 " %s" % (new_path, run_result.stderr))
822 results["disk%s_size" % disk_id] = str(disk_size)
823 results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id
825 raise errors.OpPrereqError("Disks created for import must have their"
827 results["disk_count"] = str(len(self.options.disks))
830 def _GetDiskInfo(self):
831 """Gathers information about disks used by instance, perfomes conversion.
834 @return: dictionary of disk-related options
836 @raise errors.OpPrereqError: disk is not in the same directory as .ovf file
840 disks_list = self.ovf_reader.GetDisksNames()
841 for (counter, (disk_name, disk_compression)) in enumerate(disks_list):
842 if os.path.dirname(disk_name):
843 raise errors.OpPrereqError("Disks are not allowed to have absolute"
844 " paths or paths outside main OVF directory")
845 disk, _ = os.path.splitext(disk_name)
846 disk_path = utils.PathJoin(self.input_dir, disk_name)
848 _, disk_path = self._CompressDisk(disk_path, disk_compression,
850 disk, _ = os.path.splitext(disk)
851 if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw":
852 logging.info("Conversion to raw format is required")
853 ext, new_disk_path = self._ConvertDisk("raw", disk_path)
855 final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext,
856 directory=self.output_dir)
857 final_name = os.path.basename(final_disk_path)
858 disk_size = os.path.getsize(final_disk_path) / (1024 * 1024)
859 results["disk%s_dump" % counter] = final_name
860 results["disk%s_size" % counter] = str(disk_size)
861 results["disk%s_ivname" % counter] = "disk/%s" % str(counter)
863 results["disk_count"] = str(len(disks_list))
867 """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
869 @raise errors.OpPrereqError: when saving to config file failed
872 logging.info("Conversion was succesfull, saving %s in %s directory",
873 constants.EXPORT_CONF_FILE, self.output_dir)
875 constants.INISECT_INS: {},
876 constants.INISECT_BEP: {},
877 constants.INISECT_EXP: {},
878 constants.INISECT_OSP: {},
879 constants.INISECT_HYP: {},
882 results[constants.INISECT_INS].update(self.results_disk)
883 results[constants.INISECT_INS].update(self.results_network)
884 results[constants.INISECT_INS]["name"] = self.results_name
885 if self.results_template:
886 results[constants.INISECT_INS]["disk_template"] = self.results_template
888 output_file_name = utils.PathJoin(self.output_dir,
889 constants.EXPORT_CONF_FILE)
892 for section, options in results.iteritems():
893 output.append("[%s]" % section)
894 for name, value in options.iteritems():
897 output.append("%s = %s" % (name, value))
899 output_contents = "\n".join(output)
902 utils.WriteFile(output_file_name, data=output_contents)
903 except errors.ProgrammerError, err:
904 raise errors.OpPrereqError("Saving the config file failed: %s" % err)
909 class OVFExporter(Converter):
910 def _ReadInputData(self, input_path):