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
41 import xml.dom.minidom
42 import xml.parsers.expat
44 import xml.etree.ElementTree as ET
46 import elementtree.ElementTree as ET
49 ParseError = ET.ParseError # pylint: disable=E1103
50 except AttributeError:
53 from ganeti import constants
54 from ganeti import errors
55 from ganeti import utils
58 # Schemas used in OVF format
59 GANETI_SCHEMA = "http://ganeti"
60 OVF_SCHEMA = "http://schemas.dmtf.org/ovf/envelope/1"
61 RASD_SCHEMA = ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/"
62 "CIM_ResourceAllocationSettingData")
63 VSSD_SCHEMA = ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/"
64 "CIM_VirtualSystemSettingData")
65 XML_SCHEMA = "http://www.w3.org/2001/XMLSchema-instance"
67 # File extensions in OVF package
72 COMPRESSION_EXT = ".gz"
79 COMPRESSION_TYPE = "gzip"
80 NO_COMPRESSION = [None, "identity"]
81 COMPRESS = "compression"
82 DECOMPRESS = "decompression"
83 ALLOWED_ACTIONS = [COMPRESS, DECOMPRESS]
88 ALLOWED_FORMATS = [RAW, COW, VMDK]
94 "scsi-controller": "6",
95 "ethernet-adapter": "10",
99 SCSI_SUBTYPE = "lsilogic"
101 "ganeti": "ganeti-ovf",
102 "external": "vmx-04",
105 # AllocationUnits values and conversion
108 'kb': ["kilobytes", "kb", "byte * 2^10", "kibibytes", "kib"],
109 'mb': ["megabytes", "mb", "byte * 2^20", "mebibytes", "mib"],
110 'gb': ["gigabytes", "gb", "byte * 2^30", "gibibytes", "gib"],
112 CONVERT_UNITS_TO_MB = {
113 'b': lambda x: x / (1024 * 1024),
114 'kb': lambda x: x / 1024,
116 'gb': lambda x: x * 1024,
119 # Names of the config fields
122 HYPERV = "hypervisor"
125 AUTO_BALANCE = "auto_balance"
126 DISK_TEMPLATE = "disk_template"
130 # Instance IDs of System and SCSI controller
138 # Disk format descriptions
140 RAW: "http://en.wikipedia.org/wiki/Byte",
141 VMDK: "http://www.vmware.com/interfaces/specifications/vmdk.html"
143 COW: "http://www.gnome.org/~markmc/qcow-image-format.html",
148 """ Make sure that qemu-img is present before performing operations.
150 @raise errors.OpPrereqError: when qemu-img was not found in the system
153 if not constants.QEMUIMG_PATH:
154 raise errors.OpPrereqError("qemu-img not found at build time, unable"
158 def LinkFile(old_path, prefix=None, suffix=None, directory=None):
159 """Create link with a given prefix and suffix.
161 This is a wrapper over os.link. It tries to create a hard link for given file,
162 but instead of rising error when file exists, the function changes the name
165 @type old_path:string
166 @param old_path: path to the file that is to be linked
168 @param prefix: prefix of filename for the link
170 @param suffix: suffix of the filename for the link
171 @type directory: string
172 @param directory: directory of the link
174 @raise errors.OpPrereqError: when error on linking is different than
178 assert(prefix is not None or suffix is not None)
179 if directory is None:
180 directory = os.getcwd()
181 new_path = utils.PathJoin(directory, "%s%s" % (prefix, suffix))
185 os.link(old_path, new_path)
188 if err.errno == errno.EEXIST:
189 new_path = utils.PathJoin(directory,
190 "%s_%s%s" % (prefix, counter, suffix))
193 raise errors.OpPrereqError("Error moving the file %s to %s location:"
194 " %s" % (old_path, new_path, err))
198 class OVFReader(object):
199 """Reader class for OVF files.
201 @type files_list: list
202 @ivar files_list: list of files in the OVF package
203 @type tree: ET.ElementTree
204 @ivar tree: XML tree of the .ovf file
205 @type schema_name: string
206 @ivar schema_name: name of the .ovf file
207 @type input_dir: string
208 @ivar input_dir: directory in which the .ovf file resides
211 def __init__(self, input_path):
212 """Initialiaze the reader - load the .ovf file to XML parser.
214 It is assumed that names of manifesto (.mf), certificate (.cert) and ovf
215 files are the same. In order to account any other files as part of the ovf
216 package, they have to be explicitly mentioned in the Resources section
219 @type input_path: string
220 @param input_path: absolute path to the .ovf file
222 @raise errors.OpPrereqError: when .ovf file is not a proper XML file or some
223 of the files mentioned in Resources section do not exist
226 self.tree = ET.ElementTree()
228 self.tree.parse(input_path)
229 except (ParseError, xml.parsers.expat.ExpatError), err:
230 raise errors.OpPrereqError("Error while reading %s file: %s" %
233 # Create a list of all files in the OVF package
234 (input_dir, input_file) = os.path.split(input_path)
235 (input_name, _) = os.path.splitext(input_file)
236 files_directory = utils.ListVisibleFiles(input_dir)
238 for file_name in files_directory:
239 (name, extension) = os.path.splitext(file_name)
240 if extension in FILE_EXTENSIONS and name == input_name:
241 files_list.append(file_name)
242 files_list += self._GetAttributes("{%s}References/{%s}File" %
243 (OVF_SCHEMA, OVF_SCHEMA),
244 "{%s}href" % OVF_SCHEMA)
245 for file_name in files_list:
246 file_path = utils.PathJoin(input_dir, file_name)
247 if not os.path.exists(file_path):
248 raise errors.OpPrereqError("File does not exist: %s" % file_path)
249 logging.info("Files in the OVF package: %s", " ".join(files_list))
250 self.files_list = files_list
251 self.input_dir = input_dir
252 self.schema_name = input_name
254 def _GetAttributes(self, path, attribute):
255 """Get specified attribute from all nodes accessible using given path.
257 Function follows the path from root node to the desired tags using path,
258 then reads the apropriate attribute values.
261 @param path: path of nodes to visit
262 @type attribute: string
263 @param attribute: attribute for which we gather the information
265 @return: for each accessible tag with the attribute value set, value of the
269 current_list = self.tree.findall(path)
270 results = [x.get(attribute) for x in current_list]
271 return filter(None, results)
273 def _GetElementMatchingAttr(self, path, match_attr):
274 """Searches for element on a path that matches certain attribute value.
276 Function follows the path from root node to the desired tags using path,
277 then searches for the first one matching the attribute value.
280 @param path: path of nodes to visit
281 @type match_attr: tuple
282 @param match_attr: pair (attribute, value) for which we search
283 @rtype: ET.ElementTree or None
284 @return: first element matching match_attr or None if nothing matches
287 potential_elements = self.tree.findall(path)
288 (attr, val) = match_attr
289 for elem in potential_elements:
290 if elem.get(attr) == val:
294 def _GetElementMatchingText(self, path, match_text):
295 """Searches for element on a path that matches certain text value.
297 Function follows the path from root node to the desired tags using path,
298 then searches for the first one matching the text value.
301 @param path: path of nodes to visit
302 @type match_text: tuple
303 @param match_text: pair (node, text) for which we search
304 @rtype: ET.ElementTree or None
305 @return: first element matching match_text or None if nothing matches
308 potential_elements = self.tree.findall(path)
309 (node, text) = match_text
310 for elem in potential_elements:
311 if elem.findtext(node) == text:
316 def _GetDictParameters(root, schema):
317 """Reads text in all children and creates the dictionary from the contents.
319 @type root: ET.ElementTree or None
320 @param root: father of the nodes we want to collect data about
322 @param schema: schema name to be removed from the tag
324 @return: dictionary containing tags and their text contents, tags have their
325 schema fragment removed or empty dictionary, when root is None
331 for element in list(root):
332 pref_len = len("{%s}" % schema)
333 assert(schema in element.tag)
334 tag = element.tag[pref_len:]
335 results[tag] = element.text
338 def VerifyManifest(self):
339 """Verifies manifest for the OVF package, if one is given.
341 @raise errors.OpPrereqError: if SHA1 checksums do not match
344 if "%s%s" % (self.schema_name, MF_EXT) in self.files_list:
345 logging.warning("Verifying SHA1 checksums, this may take a while")
346 manifest_filename = "%s%s" % (self.schema_name, MF_EXT)
347 manifest_path = utils.PathJoin(self.input_dir, manifest_filename)
348 manifest_content = utils.ReadFile(manifest_path).splitlines()
350 regexp = r"SHA1\((\S+)\)= (\S+)"
351 for line in manifest_content:
352 match = re.match(regexp, line)
354 file_name = match.group(1)
355 sha1_sum = match.group(2)
356 manifest_files[file_name] = sha1_sum
357 files_with_paths = [utils.PathJoin(self.input_dir, file_name)
358 for file_name in self.files_list]
359 sha1_sums = utils.FingerprintFiles(files_with_paths)
360 for file_name, value in manifest_files.iteritems():
361 if sha1_sums.get(utils.PathJoin(self.input_dir, file_name)) != value:
362 raise errors.OpPrereqError("SHA1 checksum of %s does not match the"
363 " value in manifest file" % file_name)
364 logging.info("SHA1 checksums verified")
366 def GetInstanceName(self):
367 """Provides information about instance name.
370 @return: instance name string
373 find_name = "{%s}VirtualSystem/{%s}Name" % (OVF_SCHEMA, OVF_SCHEMA)
374 return self.tree.findtext(find_name)
376 def GetDiskTemplate(self):
377 """Returns disk template from .ovf file
379 @rtype: string or None
380 @return: name of the template
382 find_template = ("{%s}GanetiSection/{%s}DiskTemplate" %
383 (GANETI_SCHEMA, GANETI_SCHEMA))
384 return self.tree.findtext(find_template)
386 def GetHypervisorData(self):
387 """Provides hypervisor information - hypervisor name and options.
390 @return: dictionary containing name of the used hypervisor and all the
394 hypervisor_search = ("{%s}GanetiSection/{%s}Hypervisor" %
395 (GANETI_SCHEMA, GANETI_SCHEMA))
396 hypervisor_data = self.tree.find(hypervisor_search)
397 if not hypervisor_data:
398 return {"hypervisor_name": constants.VALUE_AUTO}
400 "hypervisor_name": hypervisor_data.findtext("{%s}Name" % GANETI_SCHEMA,
401 default=constants.VALUE_AUTO),
403 parameters = hypervisor_data.find("{%s}Parameters" % GANETI_SCHEMA)
404 results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
408 """ Provides operating system information - os name and options.
411 @return: dictionary containing name and options for the chosen OS
415 os_search = ("{%s}GanetiSection/{%s}OperatingSystem" %
416 (GANETI_SCHEMA, GANETI_SCHEMA))
417 os_data = self.tree.find(os_search)
419 results["os_name"] = os_data.findtext("{%s}Name" % GANETI_SCHEMA)
420 parameters = os_data.find("{%s}Parameters" % GANETI_SCHEMA)
421 results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
424 def GetBackendData(self):
425 """ Provides backend information - vcpus, memory, auto balancing options.
428 @return: dictionary containing options for vcpus, memory and auto balance
434 find_vcpus = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item" %
435 (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
436 match_vcpus = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["vcpus"])
437 vcpus = self._GetElementMatchingText(find_vcpus, match_vcpus)
439 vcpus_count = vcpus.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
440 default=constants.VALUE_AUTO)
442 vcpus_count = constants.VALUE_AUTO
443 results["vcpus"] = str(vcpus_count)
445 find_memory = find_vcpus
446 match_memory = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["memory"])
447 memory = self._GetElementMatchingText(find_memory, match_memory)
450 alloc_units = memory.findtext("{%s}AllocationUnits" % RASD_SCHEMA)
451 matching_units = [units for units, variants in
452 ALLOCATION_UNITS.iteritems() if alloc_units.lower() in variants]
453 if matching_units == []:
454 raise errors.OpPrereqError("Unit %s for RAM memory unknown",
456 units = matching_units[0]
457 memory_raw = int(memory.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
458 default=constants.VALUE_AUTO))
459 memory_count = CONVERT_UNITS_TO_MB[units](memory_raw)
461 memory_count = constants.VALUE_AUTO
462 results["memory"] = str(memory_count)
464 find_balance = ("{%s}GanetiSection/{%s}AutoBalance" %
465 (GANETI_SCHEMA, GANETI_SCHEMA))
466 balance = self.tree.findtext(find_balance, default=constants.VALUE_AUTO)
467 results["auto_balance"] = balance
471 def GetTagsData(self):
472 """Provides tags information for instance.
474 @rtype: string or None
475 @return: string of comma-separated tags for the instance
478 find_tags = "{%s}GanetiSection/{%s}Tags" % (GANETI_SCHEMA, GANETI_SCHEMA)
479 results = self.tree.findtext(find_tags)
485 def GetVersionData(self):
486 """Provides version number read from .ovf file
489 @return: string containing the version number
492 find_version = ("{%s}GanetiSection/{%s}Version" %
493 (GANETI_SCHEMA, GANETI_SCHEMA))
494 return self.tree.findtext(find_version)
496 def GetNetworkData(self):
497 """Provides data about the network in the OVF instance.
499 The method gathers the data about networks used by OVF instance. It assumes
500 that 'name' tag means something - in essence, if it contains one of the
501 words 'bridged' or 'routed' then that will be the mode of this network in
502 Ganeti. The information about the network can be either in GanetiSection or
503 VirtualHardwareSection.
506 @return: dictionary containing all the network information
510 networks_search = ("{%s}NetworkSection/{%s}Network" %
511 (OVF_SCHEMA, OVF_SCHEMA))
512 network_names = self._GetAttributes(networks_search,
513 "{%s}name" % OVF_SCHEMA)
514 required = ["ip", "mac", "link", "mode"]
515 for (counter, network_name) in enumerate(network_names):
516 network_search = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item"
517 % (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
518 ganeti_search = ("{%s}GanetiSection/{%s}Network/{%s}Nic" %
519 (GANETI_SCHEMA, GANETI_SCHEMA, GANETI_SCHEMA))
520 network_match = ("{%s}Connection" % RASD_SCHEMA, network_name)
521 ganeti_match = ("{%s}name" % OVF_SCHEMA, network_name)
522 network_data = self._GetElementMatchingText(network_search, network_match)
523 network_ganeti_data = self._GetElementMatchingAttr(ganeti_search,
527 if network_ganeti_data:
528 ganeti_data["mode"] = network_ganeti_data.findtext("{%s}Mode" %
530 ganeti_data["mac"] = network_ganeti_data.findtext("{%s}MACAddress" %
532 ganeti_data["ip"] = network_ganeti_data.findtext("{%s}IPAddress" %
534 ganeti_data["link"] = network_ganeti_data.findtext("{%s}Link" %
538 mac_data = network_data.findtext("{%s}Address" % RASD_SCHEMA)
540 network_name = network_name.lower()
542 # First, some not Ganeti-specific information is collected
543 if constants.NIC_MODE_BRIDGED in network_name:
544 results["nic%s_mode" % counter] = "bridged"
545 elif constants.NIC_MODE_ROUTED in network_name:
546 results["nic%s_mode" % counter] = "routed"
547 results["nic%s_mac" % counter] = mac_data
549 # GanetiSection data overrides 'manually' collected data
550 for name, value in ganeti_data.iteritems():
551 results["nic%s_%s" % (counter, name)] = value
553 # Bridged network has no IP - unless specifically stated otherwise
554 if (results.get("nic%s_mode" % counter) == "bridged" and
555 not results.get("nic%s_ip" % counter)):
556 results["nic%s_ip" % counter] = constants.VALUE_NONE
558 for option in required:
559 if not results.get("nic%s_%s" % (counter, option)):
560 results["nic%s_%s" % (counter, option)] = constants.VALUE_AUTO
563 results["nic_count"] = str(len(network_names))
566 def GetDisksNames(self):
567 """Provides list of file names for the disks used by the instance.
570 @return: list of file names, as referenced in .ovf file
574 disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA)
575 disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA)
576 for disk in disk_ids:
577 disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA)
578 disk_match = ("{%s}id" % OVF_SCHEMA, disk)
579 disk_elem = self._GetElementMatchingAttr(disk_search, disk_match)
580 if disk_elem is None:
581 raise errors.OpPrereqError("%s file corrupted - disk %s not found in"
582 " references" % (OVF_EXT, disk))
583 disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA)
584 disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA)
585 results.append((disk_name, disk_compression))
589 def SubElementText(parent, tag, text, attrib={}, **extra):
590 # pylint: disable=W0102
591 """This is just a wrapper on ET.SubElement that always has text content.
596 elem = ET.SubElement(parent, tag, attrib=attrib, **extra)
597 elem.text = str(text)
601 class OVFWriter(object):
602 """Writer class for OVF files.
604 @type tree: ET.ElementTree
605 @ivar tree: XML tree that we are constructing
606 @type virtual_system_type: string
607 @ivar virtual_system_type: value of vssd:VirtualSystemType, for external usage
608 in VMWare this requires to be vmx
609 @type hardware_list: list
610 @ivar hardware_list: list of items prepared for VirtualHardwareSection
611 @type next_instance_id: int
612 @ivar next_instance_id: next instance id to be used when creating elements on
616 def __init__(self, has_gnt_section):
617 """Initialize the writer - set the top element.
619 @type has_gnt_section: bool
620 @param has_gnt_section: if the Ganeti schema should be added - i.e. this
621 means that Ganeti section will be present
625 "xmlns:xsi": XML_SCHEMA,
626 "xmlns:vssd": VSSD_SCHEMA,
627 "xmlns:rasd": RASD_SCHEMA,
628 "xmlns:ovf": OVF_SCHEMA,
633 env_attribs["xmlns:gnt"] = GANETI_SCHEMA
634 self.virtual_system_type = VS_TYPE["ganeti"]
636 self.virtual_system_type = VS_TYPE["external"]
637 self.tree = ET.Element("Envelope", attrib=env_attribs)
638 self.hardware_list = []
639 # INSTANCE_ID contains statically assigned IDs, starting from 0
640 self.next_instance_id = len(INSTANCE_ID) # FIXME: hackish
642 def SaveDisksData(self, disks):
643 """Convert disk information to certain OVF sections.
646 @param disks: list of dictionaries of disk options from config.ini
649 references = ET.SubElement(self.tree, "References")
650 disk_section = ET.SubElement(self.tree, "DiskSection")
651 SubElementText(disk_section, "Info", "Virtual disk information")
652 for counter, disk in enumerate(disks):
653 file_id = "file%s" % counter
654 disk_id = "disk%s" % counter
656 "ovf:href": disk["path"],
657 "ovf:size": str(disk["real-size"]),
661 "ovf:capacity": str(disk["virt-size"]),
662 "ovf:diskId": disk_id,
663 "ovf:fileRef": file_id,
664 "ovf:format": DISK_FORMAT.get(disk["format"], disk["format"]),
666 if "compression" in disk:
667 file_attribs["ovf:compression"] = disk["compression"]
668 ET.SubElement(references, "File", attrib=file_attribs)
669 ET.SubElement(disk_section, "Disk", attrib=disk_attribs)
671 # Item in VirtualHardwareSection creation
672 disk_item = ET.Element("Item")
673 SubElementText(disk_item, "rasd:ElementName", disk_id)
674 SubElementText(disk_item, "rasd:HostResource", "ovf:/disk/%s" % disk_id)
675 SubElementText(disk_item, "rasd:InstanceID", self.next_instance_id)
676 SubElementText(disk_item, "rasd:Parent", INSTANCE_ID["scsi"])
677 SubElementText(disk_item, "rasd:ResourceType", RASD_TYPE["disk"])
678 self.hardware_list.append(disk_item)
679 self.next_instance_id += 1
681 def SaveNetworksData(self, networks):
682 """Convert network information to NetworkSection.
685 @param networks: list of dictionaries of network options form config.ini
688 network_section = ET.SubElement(self.tree, "NetworkSection")
689 SubElementText(network_section, "Info", "List of logical networks")
690 for counter, network in enumerate(networks):
691 network_name = "%s%s" % (network["mode"], counter)
692 network_attrib = {"ovf:name": network_name}
693 ET.SubElement(network_section, "Network", attrib=network_attrib)
695 # Item in VirtualHardwareSection creation
696 network_item = ET.Element("Item")
697 SubElementText(network_item, "rasd:Address", network["mac"])
698 SubElementText(network_item, "rasd:Connection", network_name)
699 SubElementText(network_item, "rasd:ElementName", network_name)
700 SubElementText(network_item, "rasd:InstanceID", self.next_instance_id)
701 SubElementText(network_item, "rasd:ResourceType",
702 RASD_TYPE["ethernet-adapter"])
703 self.hardware_list.append(network_item)
704 self.next_instance_id += 1
707 def _SaveNameAndParams(root, data):
708 """Save name and parameters information under root using data.
710 @type root: ET.Element
711 @param root: root element for the Name and Parameters
713 @param data: data from which we gather the values
716 assert(data.get("name"))
717 name = SubElementText(root, "gnt:Name", data["name"])
718 params = ET.SubElement(root, "gnt:Parameters")
719 for name, value in data.iteritems():
721 SubElementText(params, "gnt:%s" % name, value)
723 def SaveGanetiData(self, ganeti, networks):
724 """Convert Ganeti-specific information to GanetiSection.
727 @param ganeti: dictionary of Ganeti-specific options from config.ini
729 @param networks: list of dictionaries of network options form config.ini
732 ganeti_section = ET.SubElement(self.tree, "gnt:GanetiSection")
734 SubElementText(ganeti_section, "gnt:Version", ganeti.get("version"))
735 SubElementText(ganeti_section, "gnt:DiskTemplate",
736 ganeti.get("disk_template"))
737 SubElementText(ganeti_section, "gnt:AutoBalance",
738 ganeti.get("auto_balance"))
739 SubElementText(ganeti_section, "gnt:Tags", ganeti.get("tags"))
741 osys = ET.SubElement(ganeti_section, "gnt:OperatingSystem")
742 self._SaveNameAndParams(osys, ganeti["os"])
744 hypervisor = ET.SubElement(ganeti_section, "gnt:Hypervisor")
745 self._SaveNameAndParams(hypervisor, ganeti["hypervisor"])
747 network_section = ET.SubElement(ganeti_section, "gnt:Network")
748 for counter, network in enumerate(networks):
749 network_name = "%s%s" % (network["mode"], counter)
750 nic_attrib = {"ovf:name": network_name}
751 nic = ET.SubElement(network_section, "gnt:Nic", attrib=nic_attrib)
752 SubElementText(nic, "gnt:Mode", network["mode"])
753 SubElementText(nic, "gnt:MACAddress", network["mac"])
754 SubElementText(nic, "gnt:IPAddress", network["ip"])
755 SubElementText(nic, "gnt:Link", network["link"])
757 def SaveVirtualSystemData(self, name, vcpus, memory):
758 """Convert virtual system information to OVF sections.
761 @param name: name of the instance
763 @param vcpus: number of VCPUs
765 @param memory: RAM memory in MB
770 vs_attrib = {"ovf:id": name}
771 virtual_system = ET.SubElement(self.tree, "VirtualSystem", attrib=vs_attrib)
772 SubElementText(virtual_system, "Info", "A virtual machine")
774 name_section = ET.SubElement(virtual_system, "Name")
775 name_section.text = name
776 os_attrib = {"ovf:id": "0"}
777 os_section = ET.SubElement(virtual_system, "OperatingSystemSection",
779 SubElementText(os_section, "Info", "Installed guest operating system")
780 hardware_section = ET.SubElement(virtual_system, "VirtualHardwareSection")
781 SubElementText(hardware_section, "Info", "Virtual hardware requirements")
784 system = ET.SubElement(hardware_section, "System")
785 SubElementText(system, "vssd:ElementName", "Virtual Hardware Family")
786 SubElementText(system, "vssd:InstanceID", INSTANCE_ID["system"])
787 SubElementText(system, "vssd:VirtualSystemIdentifier", name)
788 SubElementText(system, "vssd:VirtualSystemType", self.virtual_system_type)
791 vcpus_item = ET.SubElement(hardware_section, "Item")
792 SubElementText(vcpus_item, "rasd:ElementName",
793 "%s virtual CPU(s)" % vcpus)
794 SubElementText(vcpus_item, "rasd:InstanceID", INSTANCE_ID["vcpus"])
795 SubElementText(vcpus_item, "rasd:ResourceType", RASD_TYPE["vcpus"])
796 SubElementText(vcpus_item, "rasd:VirtualQuantity", vcpus)
799 memory_item = ET.SubElement(hardware_section, "Item")
800 SubElementText(memory_item, "rasd:AllocationUnits", "byte * 2^20")
801 SubElementText(memory_item, "rasd:ElementName", "%sMB of memory" % memory)
802 SubElementText(memory_item, "rasd:InstanceID", INSTANCE_ID["memory"])
803 SubElementText(memory_item, "rasd:ResourceType", RASD_TYPE["memory"])
804 SubElementText(memory_item, "rasd:VirtualQuantity", memory)
806 # Item for scsi controller
807 scsi_item = ET.SubElement(hardware_section, "Item")
808 SubElementText(scsi_item, "rasd:Address", INSTANCE_ID["system"])
809 SubElementText(scsi_item, "rasd:ElementName", "scsi_controller0")
810 SubElementText(scsi_item, "rasd:InstanceID", INSTANCE_ID["scsi"])
811 SubElementText(scsi_item, "rasd:ResourceSubType", SCSI_SUBTYPE)
812 SubElementText(scsi_item, "rasd:ResourceType", RASD_TYPE["scsi-controller"])
814 # Other items - from self.hardware_list
815 for item in self.hardware_list:
816 hardware_section.append(item)
818 def PrettyXmlDump(self):
819 """Formatter of the XML file.
822 @return: XML tree in the form of nicely-formatted string
825 raw_string = ET.tostring(self.tree)
826 parsed_xml = xml.dom.minidom.parseString(raw_string)
827 xml_string = parsed_xml.toprettyxml(indent=" ")
828 text_re = re.compile(">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL)
829 return text_re.sub(">\g<1></", xml_string)
832 class Converter(object):
833 """Converter class for OVF packages.
835 Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is
836 to provide a common interface for the two.
838 @type options: optparse.Values
839 @ivar options: options parsed from the command line
840 @type output_dir: string
841 @ivar output_dir: directory to which the results of conversion shall be
843 @type temp_file_manager: L{utils.TemporaryFileManager}
844 @ivar temp_file_manager: container for temporary files created during
846 @type temp_dir: string
847 @ivar temp_dir: temporary directory created then we deal with OVA
850 def __init__(self, input_path, options):
851 """Initialize the converter.
853 @type input_path: string
854 @param input_path: path to the Converter input file
855 @type options: optparse.Values
856 @param options: command line options
858 @raise errors.OpPrereqError: if file does not exist
861 input_path = os.path.abspath(input_path)
862 if not os.path.isfile(input_path):
863 raise errors.OpPrereqError("File does not exist: %s" % input_path)
864 self.options = options
865 self.temp_file_manager = utils.TemporaryFileManager()
867 self.output_dir = None
868 self._ReadInputData(input_path)
870 def _ReadInputData(self, input_path):
871 """Reads the data on which the conversion will take place.
873 @type input_path: string
874 @param input_path: absolute path to the Converter input file
877 raise NotImplementedError()
879 def _CompressDisk(self, disk_path, compression, action):
880 """Performs (de)compression on the disk and returns the new path
882 @type disk_path: string
883 @param disk_path: path to the disk
884 @type compression: string
885 @param compression: compression type
887 @param action: whether the action is compression or decompression
889 @return: new disk path after (de)compression
891 @raise errors.OpPrereqError: disk (de)compression failed or "compression"
895 assert(action in ALLOWED_ACTIONS)
896 # For now we only support gzip, as it is used in ovftool
897 if compression != COMPRESSION_TYPE:
898 raise errors.OpPrereqError("Unsupported compression type: %s"
900 disk_file = os.path.basename(disk_path)
901 if action == DECOMPRESS:
902 (disk_name, _) = os.path.splitext(disk_file)
904 elif action == COMPRESS:
906 new_path = utils.GetClosedTempfile(suffix=COMPRESSION_EXT, prefix=prefix,
908 self.temp_file_manager.Add(new_path)
909 args = ["gzip", "-c", disk_path]
910 run_result = utils.RunCmd(args, output=new_path)
911 if run_result.failed:
912 raise errors.OpPrereqError("Disk %s failed with output: %s"
913 % (action, run_result.stderr))
914 logging.info("The %s of the disk is completed", action)
915 return (COMPRESSION_EXT, new_path)
917 def _ConvertDisk(self, disk_format, disk_path):
918 """Performes conversion to specified format.
920 @type disk_format: string
921 @param disk_format: format to which the disk should be converted
922 @type disk_path: string
923 @param disk_path: path to the disk that should be converted
925 @return path to the output disk
927 @raise errors.OpPrereqError: convertion of the disk failed
931 disk_file = os.path.basename(disk_path)
932 (disk_name, disk_extension) = os.path.splitext(disk_file)
933 if disk_extension != disk_format:
934 logging.warning("Conversion of disk image to %s format, this may take"
935 " a while", disk_format)
937 new_disk_path = utils.GetClosedTempfile(suffix=".%s" % disk_format,
938 prefix=disk_name, dir=self.output_dir)
939 self.temp_file_manager.Add(new_disk_path)
941 constants.QEMUIMG_PATH,
948 run_result = utils.RunCmd(args, cwd=os.getcwd())
949 if run_result.failed:
950 raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was"
951 ": %s" % (disk_format, run_result.stderr))
952 return (".%s" % disk_format, new_disk_path)
955 def _GetDiskQemuInfo(disk_path, regexp):
956 """Figures out some information of the disk using qemu-img.
958 @type disk_path: string
959 @param disk_path: path to the disk we want to know the format of
961 @param regexp: string that has to be matched, it has to contain one group
965 @raise errors.OpPrereqError: format information cannot be retrieved
969 args = [constants.QEMUIMG_PATH, "info", disk_path]
970 run_result = utils.RunCmd(args, cwd=os.getcwd())
971 if run_result.failed:
972 raise errors.OpPrereqError("Gathering info about the disk using qemu-img"
973 " failed, output was: %s" % run_result.stderr)
974 result = run_result.output
975 regexp = r"%s" % regexp
976 match = re.search(regexp, result)
978 disk_format = match.group(1)
980 raise errors.OpPrereqError("No file information matching %s found in:"
981 " %s" % (regexp, result))
985 """Parses the data and creates a structure containing all required info.
988 raise NotImplementedError()
991 """Saves the gathered configuration in an apropriate format.
994 raise NotImplementedError()
997 """Cleans the temporary directory, if one was created.
1000 self.temp_file_manager.Cleanup()
1002 shutil.rmtree(self.temp_dir)
1003 self.temp_dir = None
1006 class OVFImporter(Converter):
1007 """Converter from OVF to Ganeti config file.
1009 @type input_dir: string
1010 @ivar input_dir: directory in which the .ovf file resides
1011 @type output_dir: string
1012 @ivar output_dir: directory to which the results of conversion shall be
1014 @type input_path: string
1015 @ivar input_path: complete path to the .ovf file
1016 @type ovf_reader: L{OVFReader}
1017 @ivar ovf_reader: OVF reader instance collects data from .ovf file
1018 @type results_name: string
1019 @ivar results_name: name of imported instance
1020 @type results_template: string
1021 @ivar results_template: disk template read from .ovf file or command line
1023 @type results_hypervisor: dict
1024 @ivar results_hypervisor: hypervisor information gathered from .ovf file or
1025 command line arguments
1026 @type results_os: dict
1027 @ivar results_os: operating system information gathered from .ovf file or
1028 command line arguments
1029 @type results_backend: dict
1030 @ivar results_backend: backend information gathered from .ovf file or
1031 command line arguments
1032 @type results_tags: string
1033 @ivar results_tags: string containing instance-specific tags
1034 @type results_version: string
1035 @ivar results_version: version as required by Ganeti import
1036 @type results_network: dict
1037 @ivar results_network: network information gathered from .ovf file or command
1039 @type results_disk: dict
1040 @ivar results_disk: disk information gathered from .ovf file or command line
1044 def _ReadInputData(self, input_path):
1045 """Reads the data on which the conversion will take place.
1047 @type input_path: string
1048 @param input_path: absolute path to the .ovf or .ova input file
1050 @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
1053 (input_dir, input_file) = os.path.split(input_path)
1054 (_, input_extension) = os.path.splitext(input_file)
1056 if input_extension == OVF_EXT:
1057 logging.info("%s file extension found, no unpacking necessary", OVF_EXT)
1058 self.input_dir = input_dir
1059 self.input_path = input_path
1060 self.temp_dir = None
1061 elif input_extension == OVA_EXT:
1062 logging.info("%s file extension found, proceeding to unpacking", OVA_EXT)
1063 self._UnpackOVA(input_path)
1065 raise errors.OpPrereqError("Unknown file extension; expected %s or %s"
1066 " file" % (OVA_EXT, OVF_EXT))
1067 assert ((input_extension == OVA_EXT and self.temp_dir) or
1068 (input_extension == OVF_EXT and not self.temp_dir))
1069 assert self.input_dir in self.input_path
1071 if self.options.output_dir:
1072 self.output_dir = os.path.abspath(self.options.output_dir)
1073 if (os.path.commonprefix([constants.EXPORT_DIR, self.output_dir]) !=
1074 constants.EXPORT_DIR):
1075 logging.warning("Export path is not under %s directory, import to"
1076 " Ganeti using gnt-backup may fail",
1077 constants.EXPORT_DIR)
1079 self.output_dir = constants.EXPORT_DIR
1081 self.ovf_reader = OVFReader(self.input_path)
1082 self.ovf_reader.VerifyManifest()
1084 def _UnpackOVA(self, input_path):
1085 """Unpacks the .ova package into temporary directory.
1087 @type input_path: string
1088 @param input_path: path to the .ova package file
1090 @raise errors.OpPrereqError: if file is not a proper tarball, one of the
1091 files in the archive seem malicious (e.g. path starts with '../') or
1092 .ova package does not contain .ovf file
1096 if not tarfile.is_tarfile(input_path):
1097 raise errors.OpPrereqError("The provided %s file is not a proper tar"
1098 " archive", OVA_EXT)
1099 ova_content = tarfile.open(input_path)
1100 temp_dir = tempfile.mkdtemp()
1101 self.temp_dir = temp_dir
1102 for file_name in ova_content.getnames():
1103 file_normname = os.path.normpath(file_name)
1105 utils.PathJoin(temp_dir, file_normname)
1106 except ValueError, err:
1107 raise errors.OpPrereqError("File %s inside %s package is not safe" %
1108 (file_name, OVA_EXT))
1109 if file_name.endswith(OVF_EXT):
1110 input_name = file_name
1112 raise errors.OpPrereqError("No %s file in %s package found" %
1114 logging.warning("Unpacking the %s archive, this may take a while",
1116 self.input_dir = temp_dir
1117 self.input_path = utils.PathJoin(self.temp_dir, input_name)
1120 extract = ova_content.extractall
1121 except AttributeError:
1122 # This is a prehistorical case of using python < 2.5
1123 for member in ova_content.getmembers():
1124 ova_content.extract(member, path=self.temp_dir)
1126 extract(self.temp_dir)
1127 except tarfile.TarError, err:
1128 raise errors.OpPrereqError("Error while extracting %s archive: %s" %
1130 logging.info("OVA package extracted to %s directory", self.temp_dir)
1133 """Parses the data and creates a structure containing all required info.
1135 The method reads the information given either as a command line option or as
1136 a part of the OVF description.
1138 @raise errors.OpPrereqError: if some required part of the description of
1139 virtual instance is missing or unable to create output directory
1142 self.results_name = self._GetInfo("instance name", self.options.name,
1143 self._ParseNameOptions, self.ovf_reader.GetInstanceName)
1144 if not self.results_name:
1145 raise errors.OpPrereqError("Name of instance not provided")
1147 self.output_dir = utils.PathJoin(self.output_dir, self.results_name)
1149 utils.Makedirs(self.output_dir)
1150 except OSError, err:
1151 raise errors.OpPrereqError("Failed to create directory %s: %s" %
1152 (self.output_dir, err))
1154 self.results_template = self._GetInfo("disk template",
1155 self.options.disk_template, self._ParseTemplateOptions,
1156 self.ovf_reader.GetDiskTemplate)
1157 if not self.results_template:
1158 logging.info("Disk template not given")
1160 self.results_hypervisor = self._GetInfo("hypervisor",
1161 self.options.hypervisor, self._ParseHypervisorOptions,
1162 self.ovf_reader.GetHypervisorData)
1163 assert self.results_hypervisor["hypervisor_name"]
1164 if self.results_hypervisor["hypervisor_name"] == constants.VALUE_AUTO:
1165 logging.debug("Default hypervisor settings from the cluster will be used")
1167 self.results_os = self._GetInfo("OS", self.options.os,
1168 self._ParseOSOptions, self.ovf_reader.GetOSData)
1169 if not self.results_os.get("os_name"):
1170 raise errors.OpPrereqError("OS name must be provided")
1172 self.results_backend = self._GetInfo("backend", self.options.beparams,
1173 self._ParseBackendOptions, self.ovf_reader.GetBackendData)
1174 assert self.results_backend.get("vcpus")
1175 assert self.results_backend.get("memory")
1176 assert self.results_backend.get("auto_balance") is not None
1178 self.results_tags = self._GetInfo("tags", self.options.tags,
1179 self._ParseTags, self.ovf_reader.GetTagsData)
1181 ovf_version = self.ovf_reader.GetVersionData()
1183 self.results_version = ovf_version
1185 self.results_version = constants.EXPORT_VERSION
1187 self.results_network = self._GetInfo("network", self.options.nics,
1188 self._ParseNicOptions, self.ovf_reader.GetNetworkData,
1189 ignore_test=self.options.no_nics)
1191 self.results_disk = self._GetInfo("disk", self.options.disks,
1192 self._ParseDiskOptions, self._GetDiskInfo,
1193 ignore_test=self.results_template == constants.DT_DISKLESS)
1195 if not self.results_disk and not self.results_network:
1196 raise errors.OpPrereqError("Either disk specification or network"
1197 " description must be present")
1200 def _GetInfo(name, cmd_arg, cmd_function, nocmd_function,
1202 """Get information about some section - e.g. disk, network, hypervisor.
1205 @param name: name of the section
1207 @param cmd_arg: command line argument specific for section 'name'
1208 @type cmd_function: callable
1209 @param cmd_function: function to call if 'cmd_args' exists
1210 @type nocmd_function: callable
1211 @param nocmd_function: function to call if 'cmd_args' is not there
1215 logging.info("Information for %s will be ignored", name)
1218 logging.info("Information for %s will be parsed from command line", name)
1219 results = cmd_function()
1221 logging.info("Information for %s will be parsed from %s file",
1223 results = nocmd_function()
1224 logging.info("Options for %s were succesfully read", name)
1227 def _ParseNameOptions(self):
1228 """Returns name if one was given in command line.
1231 @return: name of an instance
1234 return self.options.name
1236 def _ParseTemplateOptions(self):
1237 """Returns disk template if one was given in command line.
1240 @return: disk template name
1243 return self.options.disk_template
1245 def _ParseHypervisorOptions(self):
1246 """Parses hypervisor options given in a command line.
1249 @return: dictionary containing name of the chosen hypervisor and all the
1253 assert type(self.options.hypervisor) is tuple
1254 assert len(self.options.hypervisor) == 2
1256 if self.options.hypervisor[0]:
1257 results["hypervisor_name"] = self.options.hypervisor[0]
1259 results["hypervisor_name"] = constants.VALUE_AUTO
1260 results.update(self.options.hypervisor[1])
1263 def _ParseOSOptions(self):
1264 """Parses OS options given in command line.
1267 @return: dictionary containing name of chosen OS and all its options
1270 assert self.options.os
1272 results["os_name"] = self.options.os
1273 results.update(self.options.osparams)
1276 def _ParseBackendOptions(self):
1277 """Parses backend options given in command line.
1280 @return: dictionary containing vcpus, memory and auto-balance options
1283 assert self.options.beparams
1285 backend.update(self.options.beparams)
1286 must_contain = ["vcpus", "memory", "auto_balance"]
1287 for element in must_contain:
1288 if backend.get(element) is None:
1289 backend[element] = constants.VALUE_AUTO
1292 def _ParseTags(self):
1293 """Returns tags list given in command line.
1296 @return: string containing comma-separated tags
1299 return self.options.tags
1301 def _ParseNicOptions(self):
1302 """Parses network options given in a command line or as a dictionary.
1305 @return: dictionary of network-related options
1308 assert self.options.nics
1310 for (nic_id, nic_desc) in self.options.nics:
1311 results["nic%s_mode" % nic_id] = \
1312 nic_desc.get("mode", constants.VALUE_AUTO)
1313 results["nic%s_mac" % nic_id] = nic_desc.get("mac", constants.VALUE_AUTO)
1314 results["nic%s_link" % nic_id] = \
1315 nic_desc.get("link", constants.VALUE_AUTO)
1316 if nic_desc.get("mode") == "bridged":
1317 results["nic%s_ip" % nic_id] = constants.VALUE_NONE
1319 results["nic%s_ip" % nic_id] = constants.VALUE_AUTO
1320 results["nic_count"] = str(len(self.options.nics))
1323 def _ParseDiskOptions(self):
1324 """Parses disk options given in a command line.
1327 @return: dictionary of disk-related options
1329 @raise errors.OpPrereqError: disk description does not contain size
1330 information or size information is invalid or creation failed
1334 assert self.options.disks
1336 for (disk_id, disk_desc) in self.options.disks:
1337 results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id
1338 if disk_desc.get("size"):
1340 disk_size = utils.ParseUnit(disk_desc["size"])
1342 raise errors.OpPrereqError("Invalid disk size for disk %s: %s" %
1343 (disk_id, disk_desc["size"]))
1344 new_path = utils.PathJoin(self.output_dir, str(disk_id))
1346 constants.QEMUIMG_PATH,
1353 run_result = utils.RunCmd(args)
1354 if run_result.failed:
1355 raise errors.OpPrereqError("Creation of disk %s failed, output was:"
1356 " %s" % (new_path, run_result.stderr))
1357 results["disk%s_size" % disk_id] = str(disk_size)
1358 results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id
1360 raise errors.OpPrereqError("Disks created for import must have their"
1362 results["disk_count"] = str(len(self.options.disks))
1365 def _GetDiskInfo(self):
1366 """Gathers information about disks used by instance, perfomes conversion.
1369 @return: dictionary of disk-related options
1371 @raise errors.OpPrereqError: disk is not in the same directory as .ovf file
1375 disks_list = self.ovf_reader.GetDisksNames()
1376 for (counter, (disk_name, disk_compression)) in enumerate(disks_list):
1377 if os.path.dirname(disk_name):
1378 raise errors.OpPrereqError("Disks are not allowed to have absolute"
1379 " paths or paths outside main OVF directory")
1380 disk, _ = os.path.splitext(disk_name)
1381 disk_path = utils.PathJoin(self.input_dir, disk_name)
1382 if disk_compression not in NO_COMPRESSION:
1383 _, disk_path = self._CompressDisk(disk_path, disk_compression,
1385 disk, _ = os.path.splitext(disk)
1386 if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw":
1387 logging.info("Conversion to raw format is required")
1388 ext, new_disk_path = self._ConvertDisk("raw", disk_path)
1390 final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext,
1391 directory=self.output_dir)
1392 final_name = os.path.basename(final_disk_path)
1393 disk_size = os.path.getsize(final_disk_path) / (1024 * 1024)
1394 results["disk%s_dump" % counter] = final_name
1395 results["disk%s_size" % counter] = str(disk_size)
1396 results["disk%s_ivname" % counter] = "disk/%s" % str(counter)
1398 results["disk_count"] = str(len(disks_list))
1402 """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
1404 @raise errors.OpPrereqError: when saving to config file failed
1407 logging.info("Conversion was succesfull, saving %s in %s directory",
1408 constants.EXPORT_CONF_FILE, self.output_dir)
1410 constants.INISECT_INS: {},
1411 constants.INISECT_BEP: {},
1412 constants.INISECT_EXP: {},
1413 constants.INISECT_OSP: {},
1414 constants.INISECT_HYP: {},
1417 results[constants.INISECT_INS].update(self.results_disk)
1418 results[constants.INISECT_INS].update(self.results_network)
1419 results[constants.INISECT_INS]["hypervisor"] = \
1420 self.results_hypervisor["hypervisor_name"]
1421 results[constants.INISECT_INS]["name"] = self.results_name
1422 if self.results_template:
1423 results[constants.INISECT_INS]["disk_template"] = self.results_template
1424 if self.results_tags:
1425 results[constants.INISECT_INS]["tags"] = self.results_tags
1427 results[constants.INISECT_BEP].update(self.results_backend)
1429 results[constants.INISECT_EXP]["os"] = self.results_os["os_name"]
1430 results[constants.INISECT_EXP]["version"] = self.results_version
1432 del self.results_os["os_name"]
1433 results[constants.INISECT_OSP].update(self.results_os)
1435 del self.results_hypervisor["hypervisor_name"]
1436 results[constants.INISECT_HYP].update(self.results_hypervisor)
1438 output_file_name = utils.PathJoin(self.output_dir,
1439 constants.EXPORT_CONF_FILE)
1442 for section, options in results.iteritems():
1443 output.append("[%s]" % section)
1444 for name, value in options.iteritems():
1447 output.append("%s = %s" % (name, value))
1449 output_contents = "\n".join(output)
1452 utils.WriteFile(output_file_name, data=output_contents)
1453 except errors.ProgrammerError, err:
1454 raise errors.OpPrereqError("Saving the config file failed: %s" % err)
1459 class ConfigParserWithDefaults(ConfigParser.SafeConfigParser):
1460 """This is just a wrapper on SafeConfigParser, that uses default values
1463 def get(self, section, options, raw=None, vars=None): # pylint: disable=W0622
1465 result = ConfigParser.SafeConfigParser.get(self, section, options, \
1467 except ConfigParser.NoOptionError:
1471 def getint(self, section, options):
1473 result = ConfigParser.SafeConfigParser.get(self, section, options)
1474 except ConfigParser.NoOptionError:
1479 class OVFExporter(Converter):
1480 """Converter from Ganeti config file to OVF
1482 @type input_dir: string
1483 @ivar input_dir: directory in which the config.ini file resides
1484 @type output_dir: string
1485 @ivar output_dir: directory to which the results of conversion shall be
1487 @type packed_dir: string
1488 @ivar packed_dir: if we want OVA package, this points to the real (i.e. not
1489 temp) output directory
1490 @type input_path: string
1491 @ivar input_path: complete path to the config.ini file
1492 @type output_path: string
1493 @ivar output_path: complete path to .ovf file
1494 @type config_parser: L{ConfigParserWithDefaults}
1495 @ivar config_parser: parser for the config.ini file
1496 @type reference_files: list
1497 @ivar reference_files: files referenced in the ovf file
1498 @type results_disk: list
1499 @ivar results_disk: list of dictionaries of disk options from config.ini
1500 @type results_network: list
1501 @ivar results_network: list of dictionaries of network options form config.ini
1502 @type results_name: string
1503 @ivar results_name: name of the instance
1504 @type results_vcpus: string
1505 @ivar results_vcpus: number of VCPUs
1506 @type results_memory: string
1507 @ivar results_memory: RAM memory in MB
1508 @type results_ganeti: dict
1509 @ivar results_ganeti: dictionary of Ganeti-specific options from config.ini
1512 def _ReadInputData(self, input_path):
1513 """Reads the data on which the conversion will take place.
1515 @type input_path: string
1516 @param input_path: absolute path to the config.ini input file
1518 @raise errors.OpPrereqError: error when reading the config file
1521 input_dir = os.path.dirname(input_path)
1522 self.input_path = input_path
1523 self.input_dir = input_dir
1524 if self.options.output_dir:
1525 self.output_dir = os.path.abspath(self.options.output_dir)
1527 self.output_dir = input_dir
1528 self.config_parser = ConfigParserWithDefaults()
1529 logging.info("Reading configuration from %s file", input_path)
1531 self.config_parser.read(input_path)
1532 except ConfigParser.MissingSectionHeaderError, err:
1533 raise errors.OpPrereqError("Error when trying to read %s: %s" %
1535 if self.options.ova_package:
1536 self.temp_dir = tempfile.mkdtemp()
1537 self.packed_dir = self.output_dir
1538 self.output_dir = self.temp_dir
1540 self.ovf_writer = OVFWriter(not self.options.ext_usage)
1542 def _ParseName(self):
1543 """Parses name from command line options or config file.
1546 @return: name of Ganeti instance
1548 @raise errors.OpPrereqError: if name of the instance is not provided
1551 if self.options.name:
1552 name = self.options.name
1554 name = self.config_parser.get(constants.INISECT_INS, NAME)
1556 raise errors.OpPrereqError("No instance name found")
1559 def _ParseVCPUs(self):
1560 """Parses vcpus number from config file.
1563 @return: number of virtual CPUs
1565 @raise errors.OpPrereqError: if number of VCPUs equals 0
1568 vcpus = self.config_parser.getint(constants.INISECT_BEP, VCPUS)
1570 raise errors.OpPrereqError("No CPU information found")
1573 def _ParseMemory(self):
1574 """Parses vcpus number from config file.
1577 @return: amount of memory in MB
1579 @raise errors.OpPrereqError: if amount of memory equals 0
1582 memory = self.config_parser.getint(constants.INISECT_BEP, MEMORY)
1584 raise errors.OpPrereqError("No memory information found")
1587 def _ParseGaneti(self):
1588 """Parses Ganeti data from config file.
1591 @return: dictionary of Ganeti-specific options
1596 results["hypervisor"] = {}
1597 hyp_name = self.config_parser.get(constants.INISECT_INS, HYPERV)
1598 if hyp_name is None:
1599 raise errors.OpPrereqError("No hypervisor information found")
1600 results["hypervisor"]["name"] = hyp_name
1601 pairs = self.config_parser.items(constants.INISECT_HYP)
1602 for (name, value) in pairs:
1603 results["hypervisor"][name] = value
1606 os_name = self.config_parser.get(constants.INISECT_EXP, OS)
1608 raise errors.OpPrereqError("No operating system information found")
1609 results["os"]["name"] = os_name
1610 pairs = self.config_parser.items(constants.INISECT_OSP)
1611 for (name, value) in pairs:
1612 results["os"][name] = value
1615 (constants.INISECT_INS, DISK_TEMPLATE, "disk_template"),
1616 (constants.INISECT_BEP, AUTO_BALANCE, "auto_balance"),
1617 (constants.INISECT_INS, TAGS, "tags"),
1618 (constants.INISECT_EXP, VERSION, "version"),
1620 for (section, element, name) in others:
1621 results[name] = self.config_parser.get(section, element)
1624 def _ParseNetworks(self):
1625 """Parses network data from config file.
1628 @return: list of dictionaries of network options
1630 @raise errors.OpPrereqError: then network mode is not recognized
1637 self.config_parser.get(constants.INISECT_INS, "nic%s_link" % counter)
1638 if data_link is None:
1641 "mode": self.config_parser.get(constants.INISECT_INS,
1642 "nic%s_mode" % counter),
1643 "mac": self.config_parser.get(constants.INISECT_INS,
1644 "nic%s_mac" % counter),
1645 "ip": self.config_parser.get(constants.INISECT_INS,
1646 "nic%s_ip" % counter),
1649 if results[counter]["mode"] not in constants.NIC_VALID_MODES:
1650 raise errors.OpPrereqError("Network mode %s not recognized"
1651 % results[counter]["mode"])
1655 def _GetDiskOptions(self, disk_file, compression):
1656 """Convert the disk and gather disk info for .ovf file.
1658 @type disk_file: string
1659 @param disk_file: name of the disk (without the full path)
1660 @type compression: bool
1661 @param compression: whether the disk should be compressed or not
1663 @raise errors.OpPrereqError: when disk image does not exist
1666 disk_path = utils.PathJoin(self.input_dir, disk_file)
1668 if not os.path.isfile(disk_path):
1669 raise errors.OpPrereqError("Disk image does not exist: %s" % disk_path)
1670 if os.path.dirname(disk_file):
1671 raise errors.OpPrereqError("Path for the disk: %s contains a directory"
1672 " name" % disk_path)
1673 disk_name, _ = os.path.splitext(disk_file)
1674 ext, new_disk_path = self._ConvertDisk(self.options.disk_format, disk_path)
1675 results["format"] = self.options.disk_format
1676 results["virt-size"] = self._GetDiskQemuInfo(new_disk_path,
1677 "virtual size: \S+ \((\d+) bytes\)")
1679 ext2, new_disk_path = self._CompressDisk(new_disk_path, "gzip",
1681 disk_name, _ = os.path.splitext(disk_name)
1682 results["compression"] = "gzip"
1684 final_disk_path = LinkFile(new_disk_path, prefix=disk_name, suffix=ext,
1685 directory=self.output_dir)
1686 final_disk_name = os.path.basename(final_disk_path)
1687 results["real-size"] = os.path.getsize(final_disk_path)
1688 results["path"] = final_disk_name
1689 self.references_files.append(final_disk_path)
1692 def _ParseDisks(self):
1693 """Parses disk data from config file.
1696 @return: list of dictionaries of disk options
1703 self.config_parser.get(constants.INISECT_INS, "disk%s_dump" % counter)
1704 if disk_file is None:
1706 results.append(self._GetDiskOptions(disk_file, self.options.compression))
1711 """Parses the data and creates a structure containing all required info.
1715 utils.Makedirs(self.output_dir)
1716 except OSError, err:
1717 raise errors.OpPrereqError("Failed to create directory %s: %s" %
1718 (self.output_dir, err))
1720 self.references_files = []
1721 self.results_name = self._ParseName()
1722 self.results_vcpus = self._ParseVCPUs()
1723 self.results_memory = self._ParseMemory()
1724 if not self.options.ext_usage:
1725 self.results_ganeti = self._ParseGaneti()
1726 self.results_network = self._ParseNetworks()
1727 self.results_disk = self._ParseDisks()
1729 def _PrepareManifest(self, path):
1730 """Creates manifest for all the files in OVF package.
1733 @param path: path to manifesto file
1735 @raise errors.OpPrereqError: if error occurs when writing file
1738 logging.info("Preparing manifest for the OVF package")
1740 files_list = [self.output_path]
1741 files_list.extend(self.references_files)
1742 logging.warning("Calculating SHA1 checksums, this may take a while")
1743 sha1_sums = utils.FingerprintFiles(files_list)
1744 for file_path, value in sha1_sums.iteritems():
1745 file_name = os.path.basename(file_path)
1746 lines.append("SHA1(%s)= %s" % (file_name, value))
1748 data = "\n".join(lines)
1750 utils.WriteFile(path, data=data)
1751 except errors.ProgrammerError, err:
1752 raise errors.OpPrereqError("Saving the manifest file failed: %s" % err)
1755 def _PrepareTarFile(tar_path, files_list):
1756 """Creates tarfile from the files in OVF package.
1758 @type tar_path: string
1759 @param tar_path: path to the resulting file
1760 @type files_list: list
1761 @param files_list: list of files in the OVF package
1764 logging.info("Preparing tarball for the OVF package")
1765 open(tar_path, mode="w").close()
1766 ova_package = tarfile.open(name=tar_path, mode="w")
1767 for file_path in files_list:
1768 file_name = os.path.basename(file_path)
1769 ova_package.add(file_path, arcname=file_name)
1773 """Saves the gathered configuration in an apropriate format.
1775 @raise errors.OpPrereqError: if unable to create output directory
1778 output_file = "%s%s" % (self.results_name, OVF_EXT)
1779 output_path = utils.PathJoin(self.output_dir, output_file)
1780 self.ovf_writer = OVFWriter(not self.options.ext_usage)
1781 logging.info("Saving read data to %s", output_path)
1783 self.output_path = utils.PathJoin(self.output_dir, output_file)
1784 files_list = [self.output_path]
1786 self.ovf_writer.SaveDisksData(self.results_disk)
1787 self.ovf_writer.SaveNetworksData(self.results_network)
1788 if not self.options.ext_usage:
1789 self.ovf_writer.SaveGanetiData(self.results_ganeti, self.results_network)
1791 self.ovf_writer.SaveVirtualSystemData(self.results_name, self.results_vcpus,
1792 self.results_memory)
1794 data = self.ovf_writer.PrettyXmlDump()
1795 utils.WriteFile(self.output_path, data=data)
1797 manifest_file = "%s%s" % (self.results_name, MF_EXT)
1798 manifest_path = utils.PathJoin(self.output_dir, manifest_file)
1799 self._PrepareManifest(manifest_path)
1800 files_list.append(manifest_path)
1802 files_list.extend(self.references_files)
1804 if self.options.ova_package:
1805 ova_file = "%s%s" % (self.results_name, OVA_EXT)
1806 packed_path = utils.PathJoin(self.packed_dir, ova_file)
1808 utils.Makedirs(self.packed_dir)
1809 except OSError, err:
1810 raise errors.OpPrereqError("Failed to create directory %s: %s" %
1811 (self.packed_dir, err))
1812 self._PrepareTarFile(packed_path, files_list)
1813 logging.info("Creation of the OVF package was successfull")