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", "network"]
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" %
536 ganeti_data["network"] = network_ganeti_data.findtext("{%s}Network" %
540 mac_data = network_data.findtext("{%s}Address" % RASD_SCHEMA)
542 network_name = network_name.lower()
544 # First, some not Ganeti-specific information is collected
545 if constants.NIC_MODE_BRIDGED in network_name:
546 results["nic%s_mode" % counter] = "bridged"
547 elif constants.NIC_MODE_ROUTED in network_name:
548 results["nic%s_mode" % counter] = "routed"
549 results["nic%s_mac" % counter] = mac_data
551 # GanetiSection data overrides 'manually' collected data
552 for name, value in ganeti_data.iteritems():
553 results["nic%s_%s" % (counter, name)] = value
555 # Bridged network has no IP - unless specifically stated otherwise
556 if (results.get("nic%s_mode" % counter) == "bridged" and
557 not results.get("nic%s_ip" % counter)):
558 results["nic%s_ip" % counter] = constants.VALUE_NONE
560 for option in required:
561 if not results.get("nic%s_%s" % (counter, option)):
562 results["nic%s_%s" % (counter, option)] = constants.VALUE_AUTO
565 results["nic_count"] = str(len(network_names))
568 def GetDisksNames(self):
569 """Provides list of file names for the disks used by the instance.
572 @return: list of file names, as referenced in .ovf file
576 disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA)
577 disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA)
578 for disk in disk_ids:
579 disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA)
580 disk_match = ("{%s}id" % OVF_SCHEMA, disk)
581 disk_elem = self._GetElementMatchingAttr(disk_search, disk_match)
582 if disk_elem is None:
583 raise errors.OpPrereqError("%s file corrupted - disk %s not found in"
584 " references" % (OVF_EXT, disk))
585 disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA)
586 disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA)
587 results.append((disk_name, disk_compression))
591 def SubElementText(parent, tag, text, attrib={}, **extra):
592 # pylint: disable=W0102
593 """This is just a wrapper on ET.SubElement that always has text content.
598 elem = ET.SubElement(parent, tag, attrib=attrib, **extra)
599 elem.text = str(text)
603 class OVFWriter(object):
604 """Writer class for OVF files.
606 @type tree: ET.ElementTree
607 @ivar tree: XML tree that we are constructing
608 @type virtual_system_type: string
609 @ivar virtual_system_type: value of vssd:VirtualSystemType, for external usage
610 in VMWare this requires to be vmx
611 @type hardware_list: list
612 @ivar hardware_list: list of items prepared for VirtualHardwareSection
613 @type next_instance_id: int
614 @ivar next_instance_id: next instance id to be used when creating elements on
618 def __init__(self, has_gnt_section):
619 """Initialize the writer - set the top element.
621 @type has_gnt_section: bool
622 @param has_gnt_section: if the Ganeti schema should be added - i.e. this
623 means that Ganeti section will be present
627 "xmlns:xsi": XML_SCHEMA,
628 "xmlns:vssd": VSSD_SCHEMA,
629 "xmlns:rasd": RASD_SCHEMA,
630 "xmlns:ovf": OVF_SCHEMA,
635 env_attribs["xmlns:gnt"] = GANETI_SCHEMA
636 self.virtual_system_type = VS_TYPE["ganeti"]
638 self.virtual_system_type = VS_TYPE["external"]
639 self.tree = ET.Element("Envelope", attrib=env_attribs)
640 self.hardware_list = []
641 # INSTANCE_ID contains statically assigned IDs, starting from 0
642 self.next_instance_id = len(INSTANCE_ID) # FIXME: hackish
644 def SaveDisksData(self, disks):
645 """Convert disk information to certain OVF sections.
648 @param disks: list of dictionaries of disk options from config.ini
651 references = ET.SubElement(self.tree, "References")
652 disk_section = ET.SubElement(self.tree, "DiskSection")
653 SubElementText(disk_section, "Info", "Virtual disk information")
654 for counter, disk in enumerate(disks):
655 file_id = "file%s" % counter
656 disk_id = "disk%s" % counter
658 "ovf:href": disk["path"],
659 "ovf:size": str(disk["real-size"]),
663 "ovf:capacity": str(disk["virt-size"]),
664 "ovf:diskId": disk_id,
665 "ovf:fileRef": file_id,
666 "ovf:format": DISK_FORMAT.get(disk["format"], disk["format"]),
668 if "compression" in disk:
669 file_attribs["ovf:compression"] = disk["compression"]
670 ET.SubElement(references, "File", attrib=file_attribs)
671 ET.SubElement(disk_section, "Disk", attrib=disk_attribs)
673 # Item in VirtualHardwareSection creation
674 disk_item = ET.Element("Item")
675 SubElementText(disk_item, "rasd:ElementName", disk_id)
676 SubElementText(disk_item, "rasd:HostResource", "ovf:/disk/%s" % disk_id)
677 SubElementText(disk_item, "rasd:InstanceID", self.next_instance_id)
678 SubElementText(disk_item, "rasd:Parent", INSTANCE_ID["scsi"])
679 SubElementText(disk_item, "rasd:ResourceType", RASD_TYPE["disk"])
680 self.hardware_list.append(disk_item)
681 self.next_instance_id += 1
683 def SaveNetworksData(self, networks):
684 """Convert network information to NetworkSection.
687 @param networks: list of dictionaries of network options form config.ini
690 network_section = ET.SubElement(self.tree, "NetworkSection")
691 SubElementText(network_section, "Info", "List of logical networks")
692 for counter, network in enumerate(networks):
693 network_name = "%s%s" % (network["mode"], counter)
694 network_attrib = {"ovf:name": network_name}
695 ET.SubElement(network_section, "Network", attrib=network_attrib)
697 # Item in VirtualHardwareSection creation
698 network_item = ET.Element("Item")
699 SubElementText(network_item, "rasd:Address", network["mac"])
700 SubElementText(network_item, "rasd:Connection", network_name)
701 SubElementText(network_item, "rasd:ElementName", network_name)
702 SubElementText(network_item, "rasd:InstanceID", self.next_instance_id)
703 SubElementText(network_item, "rasd:ResourceType",
704 RASD_TYPE["ethernet-adapter"])
705 self.hardware_list.append(network_item)
706 self.next_instance_id += 1
709 def _SaveNameAndParams(root, data):
710 """Save name and parameters information under root using data.
712 @type root: ET.Element
713 @param root: root element for the Name and Parameters
715 @param data: data from which we gather the values
718 assert(data.get("name"))
719 name = SubElementText(root, "gnt:Name", data["name"])
720 params = ET.SubElement(root, "gnt:Parameters")
721 for name, value in data.iteritems():
723 SubElementText(params, "gnt:%s" % name, value)
725 def SaveGanetiData(self, ganeti, networks):
726 """Convert Ganeti-specific information to GanetiSection.
729 @param ganeti: dictionary of Ganeti-specific options from config.ini
731 @param networks: list of dictionaries of network options form config.ini
734 ganeti_section = ET.SubElement(self.tree, "gnt:GanetiSection")
736 SubElementText(ganeti_section, "gnt:Version", ganeti.get("version"))
737 SubElementText(ganeti_section, "gnt:DiskTemplate",
738 ganeti.get("disk_template"))
739 SubElementText(ganeti_section, "gnt:AutoBalance",
740 ganeti.get("auto_balance"))
741 SubElementText(ganeti_section, "gnt:Tags", ganeti.get("tags"))
743 osys = ET.SubElement(ganeti_section, "gnt:OperatingSystem")
744 self._SaveNameAndParams(osys, ganeti["os"])
746 hypervisor = ET.SubElement(ganeti_section, "gnt:Hypervisor")
747 self._SaveNameAndParams(hypervisor, ganeti["hypervisor"])
749 network_section = ET.SubElement(ganeti_section, "gnt:Network")
750 for counter, network in enumerate(networks):
751 network_name = "%s%s" % (network["mode"], counter)
752 nic_attrib = {"ovf:name": network_name}
753 nic = ET.SubElement(network_section, "gnt:Nic", attrib=nic_attrib)
754 SubElementText(nic, "gnt:Mode", network["mode"])
755 SubElementText(nic, "gnt:MACAddress", network["mac"])
756 SubElementText(nic, "gnt:IPAddress", network["ip"])
757 SubElementText(nic, "gnt:Link", network["link"])
758 SubElementText(nic, "gnt:Net", network["network"])
760 def SaveVirtualSystemData(self, name, vcpus, memory):
761 """Convert virtual system information to OVF sections.
764 @param name: name of the instance
766 @param vcpus: number of VCPUs
768 @param memory: RAM memory in MB
773 vs_attrib = {"ovf:id": name}
774 virtual_system = ET.SubElement(self.tree, "VirtualSystem", attrib=vs_attrib)
775 SubElementText(virtual_system, "Info", "A virtual machine")
777 name_section = ET.SubElement(virtual_system, "Name")
778 name_section.text = name
779 os_attrib = {"ovf:id": "0"}
780 os_section = ET.SubElement(virtual_system, "OperatingSystemSection",
782 SubElementText(os_section, "Info", "Installed guest operating system")
783 hardware_section = ET.SubElement(virtual_system, "VirtualHardwareSection")
784 SubElementText(hardware_section, "Info", "Virtual hardware requirements")
787 system = ET.SubElement(hardware_section, "System")
788 SubElementText(system, "vssd:ElementName", "Virtual Hardware Family")
789 SubElementText(system, "vssd:InstanceID", INSTANCE_ID["system"])
790 SubElementText(system, "vssd:VirtualSystemIdentifier", name)
791 SubElementText(system, "vssd:VirtualSystemType", self.virtual_system_type)
794 vcpus_item = ET.SubElement(hardware_section, "Item")
795 SubElementText(vcpus_item, "rasd:ElementName",
796 "%s virtual CPU(s)" % vcpus)
797 SubElementText(vcpus_item, "rasd:InstanceID", INSTANCE_ID["vcpus"])
798 SubElementText(vcpus_item, "rasd:ResourceType", RASD_TYPE["vcpus"])
799 SubElementText(vcpus_item, "rasd:VirtualQuantity", vcpus)
802 memory_item = ET.SubElement(hardware_section, "Item")
803 SubElementText(memory_item, "rasd:AllocationUnits", "byte * 2^20")
804 SubElementText(memory_item, "rasd:ElementName", "%sMB of memory" % memory)
805 SubElementText(memory_item, "rasd:InstanceID", INSTANCE_ID["memory"])
806 SubElementText(memory_item, "rasd:ResourceType", RASD_TYPE["memory"])
807 SubElementText(memory_item, "rasd:VirtualQuantity", memory)
809 # Item for scsi controller
810 scsi_item = ET.SubElement(hardware_section, "Item")
811 SubElementText(scsi_item, "rasd:Address", INSTANCE_ID["system"])
812 SubElementText(scsi_item, "rasd:ElementName", "scsi_controller0")
813 SubElementText(scsi_item, "rasd:InstanceID", INSTANCE_ID["scsi"])
814 SubElementText(scsi_item, "rasd:ResourceSubType", SCSI_SUBTYPE)
815 SubElementText(scsi_item, "rasd:ResourceType", RASD_TYPE["scsi-controller"])
817 # Other items - from self.hardware_list
818 for item in self.hardware_list:
819 hardware_section.append(item)
821 def PrettyXmlDump(self):
822 """Formatter of the XML file.
825 @return: XML tree in the form of nicely-formatted string
828 raw_string = ET.tostring(self.tree)
829 parsed_xml = xml.dom.minidom.parseString(raw_string)
830 xml_string = parsed_xml.toprettyxml(indent=" ")
831 text_re = re.compile(">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL)
832 return text_re.sub(">\g<1></", xml_string)
835 class Converter(object):
836 """Converter class for OVF packages.
838 Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is
839 to provide a common interface for the two.
841 @type options: optparse.Values
842 @ivar options: options parsed from the command line
843 @type output_dir: string
844 @ivar output_dir: directory to which the results of conversion shall be
846 @type temp_file_manager: L{utils.TemporaryFileManager}
847 @ivar temp_file_manager: container for temporary files created during
849 @type temp_dir: string
850 @ivar temp_dir: temporary directory created then we deal with OVA
853 def __init__(self, input_path, options):
854 """Initialize the converter.
856 @type input_path: string
857 @param input_path: path to the Converter input file
858 @type options: optparse.Values
859 @param options: command line options
861 @raise errors.OpPrereqError: if file does not exist
864 input_path = os.path.abspath(input_path)
865 if not os.path.isfile(input_path):
866 raise errors.OpPrereqError("File does not exist: %s" % input_path)
867 self.options = options
868 self.temp_file_manager = utils.TemporaryFileManager()
870 self.output_dir = None
871 self._ReadInputData(input_path)
873 def _ReadInputData(self, input_path):
874 """Reads the data on which the conversion will take place.
876 @type input_path: string
877 @param input_path: absolute path to the Converter input file
880 raise NotImplementedError()
882 def _CompressDisk(self, disk_path, compression, action):
883 """Performs (de)compression on the disk and returns the new path
885 @type disk_path: string
886 @param disk_path: path to the disk
887 @type compression: string
888 @param compression: compression type
890 @param action: whether the action is compression or decompression
892 @return: new disk path after (de)compression
894 @raise errors.OpPrereqError: disk (de)compression failed or "compression"
898 assert(action in ALLOWED_ACTIONS)
899 # For now we only support gzip, as it is used in ovftool
900 if compression != COMPRESSION_TYPE:
901 raise errors.OpPrereqError("Unsupported compression type: %s"
903 disk_file = os.path.basename(disk_path)
904 if action == DECOMPRESS:
905 (disk_name, _) = os.path.splitext(disk_file)
907 elif action == COMPRESS:
909 new_path = utils.GetClosedTempfile(suffix=COMPRESSION_EXT, prefix=prefix,
911 self.temp_file_manager.Add(new_path)
912 args = ["gzip", "-c", disk_path]
913 run_result = utils.RunCmd(args, output=new_path)
914 if run_result.failed:
915 raise errors.OpPrereqError("Disk %s failed with output: %s"
916 % (action, run_result.stderr))
917 logging.info("The %s of the disk is completed", action)
918 return (COMPRESSION_EXT, new_path)
920 def _ConvertDisk(self, disk_format, disk_path):
921 """Performes conversion to specified format.
923 @type disk_format: string
924 @param disk_format: format to which the disk should be converted
925 @type disk_path: string
926 @param disk_path: path to the disk that should be converted
928 @return path to the output disk
930 @raise errors.OpPrereqError: convertion of the disk failed
934 disk_file = os.path.basename(disk_path)
935 (disk_name, disk_extension) = os.path.splitext(disk_file)
936 if disk_extension != disk_format:
937 logging.warning("Conversion of disk image to %s format, this may take"
938 " a while", disk_format)
940 new_disk_path = utils.GetClosedTempfile(suffix=".%s" % disk_format,
941 prefix=disk_name, dir=self.output_dir)
942 self.temp_file_manager.Add(new_disk_path)
944 constants.QEMUIMG_PATH,
951 run_result = utils.RunCmd(args, cwd=os.getcwd())
952 if run_result.failed:
953 raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was"
954 ": %s" % (disk_format, run_result.stderr))
955 return (".%s" % disk_format, new_disk_path)
958 def _GetDiskQemuInfo(disk_path, regexp):
959 """Figures out some information of the disk using qemu-img.
961 @type disk_path: string
962 @param disk_path: path to the disk we want to know the format of
964 @param regexp: string that has to be matched, it has to contain one group
968 @raise errors.OpPrereqError: format information cannot be retrieved
972 args = [constants.QEMUIMG_PATH, "info", disk_path]
973 run_result = utils.RunCmd(args, cwd=os.getcwd())
974 if run_result.failed:
975 raise errors.OpPrereqError("Gathering info about the disk using qemu-img"
976 " failed, output was: %s" % run_result.stderr)
977 result = run_result.output
978 regexp = r"%s" % regexp
979 match = re.search(regexp, result)
981 disk_format = match.group(1)
983 raise errors.OpPrereqError("No file information matching %s found in:"
984 " %s" % (regexp, result))
988 """Parses the data and creates a structure containing all required info.
991 raise NotImplementedError()
994 """Saves the gathered configuration in an apropriate format.
997 raise NotImplementedError()
1000 """Cleans the temporary directory, if one was created.
1003 self.temp_file_manager.Cleanup()
1005 shutil.rmtree(self.temp_dir)
1006 self.temp_dir = None
1009 class OVFImporter(Converter):
1010 """Converter from OVF to Ganeti config file.
1012 @type input_dir: string
1013 @ivar input_dir: directory in which the .ovf file resides
1014 @type output_dir: string
1015 @ivar output_dir: directory to which the results of conversion shall be
1017 @type input_path: string
1018 @ivar input_path: complete path to the .ovf file
1019 @type ovf_reader: L{OVFReader}
1020 @ivar ovf_reader: OVF reader instance collects data from .ovf file
1021 @type results_name: string
1022 @ivar results_name: name of imported instance
1023 @type results_template: string
1024 @ivar results_template: disk template read from .ovf file or command line
1026 @type results_hypervisor: dict
1027 @ivar results_hypervisor: hypervisor information gathered from .ovf file or
1028 command line arguments
1029 @type results_os: dict
1030 @ivar results_os: operating system information gathered from .ovf file or
1031 command line arguments
1032 @type results_backend: dict
1033 @ivar results_backend: backend information gathered from .ovf file or
1034 command line arguments
1035 @type results_tags: string
1036 @ivar results_tags: string containing instance-specific tags
1037 @type results_version: string
1038 @ivar results_version: version as required by Ganeti import
1039 @type results_network: dict
1040 @ivar results_network: network information gathered from .ovf file or command
1042 @type results_disk: dict
1043 @ivar results_disk: disk information gathered from .ovf file or command line
1047 def _ReadInputData(self, input_path):
1048 """Reads the data on which the conversion will take place.
1050 @type input_path: string
1051 @param input_path: absolute path to the .ovf or .ova input file
1053 @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
1056 (input_dir, input_file) = os.path.split(input_path)
1057 (_, input_extension) = os.path.splitext(input_file)
1059 if input_extension == OVF_EXT:
1060 logging.info("%s file extension found, no unpacking necessary", OVF_EXT)
1061 self.input_dir = input_dir
1062 self.input_path = input_path
1063 self.temp_dir = None
1064 elif input_extension == OVA_EXT:
1065 logging.info("%s file extension found, proceeding to unpacking", OVA_EXT)
1066 self._UnpackOVA(input_path)
1068 raise errors.OpPrereqError("Unknown file extension; expected %s or %s"
1069 " file" % (OVA_EXT, OVF_EXT))
1070 assert ((input_extension == OVA_EXT and self.temp_dir) or
1071 (input_extension == OVF_EXT and not self.temp_dir))
1072 assert self.input_dir in self.input_path
1074 if self.options.output_dir:
1075 self.output_dir = os.path.abspath(self.options.output_dir)
1076 if (os.path.commonprefix([constants.EXPORT_DIR, self.output_dir]) !=
1077 constants.EXPORT_DIR):
1078 logging.warning("Export path is not under %s directory, import to"
1079 " Ganeti using gnt-backup may fail",
1080 constants.EXPORT_DIR)
1082 self.output_dir = constants.EXPORT_DIR
1084 self.ovf_reader = OVFReader(self.input_path)
1085 self.ovf_reader.VerifyManifest()
1087 def _UnpackOVA(self, input_path):
1088 """Unpacks the .ova package into temporary directory.
1090 @type input_path: string
1091 @param input_path: path to the .ova package file
1093 @raise errors.OpPrereqError: if file is not a proper tarball, one of the
1094 files in the archive seem malicious (e.g. path starts with '../') or
1095 .ova package does not contain .ovf file
1099 if not tarfile.is_tarfile(input_path):
1100 raise errors.OpPrereqError("The provided %s file is not a proper tar"
1101 " archive", OVA_EXT)
1102 ova_content = tarfile.open(input_path)
1103 temp_dir = tempfile.mkdtemp()
1104 self.temp_dir = temp_dir
1105 for file_name in ova_content.getnames():
1106 file_normname = os.path.normpath(file_name)
1108 utils.PathJoin(temp_dir, file_normname)
1109 except ValueError, err:
1110 raise errors.OpPrereqError("File %s inside %s package is not safe" %
1111 (file_name, OVA_EXT))
1112 if file_name.endswith(OVF_EXT):
1113 input_name = file_name
1115 raise errors.OpPrereqError("No %s file in %s package found" %
1117 logging.warning("Unpacking the %s archive, this may take a while",
1119 self.input_dir = temp_dir
1120 self.input_path = utils.PathJoin(self.temp_dir, input_name)
1123 extract = ova_content.extractall
1124 except AttributeError:
1125 # This is a prehistorical case of using python < 2.5
1126 for member in ova_content.getmembers():
1127 ova_content.extract(member, path=self.temp_dir)
1129 extract(self.temp_dir)
1130 except tarfile.TarError, err:
1131 raise errors.OpPrereqError("Error while extracting %s archive: %s" %
1133 logging.info("OVA package extracted to %s directory", self.temp_dir)
1136 """Parses the data and creates a structure containing all required info.
1138 The method reads the information given either as a command line option or as
1139 a part of the OVF description.
1141 @raise errors.OpPrereqError: if some required part of the description of
1142 virtual instance is missing or unable to create output directory
1145 self.results_name = self._GetInfo("instance name", self.options.name,
1146 self._ParseNameOptions, self.ovf_reader.GetInstanceName)
1147 if not self.results_name:
1148 raise errors.OpPrereqError("Name of instance not provided")
1150 self.output_dir = utils.PathJoin(self.output_dir, self.results_name)
1152 utils.Makedirs(self.output_dir)
1153 except OSError, err:
1154 raise errors.OpPrereqError("Failed to create directory %s: %s" %
1155 (self.output_dir, err))
1157 self.results_template = self._GetInfo("disk template",
1158 self.options.disk_template, self._ParseTemplateOptions,
1159 self.ovf_reader.GetDiskTemplate)
1160 if not self.results_template:
1161 logging.info("Disk template not given")
1163 self.results_hypervisor = self._GetInfo("hypervisor",
1164 self.options.hypervisor, self._ParseHypervisorOptions,
1165 self.ovf_reader.GetHypervisorData)
1166 assert self.results_hypervisor["hypervisor_name"]
1167 if self.results_hypervisor["hypervisor_name"] == constants.VALUE_AUTO:
1168 logging.debug("Default hypervisor settings from the cluster will be used")
1170 self.results_os = self._GetInfo("OS", self.options.os,
1171 self._ParseOSOptions, self.ovf_reader.GetOSData)
1172 if not self.results_os.get("os_name"):
1173 raise errors.OpPrereqError("OS name must be provided")
1175 self.results_backend = self._GetInfo("backend", self.options.beparams,
1176 self._ParseBackendOptions, self.ovf_reader.GetBackendData)
1177 assert self.results_backend.get("vcpus")
1178 assert self.results_backend.get("memory")
1179 assert self.results_backend.get("auto_balance") is not None
1181 self.results_tags = self._GetInfo("tags", self.options.tags,
1182 self._ParseTags, self.ovf_reader.GetTagsData)
1184 ovf_version = self.ovf_reader.GetVersionData()
1186 self.results_version = ovf_version
1188 self.results_version = constants.EXPORT_VERSION
1190 self.results_network = self._GetInfo("network", self.options.nics,
1191 self._ParseNicOptions, self.ovf_reader.GetNetworkData,
1192 ignore_test=self.options.no_nics)
1194 self.results_disk = self._GetInfo("disk", self.options.disks,
1195 self._ParseDiskOptions, self._GetDiskInfo,
1196 ignore_test=self.results_template == constants.DT_DISKLESS)
1198 if not self.results_disk and not self.results_network:
1199 raise errors.OpPrereqError("Either disk specification or network"
1200 " description must be present")
1203 def _GetInfo(name, cmd_arg, cmd_function, nocmd_function,
1205 """Get information about some section - e.g. disk, network, hypervisor.
1208 @param name: name of the section
1210 @param cmd_arg: command line argument specific for section 'name'
1211 @type cmd_function: callable
1212 @param cmd_function: function to call if 'cmd_args' exists
1213 @type nocmd_function: callable
1214 @param nocmd_function: function to call if 'cmd_args' is not there
1218 logging.info("Information for %s will be ignored", name)
1221 logging.info("Information for %s will be parsed from command line", name)
1222 results = cmd_function()
1224 logging.info("Information for %s will be parsed from %s file",
1226 results = nocmd_function()
1227 logging.info("Options for %s were succesfully read", name)
1230 def _ParseNameOptions(self):
1231 """Returns name if one was given in command line.
1234 @return: name of an instance
1237 return self.options.name
1239 def _ParseTemplateOptions(self):
1240 """Returns disk template if one was given in command line.
1243 @return: disk template name
1246 return self.options.disk_template
1248 def _ParseHypervisorOptions(self):
1249 """Parses hypervisor options given in a command line.
1252 @return: dictionary containing name of the chosen hypervisor and all the
1256 assert type(self.options.hypervisor) is tuple
1257 assert len(self.options.hypervisor) == 2
1259 if self.options.hypervisor[0]:
1260 results["hypervisor_name"] = self.options.hypervisor[0]
1262 results["hypervisor_name"] = constants.VALUE_AUTO
1263 results.update(self.options.hypervisor[1])
1266 def _ParseOSOptions(self):
1267 """Parses OS options given in command line.
1270 @return: dictionary containing name of chosen OS and all its options
1273 assert self.options.os
1275 results["os_name"] = self.options.os
1276 results.update(self.options.osparams)
1279 def _ParseBackendOptions(self):
1280 """Parses backend options given in command line.
1283 @return: dictionary containing vcpus, memory and auto-balance options
1286 assert self.options.beparams
1288 backend.update(self.options.beparams)
1289 must_contain = ["vcpus", "memory", "auto_balance"]
1290 for element in must_contain:
1291 if backend.get(element) is None:
1292 backend[element] = constants.VALUE_AUTO
1295 def _ParseTags(self):
1296 """Returns tags list given in command line.
1299 @return: string containing comma-separated tags
1302 return self.options.tags
1304 def _ParseNicOptions(self):
1305 """Parses network options given in a command line or as a dictionary.
1308 @return: dictionary of network-related options
1311 assert self.options.nics
1313 for (nic_id, nic_desc) in self.options.nics:
1314 results["nic%s_mode" % nic_id] = \
1315 nic_desc.get("mode", constants.VALUE_AUTO)
1316 results["nic%s_mac" % nic_id] = nic_desc.get("mac", constants.VALUE_AUTO)
1317 results["nic%s_link" % nic_id] = \
1318 nic_desc.get("link", constants.VALUE_AUTO)
1319 results["nic%s_network" % nic_id] = \
1320 nic_desc.get("network", constants.VALUE_AUTO)
1321 if nic_desc.get("mode") == "bridged":
1322 results["nic%s_ip" % nic_id] = constants.VALUE_NONE
1324 results["nic%s_ip" % nic_id] = constants.VALUE_AUTO
1325 results["nic_count"] = str(len(self.options.nics))
1328 def _ParseDiskOptions(self):
1329 """Parses disk options given in a command line.
1332 @return: dictionary of disk-related options
1334 @raise errors.OpPrereqError: disk description does not contain size
1335 information or size information is invalid or creation failed
1339 assert self.options.disks
1341 for (disk_id, disk_desc) in self.options.disks:
1342 results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id
1343 if disk_desc.get("size"):
1345 disk_size = utils.ParseUnit(disk_desc["size"])
1347 raise errors.OpPrereqError("Invalid disk size for disk %s: %s" %
1348 (disk_id, disk_desc["size"]))
1349 new_path = utils.PathJoin(self.output_dir, str(disk_id))
1351 constants.QEMUIMG_PATH,
1358 run_result = utils.RunCmd(args)
1359 if run_result.failed:
1360 raise errors.OpPrereqError("Creation of disk %s failed, output was:"
1361 " %s" % (new_path, run_result.stderr))
1362 results["disk%s_size" % disk_id] = str(disk_size)
1363 results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id
1365 raise errors.OpPrereqError("Disks created for import must have their"
1367 results["disk_count"] = str(len(self.options.disks))
1370 def _GetDiskInfo(self):
1371 """Gathers information about disks used by instance, perfomes conversion.
1374 @return: dictionary of disk-related options
1376 @raise errors.OpPrereqError: disk is not in the same directory as .ovf file
1380 disks_list = self.ovf_reader.GetDisksNames()
1381 for (counter, (disk_name, disk_compression)) in enumerate(disks_list):
1382 if os.path.dirname(disk_name):
1383 raise errors.OpPrereqError("Disks are not allowed to have absolute"
1384 " paths or paths outside main OVF directory")
1385 disk, _ = os.path.splitext(disk_name)
1386 disk_path = utils.PathJoin(self.input_dir, disk_name)
1387 if disk_compression not in NO_COMPRESSION:
1388 _, disk_path = self._CompressDisk(disk_path, disk_compression,
1390 disk, _ = os.path.splitext(disk)
1391 if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw":
1392 logging.info("Conversion to raw format is required")
1393 ext, new_disk_path = self._ConvertDisk("raw", disk_path)
1395 final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext,
1396 directory=self.output_dir)
1397 final_name = os.path.basename(final_disk_path)
1398 disk_size = os.path.getsize(final_disk_path) / (1024 * 1024)
1399 results["disk%s_dump" % counter] = final_name
1400 results["disk%s_size" % counter] = str(disk_size)
1401 results["disk%s_ivname" % counter] = "disk/%s" % str(counter)
1403 results["disk_count"] = str(len(disks_list))
1407 """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
1409 @raise errors.OpPrereqError: when saving to config file failed
1412 logging.info("Conversion was succesfull, saving %s in %s directory",
1413 constants.EXPORT_CONF_FILE, self.output_dir)
1415 constants.INISECT_INS: {},
1416 constants.INISECT_BEP: {},
1417 constants.INISECT_EXP: {},
1418 constants.INISECT_OSP: {},
1419 constants.INISECT_HYP: {},
1422 results[constants.INISECT_INS].update(self.results_disk)
1423 results[constants.INISECT_INS].update(self.results_network)
1424 results[constants.INISECT_INS]["hypervisor"] = \
1425 self.results_hypervisor["hypervisor_name"]
1426 results[constants.INISECT_INS]["name"] = self.results_name
1427 if self.results_template:
1428 results[constants.INISECT_INS]["disk_template"] = self.results_template
1429 if self.results_tags:
1430 results[constants.INISECT_INS]["tags"] = self.results_tags
1432 results[constants.INISECT_BEP].update(self.results_backend)
1434 results[constants.INISECT_EXP]["os"] = self.results_os["os_name"]
1435 results[constants.INISECT_EXP]["version"] = self.results_version
1437 del self.results_os["os_name"]
1438 results[constants.INISECT_OSP].update(self.results_os)
1440 del self.results_hypervisor["hypervisor_name"]
1441 results[constants.INISECT_HYP].update(self.results_hypervisor)
1443 output_file_name = utils.PathJoin(self.output_dir,
1444 constants.EXPORT_CONF_FILE)
1447 for section, options in results.iteritems():
1448 output.append("[%s]" % section)
1449 for name, value in options.iteritems():
1452 output.append("%s = %s" % (name, value))
1454 output_contents = "\n".join(output)
1457 utils.WriteFile(output_file_name, data=output_contents)
1458 except errors.ProgrammerError, err:
1459 raise errors.OpPrereqError("Saving the config file failed: %s" % err)
1464 class ConfigParserWithDefaults(ConfigParser.SafeConfigParser):
1465 """This is just a wrapper on SafeConfigParser, that uses default values
1468 def get(self, section, options, raw=None, vars=None): # pylint: disable=W0622
1470 result = ConfigParser.SafeConfigParser.get(self, section, options, \
1472 except ConfigParser.NoOptionError:
1476 def getint(self, section, options):
1478 result = ConfigParser.SafeConfigParser.get(self, section, options)
1479 except ConfigParser.NoOptionError:
1484 class OVFExporter(Converter):
1485 """Converter from Ganeti config file to OVF
1487 @type input_dir: string
1488 @ivar input_dir: directory in which the config.ini file resides
1489 @type output_dir: string
1490 @ivar output_dir: directory to which the results of conversion shall be
1492 @type packed_dir: string
1493 @ivar packed_dir: if we want OVA package, this points to the real (i.e. not
1494 temp) output directory
1495 @type input_path: string
1496 @ivar input_path: complete path to the config.ini file
1497 @type output_path: string
1498 @ivar output_path: complete path to .ovf file
1499 @type config_parser: L{ConfigParserWithDefaults}
1500 @ivar config_parser: parser for the config.ini file
1501 @type reference_files: list
1502 @ivar reference_files: files referenced in the ovf file
1503 @type results_disk: list
1504 @ivar results_disk: list of dictionaries of disk options from config.ini
1505 @type results_network: list
1506 @ivar results_network: list of dictionaries of network options form config.ini
1507 @type results_name: string
1508 @ivar results_name: name of the instance
1509 @type results_vcpus: string
1510 @ivar results_vcpus: number of VCPUs
1511 @type results_memory: string
1512 @ivar results_memory: RAM memory in MB
1513 @type results_ganeti: dict
1514 @ivar results_ganeti: dictionary of Ganeti-specific options from config.ini
1517 def _ReadInputData(self, input_path):
1518 """Reads the data on which the conversion will take place.
1520 @type input_path: string
1521 @param input_path: absolute path to the config.ini input file
1523 @raise errors.OpPrereqError: error when reading the config file
1526 input_dir = os.path.dirname(input_path)
1527 self.input_path = input_path
1528 self.input_dir = input_dir
1529 if self.options.output_dir:
1530 self.output_dir = os.path.abspath(self.options.output_dir)
1532 self.output_dir = input_dir
1533 self.config_parser = ConfigParserWithDefaults()
1534 logging.info("Reading configuration from %s file", input_path)
1536 self.config_parser.read(input_path)
1537 except ConfigParser.MissingSectionHeaderError, err:
1538 raise errors.OpPrereqError("Error when trying to read %s: %s" %
1540 if self.options.ova_package:
1541 self.temp_dir = tempfile.mkdtemp()
1542 self.packed_dir = self.output_dir
1543 self.output_dir = self.temp_dir
1545 self.ovf_writer = OVFWriter(not self.options.ext_usage)
1547 def _ParseName(self):
1548 """Parses name from command line options or config file.
1551 @return: name of Ganeti instance
1553 @raise errors.OpPrereqError: if name of the instance is not provided
1556 if self.options.name:
1557 name = self.options.name
1559 name = self.config_parser.get(constants.INISECT_INS, NAME)
1561 raise errors.OpPrereqError("No instance name found")
1564 def _ParseVCPUs(self):
1565 """Parses vcpus number from config file.
1568 @return: number of virtual CPUs
1570 @raise errors.OpPrereqError: if number of VCPUs equals 0
1573 vcpus = self.config_parser.getint(constants.INISECT_BEP, VCPUS)
1575 raise errors.OpPrereqError("No CPU information found")
1578 def _ParseMemory(self):
1579 """Parses vcpus number from config file.
1582 @return: amount of memory in MB
1584 @raise errors.OpPrereqError: if amount of memory equals 0
1587 memory = self.config_parser.getint(constants.INISECT_BEP, MEMORY)
1589 raise errors.OpPrereqError("No memory information found")
1592 def _ParseGaneti(self):
1593 """Parses Ganeti data from config file.
1596 @return: dictionary of Ganeti-specific options
1601 results["hypervisor"] = {}
1602 hyp_name = self.config_parser.get(constants.INISECT_INS, HYPERV)
1603 if hyp_name is None:
1604 raise errors.OpPrereqError("No hypervisor information found")
1605 results["hypervisor"]["name"] = hyp_name
1606 pairs = self.config_parser.items(constants.INISECT_HYP)
1607 for (name, value) in pairs:
1608 results["hypervisor"][name] = value
1611 os_name = self.config_parser.get(constants.INISECT_EXP, OS)
1613 raise errors.OpPrereqError("No operating system information found")
1614 results["os"]["name"] = os_name
1615 pairs = self.config_parser.items(constants.INISECT_OSP)
1616 for (name, value) in pairs:
1617 results["os"][name] = value
1620 (constants.INISECT_INS, DISK_TEMPLATE, "disk_template"),
1621 (constants.INISECT_BEP, AUTO_BALANCE, "auto_balance"),
1622 (constants.INISECT_INS, TAGS, "tags"),
1623 (constants.INISECT_EXP, VERSION, "version"),
1625 for (section, element, name) in others:
1626 results[name] = self.config_parser.get(section, element)
1629 def _ParseNetworks(self):
1630 """Parses network data from config file.
1633 @return: list of dictionaries of network options
1635 @raise errors.OpPrereqError: then network mode is not recognized
1642 self.config_parser.get(constants.INISECT_INS,
1643 "nic%s_link" % counter)
1644 if data_link is None:
1647 "mode": self.config_parser.get(constants.INISECT_INS,
1648 "nic%s_mode" % counter),
1649 "mac": self.config_parser.get(constants.INISECT_INS,
1650 "nic%s_mac" % counter),
1651 "ip": self.config_parser.get(constants.INISECT_INS,
1652 "nic%s_ip" % counter),
1653 "network": self.config_parser.get(constants.INISECT_INS,
1654 "nic%s_network" % counter),
1657 if results[counter]["mode"] not in constants.NIC_VALID_MODES:
1658 raise errors.OpPrereqError("Network mode %s not recognized"
1659 % results[counter]["mode"])
1663 def _GetDiskOptions(self, disk_file, compression):
1664 """Convert the disk and gather disk info for .ovf file.
1666 @type disk_file: string
1667 @param disk_file: name of the disk (without the full path)
1668 @type compression: bool
1669 @param compression: whether the disk should be compressed or not
1671 @raise errors.OpPrereqError: when disk image does not exist
1674 disk_path = utils.PathJoin(self.input_dir, disk_file)
1676 if not os.path.isfile(disk_path):
1677 raise errors.OpPrereqError("Disk image does not exist: %s" % disk_path)
1678 if os.path.dirname(disk_file):
1679 raise errors.OpPrereqError("Path for the disk: %s contains a directory"
1680 " name" % disk_path)
1681 disk_name, _ = os.path.splitext(disk_file)
1682 ext, new_disk_path = self._ConvertDisk(self.options.disk_format, disk_path)
1683 results["format"] = self.options.disk_format
1684 results["virt-size"] = self._GetDiskQemuInfo(new_disk_path,
1685 "virtual size: \S+ \((\d+) bytes\)")
1687 ext2, new_disk_path = self._CompressDisk(new_disk_path, "gzip",
1689 disk_name, _ = os.path.splitext(disk_name)
1690 results["compression"] = "gzip"
1692 final_disk_path = LinkFile(new_disk_path, prefix=disk_name, suffix=ext,
1693 directory=self.output_dir)
1694 final_disk_name = os.path.basename(final_disk_path)
1695 results["real-size"] = os.path.getsize(final_disk_path)
1696 results["path"] = final_disk_name
1697 self.references_files.append(final_disk_path)
1700 def _ParseDisks(self):
1701 """Parses disk data from config file.
1704 @return: list of dictionaries of disk options
1711 self.config_parser.get(constants.INISECT_INS, "disk%s_dump" % counter)
1712 if disk_file is None:
1714 results.append(self._GetDiskOptions(disk_file, self.options.compression))
1719 """Parses the data and creates a structure containing all required info.
1723 utils.Makedirs(self.output_dir)
1724 except OSError, err:
1725 raise errors.OpPrereqError("Failed to create directory %s: %s" %
1726 (self.output_dir, err))
1728 self.references_files = []
1729 self.results_name = self._ParseName()
1730 self.results_vcpus = self._ParseVCPUs()
1731 self.results_memory = self._ParseMemory()
1732 if not self.options.ext_usage:
1733 self.results_ganeti = self._ParseGaneti()
1734 self.results_network = self._ParseNetworks()
1735 self.results_disk = self._ParseDisks()
1737 def _PrepareManifest(self, path):
1738 """Creates manifest for all the files in OVF package.
1741 @param path: path to manifesto file
1743 @raise errors.OpPrereqError: if error occurs when writing file
1746 logging.info("Preparing manifest for the OVF package")
1748 files_list = [self.output_path]
1749 files_list.extend(self.references_files)
1750 logging.warning("Calculating SHA1 checksums, this may take a while")
1751 sha1_sums = utils.FingerprintFiles(files_list)
1752 for file_path, value in sha1_sums.iteritems():
1753 file_name = os.path.basename(file_path)
1754 lines.append("SHA1(%s)= %s" % (file_name, value))
1756 data = "\n".join(lines)
1758 utils.WriteFile(path, data=data)
1759 except errors.ProgrammerError, err:
1760 raise errors.OpPrereqError("Saving the manifest file failed: %s" % err)
1763 def _PrepareTarFile(tar_path, files_list):
1764 """Creates tarfile from the files in OVF package.
1766 @type tar_path: string
1767 @param tar_path: path to the resulting file
1768 @type files_list: list
1769 @param files_list: list of files in the OVF package
1772 logging.info("Preparing tarball for the OVF package")
1773 open(tar_path, mode="w").close()
1774 ova_package = tarfile.open(name=tar_path, mode="w")
1775 for file_path in files_list:
1776 file_name = os.path.basename(file_path)
1777 ova_package.add(file_path, arcname=file_name)
1781 """Saves the gathered configuration in an apropriate format.
1783 @raise errors.OpPrereqError: if unable to create output directory
1786 output_file = "%s%s" % (self.results_name, OVF_EXT)
1787 output_path = utils.PathJoin(self.output_dir, output_file)
1788 self.ovf_writer = OVFWriter(not self.options.ext_usage)
1789 logging.info("Saving read data to %s", output_path)
1791 self.output_path = utils.PathJoin(self.output_dir, output_file)
1792 files_list = [self.output_path]
1794 self.ovf_writer.SaveDisksData(self.results_disk)
1795 self.ovf_writer.SaveNetworksData(self.results_network)
1796 if not self.options.ext_usage:
1797 self.ovf_writer.SaveGanetiData(self.results_ganeti, self.results_network)
1799 self.ovf_writer.SaveVirtualSystemData(self.results_name, self.results_vcpus,
1800 self.results_memory)
1802 data = self.ovf_writer.PrettyXmlDump()
1803 utils.WriteFile(self.output_path, data=data)
1805 manifest_file = "%s%s" % (self.results_name, MF_EXT)
1806 manifest_path = utils.PathJoin(self.output_dir, manifest_file)
1807 self._PrepareManifest(manifest_path)
1808 files_list.append(manifest_path)
1810 files_list.extend(self.references_files)
1812 if self.options.ova_package:
1813 ova_file = "%s%s" % (self.results_name, OVA_EXT)
1814 packed_path = utils.PathJoin(self.packed_dir, ova_file)
1816 utils.Makedirs(self.packed_dir)
1817 except OSError, err:
1818 raise errors.OpPrereqError("Failed to create directory %s: %s" %
1819 (self.packed_dir, err))
1820 self._PrepareTarFile(packed_path, files_list)
1821 logging.info("Creation of the OVF package was successfull")