4 # Copyright (C) 2011, 2012 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"
155 " to continue", errors.ECODE_STATE)
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),
195 errors.ECODE_ENVIRON)
199 class OVFReader(object):
200 """Reader class for OVF files.
202 @type files_list: list
203 @ivar files_list: list of files in the OVF package
204 @type tree: ET.ElementTree
205 @ivar tree: XML tree of the .ovf file
206 @type schema_name: string
207 @ivar schema_name: name of the .ovf file
208 @type input_dir: string
209 @ivar input_dir: directory in which the .ovf file resides
212 def __init__(self, input_path):
213 """Initialiaze the reader - load the .ovf file to XML parser.
215 It is assumed that names of manifesto (.mf), certificate (.cert) and ovf
216 files are the same. In order to account any other files as part of the ovf
217 package, they have to be explicitly mentioned in the Resources section
220 @type input_path: string
221 @param input_path: absolute path to the .ovf file
223 @raise errors.OpPrereqError: when .ovf file is not a proper XML file or some
224 of the files mentioned in Resources section do not exist
227 self.tree = ET.ElementTree()
229 self.tree.parse(input_path)
230 except (ParseError, xml.parsers.expat.ExpatError), err:
231 raise errors.OpPrereqError("Error while reading %s file: %s" %
232 (OVF_EXT, err), errors.ECODE_ENVIRON)
234 # Create a list of all files in the OVF package
235 (input_dir, input_file) = os.path.split(input_path)
236 (input_name, _) = os.path.splitext(input_file)
237 files_directory = utils.ListVisibleFiles(input_dir)
239 for file_name in files_directory:
240 (name, extension) = os.path.splitext(file_name)
241 if extension in FILE_EXTENSIONS and name == input_name:
242 files_list.append(file_name)
243 files_list += self._GetAttributes("{%s}References/{%s}File" %
244 (OVF_SCHEMA, OVF_SCHEMA),
245 "{%s}href" % OVF_SCHEMA)
246 for file_name in files_list:
247 file_path = utils.PathJoin(input_dir, file_name)
248 if not os.path.exists(file_path):
249 raise errors.OpPrereqError("File does not exist: %s" % file_path,
250 errors.ECODE_ENVIRON)
251 logging.info("Files in the OVF package: %s", " ".join(files_list))
252 self.files_list = files_list
253 self.input_dir = input_dir
254 self.schema_name = input_name
256 def _GetAttributes(self, path, attribute):
257 """Get specified attribute from all nodes accessible using given path.
259 Function follows the path from root node to the desired tags using path,
260 then reads the apropriate attribute values.
263 @param path: path of nodes to visit
264 @type attribute: string
265 @param attribute: attribute for which we gather the information
267 @return: for each accessible tag with the attribute value set, value of the
271 current_list = self.tree.findall(path)
272 results = [x.get(attribute) for x in current_list]
273 return filter(None, results)
275 def _GetElementMatchingAttr(self, path, match_attr):
276 """Searches for element on a path that matches certain attribute value.
278 Function follows the path from root node to the desired tags using path,
279 then searches for the first one matching the attribute value.
282 @param path: path of nodes to visit
283 @type match_attr: tuple
284 @param match_attr: pair (attribute, value) for which we search
285 @rtype: ET.ElementTree or None
286 @return: first element matching match_attr or None if nothing matches
289 potential_elements = self.tree.findall(path)
290 (attr, val) = match_attr
291 for elem in potential_elements:
292 if elem.get(attr) == val:
296 def _GetElementMatchingText(self, path, match_text):
297 """Searches for element on a path that matches certain text value.
299 Function follows the path from root node to the desired tags using path,
300 then searches for the first one matching the text value.
303 @param path: path of nodes to visit
304 @type match_text: tuple
305 @param match_text: pair (node, text) for which we search
306 @rtype: ET.ElementTree or None
307 @return: first element matching match_text or None if nothing matches
310 potential_elements = self.tree.findall(path)
311 (node, text) = match_text
312 for elem in potential_elements:
313 if elem.findtext(node) == text:
318 def _GetDictParameters(root, schema):
319 """Reads text in all children and creates the dictionary from the contents.
321 @type root: ET.ElementTree or None
322 @param root: father of the nodes we want to collect data about
324 @param schema: schema name to be removed from the tag
326 @return: dictionary containing tags and their text contents, tags have their
327 schema fragment removed or empty dictionary, when root is None
333 for element in list(root):
334 pref_len = len("{%s}" % schema)
335 assert(schema in element.tag)
336 tag = element.tag[pref_len:]
337 results[tag] = element.text
340 def VerifyManifest(self):
341 """Verifies manifest for the OVF package, if one is given.
343 @raise errors.OpPrereqError: if SHA1 checksums do not match
346 if "%s%s" % (self.schema_name, MF_EXT) in self.files_list:
347 logging.warning("Verifying SHA1 checksums, this may take a while")
348 manifest_filename = "%s%s" % (self.schema_name, MF_EXT)
349 manifest_path = utils.PathJoin(self.input_dir, manifest_filename)
350 manifest_content = utils.ReadFile(manifest_path).splitlines()
352 regexp = r"SHA1\((\S+)\)= (\S+)"
353 for line in manifest_content:
354 match = re.match(regexp, line)
356 file_name = match.group(1)
357 sha1_sum = match.group(2)
358 manifest_files[file_name] = sha1_sum
359 files_with_paths = [utils.PathJoin(self.input_dir, file_name)
360 for file_name in self.files_list]
361 sha1_sums = utils.FingerprintFiles(files_with_paths)
362 for file_name, value in manifest_files.iteritems():
363 if sha1_sums.get(utils.PathJoin(self.input_dir, file_name)) != value:
364 raise errors.OpPrereqError("SHA1 checksum of %s does not match the"
365 " value in manifest file" % file_name,
366 errors.ECODE_ENVIRON)
367 logging.info("SHA1 checksums verified")
369 def GetInstanceName(self):
370 """Provides information about instance name.
373 @return: instance name string
376 find_name = "{%s}VirtualSystem/{%s}Name" % (OVF_SCHEMA, OVF_SCHEMA)
377 return self.tree.findtext(find_name)
379 def GetDiskTemplate(self):
380 """Returns disk template from .ovf file
382 @rtype: string or None
383 @return: name of the template
385 find_template = ("{%s}GanetiSection/{%s}DiskTemplate" %
386 (GANETI_SCHEMA, GANETI_SCHEMA))
387 return self.tree.findtext(find_template)
389 def GetHypervisorData(self):
390 """Provides hypervisor information - hypervisor name and options.
393 @return: dictionary containing name of the used hypervisor and all the
397 hypervisor_search = ("{%s}GanetiSection/{%s}Hypervisor" %
398 (GANETI_SCHEMA, GANETI_SCHEMA))
399 hypervisor_data = self.tree.find(hypervisor_search)
400 if not hypervisor_data:
401 return {"hypervisor_name": constants.VALUE_AUTO}
403 "hypervisor_name": hypervisor_data.findtext("{%s}Name" % GANETI_SCHEMA,
404 default=constants.VALUE_AUTO),
406 parameters = hypervisor_data.find("{%s}Parameters" % GANETI_SCHEMA)
407 results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
411 """ Provides operating system information - os name and options.
414 @return: dictionary containing name and options for the chosen OS
418 os_search = ("{%s}GanetiSection/{%s}OperatingSystem" %
419 (GANETI_SCHEMA, GANETI_SCHEMA))
420 os_data = self.tree.find(os_search)
422 results["os_name"] = os_data.findtext("{%s}Name" % GANETI_SCHEMA)
423 parameters = os_data.find("{%s}Parameters" % GANETI_SCHEMA)
424 results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
427 def GetBackendData(self):
428 """ Provides backend information - vcpus, memory, auto balancing options.
431 @return: dictionary containing options for vcpus, memory and auto balance
437 find_vcpus = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item" %
438 (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
439 match_vcpus = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["vcpus"])
440 vcpus = self._GetElementMatchingText(find_vcpus, match_vcpus)
442 vcpus_count = vcpus.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
443 default=constants.VALUE_AUTO)
445 vcpus_count = constants.VALUE_AUTO
446 results["vcpus"] = str(vcpus_count)
448 find_memory = find_vcpus
449 match_memory = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["memory"])
450 memory = self._GetElementMatchingText(find_memory, match_memory)
453 alloc_units = memory.findtext("{%s}AllocationUnits" % RASD_SCHEMA)
454 matching_units = [units for units, variants in ALLOCATION_UNITS.items()
455 if alloc_units.lower() in variants]
456 if matching_units == []:
457 raise errors.OpPrereqError("Unit %s for RAM memory unknown" %
458 alloc_units, errors.ECODE_INVAL)
459 units = matching_units[0]
460 memory_raw = int(memory.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
461 default=constants.VALUE_AUTO))
462 memory_count = CONVERT_UNITS_TO_MB[units](memory_raw)
464 memory_count = constants.VALUE_AUTO
465 results["memory"] = str(memory_count)
467 find_balance = ("{%s}GanetiSection/{%s}AutoBalance" %
468 (GANETI_SCHEMA, GANETI_SCHEMA))
469 balance = self.tree.findtext(find_balance, default=constants.VALUE_AUTO)
470 results["auto_balance"] = balance
474 def GetTagsData(self):
475 """Provides tags information for instance.
477 @rtype: string or None
478 @return: string of comma-separated tags for the instance
481 find_tags = "{%s}GanetiSection/{%s}Tags" % (GANETI_SCHEMA, GANETI_SCHEMA)
482 results = self.tree.findtext(find_tags)
488 def GetVersionData(self):
489 """Provides version number read from .ovf file
492 @return: string containing the version number
495 find_version = ("{%s}GanetiSection/{%s}Version" %
496 (GANETI_SCHEMA, GANETI_SCHEMA))
497 return self.tree.findtext(find_version)
499 def GetNetworkData(self):
500 """Provides data about the network in the OVF instance.
502 The method gathers the data about networks used by OVF instance. It assumes
503 that 'name' tag means something - in essence, if it contains one of the
504 words 'bridged' or 'routed' then that will be the mode of this network in
505 Ganeti. The information about the network can be either in GanetiSection or
506 VirtualHardwareSection.
509 @return: dictionary containing all the network information
513 networks_search = ("{%s}NetworkSection/{%s}Network" %
514 (OVF_SCHEMA, OVF_SCHEMA))
515 network_names = self._GetAttributes(networks_search,
516 "{%s}name" % OVF_SCHEMA)
517 required = ["ip", "mac", "link", "mode"]
518 for (counter, network_name) in enumerate(network_names):
519 network_search = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item"
520 % (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
521 ganeti_search = ("{%s}GanetiSection/{%s}Network/{%s}Nic" %
522 (GANETI_SCHEMA, GANETI_SCHEMA, GANETI_SCHEMA))
523 network_match = ("{%s}Connection" % RASD_SCHEMA, network_name)
524 ganeti_match = ("{%s}name" % OVF_SCHEMA, network_name)
525 network_data = self._GetElementMatchingText(network_search, network_match)
526 network_ganeti_data = self._GetElementMatchingAttr(ganeti_search,
530 if network_ganeti_data:
531 ganeti_data["mode"] = network_ganeti_data.findtext("{%s}Mode" %
533 ganeti_data["mac"] = network_ganeti_data.findtext("{%s}MACAddress" %
535 ganeti_data["ip"] = network_ganeti_data.findtext("{%s}IPAddress" %
537 ganeti_data["link"] = network_ganeti_data.findtext("{%s}Link" %
541 mac_data = network_data.findtext("{%s}Address" % RASD_SCHEMA)
543 network_name = network_name.lower()
545 # First, some not Ganeti-specific information is collected
546 if constants.NIC_MODE_BRIDGED in network_name:
547 results["nic%s_mode" % counter] = "bridged"
548 elif constants.NIC_MODE_ROUTED in network_name:
549 results["nic%s_mode" % counter] = "routed"
550 results["nic%s_mac" % counter] = mac_data
552 # GanetiSection data overrides 'manually' collected data
553 for name, value in ganeti_data.iteritems():
554 results["nic%s_%s" % (counter, name)] = value
556 # Bridged network has no IP - unless specifically stated otherwise
557 if (results.get("nic%s_mode" % counter) == "bridged" and
558 not results.get("nic%s_ip" % counter)):
559 results["nic%s_ip" % counter] = constants.VALUE_NONE
561 for option in required:
562 if not results.get("nic%s_%s" % (counter, option)):
563 results["nic%s_%s" % (counter, option)] = constants.VALUE_AUTO
566 results["nic_count"] = str(len(network_names))
569 def GetDisksNames(self):
570 """Provides list of file names for the disks used by the instance.
573 @return: list of file names, as referenced in .ovf file
577 disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA)
578 disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA)
579 for disk in disk_ids:
580 disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA)
581 disk_match = ("{%s}id" % OVF_SCHEMA, disk)
582 disk_elem = self._GetElementMatchingAttr(disk_search, disk_match)
583 if disk_elem is None:
584 raise errors.OpPrereqError("%s file corrupted - disk %s not found in"
585 " references" % (OVF_EXT, disk),
586 errors.ECODE_ENVIRON)
587 disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA)
588 disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA)
589 results.append((disk_name, disk_compression))
593 def SubElementText(parent, tag, text, attrib={}, **extra):
594 # pylint: disable=W0102
595 """This is just a wrapper on ET.SubElement that always has text content.
600 elem = ET.SubElement(parent, tag, attrib=attrib, **extra)
601 elem.text = str(text)
605 class OVFWriter(object):
606 """Writer class for OVF files.
608 @type tree: ET.ElementTree
609 @ivar tree: XML tree that we are constructing
610 @type virtual_system_type: string
611 @ivar virtual_system_type: value of vssd:VirtualSystemType, for external usage
612 in VMWare this requires to be vmx
613 @type hardware_list: list
614 @ivar hardware_list: list of items prepared for VirtualHardwareSection
615 @type next_instance_id: int
616 @ivar next_instance_id: next instance id to be used when creating elements on
620 def __init__(self, has_gnt_section):
621 """Initialize the writer - set the top element.
623 @type has_gnt_section: bool
624 @param has_gnt_section: if the Ganeti schema should be added - i.e. this
625 means that Ganeti section will be present
629 "xmlns:xsi": XML_SCHEMA,
630 "xmlns:vssd": VSSD_SCHEMA,
631 "xmlns:rasd": RASD_SCHEMA,
632 "xmlns:ovf": OVF_SCHEMA,
637 env_attribs["xmlns:gnt"] = GANETI_SCHEMA
638 self.virtual_system_type = VS_TYPE["ganeti"]
640 self.virtual_system_type = VS_TYPE["external"]
641 self.tree = ET.Element("Envelope", attrib=env_attribs)
642 self.hardware_list = []
643 # INSTANCE_ID contains statically assigned IDs, starting from 0
644 self.next_instance_id = len(INSTANCE_ID) # FIXME: hackish
646 def SaveDisksData(self, disks):
647 """Convert disk information to certain OVF sections.
650 @param disks: list of dictionaries of disk options from config.ini
653 references = ET.SubElement(self.tree, "References")
654 disk_section = ET.SubElement(self.tree, "DiskSection")
655 SubElementText(disk_section, "Info", "Virtual disk information")
656 for counter, disk in enumerate(disks):
657 file_id = "file%s" % counter
658 disk_id = "disk%s" % counter
660 "ovf:href": disk["path"],
661 "ovf:size": str(disk["real-size"]),
665 "ovf:capacity": str(disk["virt-size"]),
666 "ovf:diskId": disk_id,
667 "ovf:fileRef": file_id,
668 "ovf:format": DISK_FORMAT.get(disk["format"], disk["format"]),
670 if "compression" in disk:
671 file_attribs["ovf:compression"] = disk["compression"]
672 ET.SubElement(references, "File", attrib=file_attribs)
673 ET.SubElement(disk_section, "Disk", attrib=disk_attribs)
675 # Item in VirtualHardwareSection creation
676 disk_item = ET.Element("Item")
677 SubElementText(disk_item, "rasd:ElementName", disk_id)
678 SubElementText(disk_item, "rasd:HostResource", "ovf:/disk/%s" % disk_id)
679 SubElementText(disk_item, "rasd:InstanceID", self.next_instance_id)
680 SubElementText(disk_item, "rasd:Parent", INSTANCE_ID["scsi"])
681 SubElementText(disk_item, "rasd:ResourceType", RASD_TYPE["disk"])
682 self.hardware_list.append(disk_item)
683 self.next_instance_id += 1
685 def SaveNetworksData(self, networks):
686 """Convert network information to NetworkSection.
689 @param networks: list of dictionaries of network options form config.ini
692 network_section = ET.SubElement(self.tree, "NetworkSection")
693 SubElementText(network_section, "Info", "List of logical networks")
694 for counter, network in enumerate(networks):
695 network_name = "%s%s" % (network["mode"], counter)
696 network_attrib = {"ovf:name": network_name}
697 ET.SubElement(network_section, "Network", attrib=network_attrib)
699 # Item in VirtualHardwareSection creation
700 network_item = ET.Element("Item")
701 SubElementText(network_item, "rasd:Address", network["mac"])
702 SubElementText(network_item, "rasd:Connection", network_name)
703 SubElementText(network_item, "rasd:ElementName", network_name)
704 SubElementText(network_item, "rasd:InstanceID", self.next_instance_id)
705 SubElementText(network_item, "rasd:ResourceType",
706 RASD_TYPE["ethernet-adapter"])
707 self.hardware_list.append(network_item)
708 self.next_instance_id += 1
711 def _SaveNameAndParams(root, data):
712 """Save name and parameters information under root using data.
714 @type root: ET.Element
715 @param root: root element for the Name and Parameters
717 @param data: data from which we gather the values
720 assert(data.get("name"))
721 name = SubElementText(root, "gnt:Name", data["name"])
722 params = ET.SubElement(root, "gnt:Parameters")
723 for name, value in data.iteritems():
725 SubElementText(params, "gnt:%s" % name, value)
727 def SaveGanetiData(self, ganeti, networks):
728 """Convert Ganeti-specific information to GanetiSection.
731 @param ganeti: dictionary of Ganeti-specific options from config.ini
733 @param networks: list of dictionaries of network options form config.ini
736 ganeti_section = ET.SubElement(self.tree, "gnt:GanetiSection")
738 SubElementText(ganeti_section, "gnt:Version", ganeti.get("version"))
739 SubElementText(ganeti_section, "gnt:DiskTemplate",
740 ganeti.get("disk_template"))
741 SubElementText(ganeti_section, "gnt:AutoBalance",
742 ganeti.get("auto_balance"))
743 SubElementText(ganeti_section, "gnt:Tags", ganeti.get("tags"))
745 osys = ET.SubElement(ganeti_section, "gnt:OperatingSystem")
746 self._SaveNameAndParams(osys, ganeti["os"])
748 hypervisor = ET.SubElement(ganeti_section, "gnt:Hypervisor")
749 self._SaveNameAndParams(hypervisor, ganeti["hypervisor"])
751 network_section = ET.SubElement(ganeti_section, "gnt:Network")
752 for counter, network in enumerate(networks):
753 network_name = "%s%s" % (network["mode"], counter)
754 nic_attrib = {"ovf:name": network_name}
755 nic = ET.SubElement(network_section, "gnt:Nic", attrib=nic_attrib)
756 SubElementText(nic, "gnt:Mode", network["mode"])
757 SubElementText(nic, "gnt:MACAddress", network["mac"])
758 SubElementText(nic, "gnt:IPAddress", network["ip"])
759 SubElementText(nic, "gnt:Link", network["link"])
761 def SaveVirtualSystemData(self, name, vcpus, memory):
762 """Convert virtual system information to OVF sections.
765 @param name: name of the instance
767 @param vcpus: number of VCPUs
769 @param memory: RAM memory in MB
774 vs_attrib = {"ovf:id": name}
775 virtual_system = ET.SubElement(self.tree, "VirtualSystem", attrib=vs_attrib)
776 SubElementText(virtual_system, "Info", "A virtual machine")
778 name_section = ET.SubElement(virtual_system, "Name")
779 name_section.text = name
780 os_attrib = {"ovf:id": "0"}
781 os_section = ET.SubElement(virtual_system, "OperatingSystemSection",
783 SubElementText(os_section, "Info", "Installed guest operating system")
784 hardware_section = ET.SubElement(virtual_system, "VirtualHardwareSection")
785 SubElementText(hardware_section, "Info", "Virtual hardware requirements")
788 system = ET.SubElement(hardware_section, "System")
789 SubElementText(system, "vssd:ElementName", "Virtual Hardware Family")
790 SubElementText(system, "vssd:InstanceID", INSTANCE_ID["system"])
791 SubElementText(system, "vssd:VirtualSystemIdentifier", name)
792 SubElementText(system, "vssd:VirtualSystemType", self.virtual_system_type)
795 vcpus_item = ET.SubElement(hardware_section, "Item")
796 SubElementText(vcpus_item, "rasd:ElementName",
797 "%s virtual CPU(s)" % vcpus)
798 SubElementText(vcpus_item, "rasd:InstanceID", INSTANCE_ID["vcpus"])
799 SubElementText(vcpus_item, "rasd:ResourceType", RASD_TYPE["vcpus"])
800 SubElementText(vcpus_item, "rasd:VirtualQuantity", vcpus)
803 memory_item = ET.SubElement(hardware_section, "Item")
804 SubElementText(memory_item, "rasd:AllocationUnits", "byte * 2^20")
805 SubElementText(memory_item, "rasd:ElementName", "%sMB of memory" % memory)
806 SubElementText(memory_item, "rasd:InstanceID", INSTANCE_ID["memory"])
807 SubElementText(memory_item, "rasd:ResourceType", RASD_TYPE["memory"])
808 SubElementText(memory_item, "rasd:VirtualQuantity", memory)
810 # Item for scsi controller
811 scsi_item = ET.SubElement(hardware_section, "Item")
812 SubElementText(scsi_item, "rasd:Address", INSTANCE_ID["system"])
813 SubElementText(scsi_item, "rasd:ElementName", "scsi_controller0")
814 SubElementText(scsi_item, "rasd:InstanceID", INSTANCE_ID["scsi"])
815 SubElementText(scsi_item, "rasd:ResourceSubType", SCSI_SUBTYPE)
816 SubElementText(scsi_item, "rasd:ResourceType", RASD_TYPE["scsi-controller"])
818 # Other items - from self.hardware_list
819 for item in self.hardware_list:
820 hardware_section.append(item)
822 def PrettyXmlDump(self):
823 """Formatter of the XML file.
826 @return: XML tree in the form of nicely-formatted string
829 raw_string = ET.tostring(self.tree)
830 parsed_xml = xml.dom.minidom.parseString(raw_string)
831 xml_string = parsed_xml.toprettyxml(indent=" ")
832 text_re = re.compile(">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL)
833 return text_re.sub(">\g<1></", xml_string)
836 class Converter(object):
837 """Converter class for OVF packages.
839 Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is
840 to provide a common interface for the two.
842 @type options: optparse.Values
843 @ivar options: options parsed from the command line
844 @type output_dir: string
845 @ivar output_dir: directory to which the results of conversion shall be
847 @type temp_file_manager: L{utils.TemporaryFileManager}
848 @ivar temp_file_manager: container for temporary files created during
850 @type temp_dir: string
851 @ivar temp_dir: temporary directory created then we deal with OVA
854 def __init__(self, input_path, options):
855 """Initialize the converter.
857 @type input_path: string
858 @param input_path: path to the Converter input file
859 @type options: optparse.Values
860 @param options: command line options
862 @raise errors.OpPrereqError: if file does not exist
865 input_path = os.path.abspath(input_path)
866 if not os.path.isfile(input_path):
867 raise errors.OpPrereqError("File does not exist: %s" % input_path,
868 errors.ECODE_ENVIRON)
869 self.options = options
870 self.temp_file_manager = utils.TemporaryFileManager()
872 self.output_dir = None
873 self._ReadInputData(input_path)
875 def _ReadInputData(self, input_path):
876 """Reads the data on which the conversion will take place.
878 @type input_path: string
879 @param input_path: absolute path to the Converter input file
882 raise NotImplementedError()
884 def _CompressDisk(self, disk_path, compression, action):
885 """Performs (de)compression on the disk and returns the new path
887 @type disk_path: string
888 @param disk_path: path to the disk
889 @type compression: string
890 @param compression: compression type
892 @param action: whether the action is compression or decompression
894 @return: new disk path after (de)compression
896 @raise errors.OpPrereqError: disk (de)compression failed or "compression"
900 assert(action in ALLOWED_ACTIONS)
901 # For now we only support gzip, as it is used in ovftool
902 if compression != COMPRESSION_TYPE:
903 raise errors.OpPrereqError("Unsupported compression type: %s"
904 % compression, errors.ECODE_INVAL)
905 disk_file = os.path.basename(disk_path)
906 if action == DECOMPRESS:
907 (disk_name, _) = os.path.splitext(disk_file)
909 elif action == COMPRESS:
911 new_path = utils.GetClosedTempfile(suffix=COMPRESSION_EXT, prefix=prefix,
913 self.temp_file_manager.Add(new_path)
914 args = ["gzip", "-c", disk_path]
915 run_result = utils.RunCmd(args, output=new_path)
916 if run_result.failed:
917 raise errors.OpPrereqError("Disk %s failed with output: %s"
918 % (action, run_result.stderr),
919 errors.ECODE_ENVIRON)
920 logging.info("The %s of the disk is completed", action)
921 return (COMPRESSION_EXT, new_path)
923 def _ConvertDisk(self, disk_format, disk_path):
924 """Performes conversion to specified format.
926 @type disk_format: string
927 @param disk_format: format to which the disk should be converted
928 @type disk_path: string
929 @param disk_path: path to the disk that should be converted
931 @return path to the output disk
933 @raise errors.OpPrereqError: convertion of the disk failed
937 disk_file = os.path.basename(disk_path)
938 (disk_name, disk_extension) = os.path.splitext(disk_file)
939 if disk_extension != disk_format:
940 logging.warning("Conversion of disk image to %s format, this may take"
941 " a while", disk_format)
943 new_disk_path = utils.GetClosedTempfile(
944 suffix=".%s" % disk_format, prefix=disk_name, dir=self.output_dir)
945 self.temp_file_manager.Add(new_disk_path)
947 constants.QEMUIMG_PATH,
954 run_result = utils.RunCmd(args, cwd=os.getcwd())
955 if run_result.failed:
956 raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was"
957 ": %s" % (disk_format, run_result.stderr),
958 errors.ECODE_ENVIRON)
959 return (".%s" % disk_format, new_disk_path)
962 def _GetDiskQemuInfo(disk_path, regexp):
963 """Figures out some information of the disk using qemu-img.
965 @type disk_path: string
966 @param disk_path: path to the disk we want to know the format of
968 @param regexp: string that has to be matched, it has to contain one group
972 @raise errors.OpPrereqError: format information cannot be retrieved
976 args = [constants.QEMUIMG_PATH, "info", disk_path]
977 run_result = utils.RunCmd(args, cwd=os.getcwd())
978 if run_result.failed:
979 raise errors.OpPrereqError("Gathering info about the disk using qemu-img"
980 " failed, output was: %s" % run_result.stderr,
981 errors.ECODE_ENVIRON)
982 result = run_result.output
983 regexp = r"%s" % regexp
984 match = re.search(regexp, result)
986 disk_format = match.group(1)
988 raise errors.OpPrereqError("No file information matching %s found in:"
989 " %s" % (regexp, result),
990 errors.ECODE_ENVIRON)
994 """Parses the data and creates a structure containing all required info.
997 raise NotImplementedError()
1000 """Saves the gathered configuration in an apropriate format.
1003 raise NotImplementedError()
1006 """Cleans the temporary directory, if one was created.
1009 self.temp_file_manager.Cleanup()
1011 shutil.rmtree(self.temp_dir)
1012 self.temp_dir = None
1015 class OVFImporter(Converter):
1016 """Converter from OVF to Ganeti config file.
1018 @type input_dir: string
1019 @ivar input_dir: directory in which the .ovf file resides
1020 @type output_dir: string
1021 @ivar output_dir: directory to which the results of conversion shall be
1023 @type input_path: string
1024 @ivar input_path: complete path to the .ovf file
1025 @type ovf_reader: L{OVFReader}
1026 @ivar ovf_reader: OVF reader instance collects data from .ovf file
1027 @type results_name: string
1028 @ivar results_name: name of imported instance
1029 @type results_template: string
1030 @ivar results_template: disk template read from .ovf file or command line
1032 @type results_hypervisor: dict
1033 @ivar results_hypervisor: hypervisor information gathered from .ovf file or
1034 command line arguments
1035 @type results_os: dict
1036 @ivar results_os: operating system information gathered from .ovf file or
1037 command line arguments
1038 @type results_backend: dict
1039 @ivar results_backend: backend information gathered from .ovf file or
1040 command line arguments
1041 @type results_tags: string
1042 @ivar results_tags: string containing instance-specific tags
1043 @type results_version: string
1044 @ivar results_version: version as required by Ganeti import
1045 @type results_network: dict
1046 @ivar results_network: network information gathered from .ovf file or command
1048 @type results_disk: dict
1049 @ivar results_disk: disk information gathered from .ovf file or command line
1053 def _ReadInputData(self, input_path):
1054 """Reads the data on which the conversion will take place.
1056 @type input_path: string
1057 @param input_path: absolute path to the .ovf or .ova input file
1059 @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
1062 (input_dir, input_file) = os.path.split(input_path)
1063 (_, input_extension) = os.path.splitext(input_file)
1065 if input_extension == OVF_EXT:
1066 logging.info("%s file extension found, no unpacking necessary", OVF_EXT)
1067 self.input_dir = input_dir
1068 self.input_path = input_path
1069 self.temp_dir = None
1070 elif input_extension == OVA_EXT:
1071 logging.info("%s file extension found, proceeding to unpacking", OVA_EXT)
1072 self._UnpackOVA(input_path)
1074 raise errors.OpPrereqError("Unknown file extension; expected %s or %s"
1075 " file" % (OVA_EXT, OVF_EXT),
1077 assert ((input_extension == OVA_EXT and self.temp_dir) or
1078 (input_extension == OVF_EXT and not self.temp_dir))
1079 assert self.input_dir in self.input_path
1081 if self.options.output_dir:
1082 self.output_dir = os.path.abspath(self.options.output_dir)
1083 if (os.path.commonprefix([constants.EXPORT_DIR, self.output_dir]) !=
1084 constants.EXPORT_DIR):
1085 logging.warning("Export path is not under %s directory, import to"
1086 " Ganeti using gnt-backup may fail",
1087 constants.EXPORT_DIR)
1089 self.output_dir = constants.EXPORT_DIR
1091 self.ovf_reader = OVFReader(self.input_path)
1092 self.ovf_reader.VerifyManifest()
1094 def _UnpackOVA(self, input_path):
1095 """Unpacks the .ova package into temporary directory.
1097 @type input_path: string
1098 @param input_path: path to the .ova package file
1100 @raise errors.OpPrereqError: if file is not a proper tarball, one of the
1101 files in the archive seem malicious (e.g. path starts with '../') or
1102 .ova package does not contain .ovf file
1106 if not tarfile.is_tarfile(input_path):
1107 raise errors.OpPrereqError("The provided %s file is not a proper tar"
1108 " archive" % OVA_EXT, errors.ECODE_ENVIRON)
1109 ova_content = tarfile.open(input_path)
1110 temp_dir = tempfile.mkdtemp()
1111 self.temp_dir = temp_dir
1112 for file_name in ova_content.getnames():
1113 file_normname = os.path.normpath(file_name)
1115 utils.PathJoin(temp_dir, file_normname)
1116 except ValueError, err:
1117 raise errors.OpPrereqError("File %s inside %s package is not safe" %
1118 (file_name, OVA_EXT), errors.ECODE_ENVIRON)
1119 if file_name.endswith(OVF_EXT):
1120 input_name = file_name
1122 raise errors.OpPrereqError("No %s file in %s package found" %
1123 (OVF_EXT, OVA_EXT), errors.ECODE_ENVIRON)
1124 logging.warning("Unpacking the %s archive, this may take a while",
1126 self.input_dir = temp_dir
1127 self.input_path = utils.PathJoin(self.temp_dir, input_name)
1130 extract = ova_content.extractall
1131 except AttributeError:
1132 # This is a prehistorical case of using python < 2.5
1133 for member in ova_content.getmembers():
1134 ova_content.extract(member, path=self.temp_dir)
1136 extract(self.temp_dir)
1137 except tarfile.TarError, err:
1138 raise errors.OpPrereqError("Error while extracting %s archive: %s" %
1139 (OVA_EXT, err), errors.ECODE_ENVIRON)
1140 logging.info("OVA package extracted to %s directory", self.temp_dir)
1143 """Parses the data and creates a structure containing all required info.
1145 The method reads the information given either as a command line option or as
1146 a part of the OVF description.
1148 @raise errors.OpPrereqError: if some required part of the description of
1149 virtual instance is missing or unable to create output directory
1152 self.results_name = self._GetInfo("instance name", self.options.name,
1153 self._ParseNameOptions,
1154 self.ovf_reader.GetInstanceName)
1155 if not self.results_name:
1156 raise errors.OpPrereqError("Name of instance not provided",
1159 self.output_dir = utils.PathJoin(self.output_dir, self.results_name)
1161 utils.Makedirs(self.output_dir)
1162 except OSError, err:
1163 raise errors.OpPrereqError("Failed to create directory %s: %s" %
1164 (self.output_dir, err), errors.ECODE_ENVIRON)
1166 self.results_template = self._GetInfo(
1167 "disk template", self.options.disk_template, self._ParseTemplateOptions,
1168 self.ovf_reader.GetDiskTemplate)
1169 if not self.results_template:
1170 logging.info("Disk template not given")
1172 self.results_hypervisor = self._GetInfo(
1173 "hypervisor", self.options.hypervisor, self._ParseHypervisorOptions,
1174 self.ovf_reader.GetHypervisorData)
1175 assert self.results_hypervisor["hypervisor_name"]
1176 if self.results_hypervisor["hypervisor_name"] == constants.VALUE_AUTO:
1177 logging.debug("Default hypervisor settings from the cluster will be used")
1179 self.results_os = self._GetInfo(
1180 "OS", self.options.os, self._ParseOSOptions, self.ovf_reader.GetOSData)
1181 if not self.results_os.get("os_name"):
1182 raise errors.OpPrereqError("OS name must be provided",
1185 self.results_backend = self._GetInfo(
1186 "backend", self.options.beparams,
1187 self._ParseBackendOptions, self.ovf_reader.GetBackendData)
1188 assert self.results_backend.get("vcpus")
1189 assert self.results_backend.get("memory")
1190 assert self.results_backend.get("auto_balance") is not None
1192 self.results_tags = self._GetInfo(
1193 "tags", self.options.tags, self._ParseTags, self.ovf_reader.GetTagsData)
1195 ovf_version = self.ovf_reader.GetVersionData()
1197 self.results_version = ovf_version
1199 self.results_version = constants.EXPORT_VERSION
1201 self.results_network = self._GetInfo(
1202 "network", self.options.nics, self._ParseNicOptions,
1203 self.ovf_reader.GetNetworkData, ignore_test=self.options.no_nics)
1205 self.results_disk = self._GetInfo(
1206 "disk", self.options.disks, self._ParseDiskOptions, self._GetDiskInfo,
1207 ignore_test=self.results_template == constants.DT_DISKLESS)
1209 if not self.results_disk and not self.results_network:
1210 raise errors.OpPrereqError("Either disk specification or network"
1211 " description must be present",
1215 def _GetInfo(name, cmd_arg, cmd_function, nocmd_function,
1217 """Get information about some section - e.g. disk, network, hypervisor.
1220 @param name: name of the section
1222 @param cmd_arg: command line argument specific for section 'name'
1223 @type cmd_function: callable
1224 @param cmd_function: function to call if 'cmd_args' exists
1225 @type nocmd_function: callable
1226 @param nocmd_function: function to call if 'cmd_args' is not there
1230 logging.info("Information for %s will be ignored", name)
1233 logging.info("Information for %s will be parsed from command line", name)
1234 results = cmd_function()
1236 logging.info("Information for %s will be parsed from %s file",
1238 results = nocmd_function()
1239 logging.info("Options for %s were succesfully read", name)
1242 def _ParseNameOptions(self):
1243 """Returns name if one was given in command line.
1246 @return: name of an instance
1249 return self.options.name
1251 def _ParseTemplateOptions(self):
1252 """Returns disk template if one was given in command line.
1255 @return: disk template name
1258 return self.options.disk_template
1260 def _ParseHypervisorOptions(self):
1261 """Parses hypervisor options given in a command line.
1264 @return: dictionary containing name of the chosen hypervisor and all the
1268 assert type(self.options.hypervisor) is tuple
1269 assert len(self.options.hypervisor) == 2
1271 if self.options.hypervisor[0]:
1272 results["hypervisor_name"] = self.options.hypervisor[0]
1274 results["hypervisor_name"] = constants.VALUE_AUTO
1275 results.update(self.options.hypervisor[1])
1278 def _ParseOSOptions(self):
1279 """Parses OS options given in command line.
1282 @return: dictionary containing name of chosen OS and all its options
1285 assert self.options.os
1287 results["os_name"] = self.options.os
1288 results.update(self.options.osparams)
1291 def _ParseBackendOptions(self):
1292 """Parses backend options given in command line.
1295 @return: dictionary containing vcpus, memory and auto-balance options
1298 assert self.options.beparams
1300 backend.update(self.options.beparams)
1301 must_contain = ["vcpus", "memory", "auto_balance"]
1302 for element in must_contain:
1303 if backend.get(element) is None:
1304 backend[element] = constants.VALUE_AUTO
1307 def _ParseTags(self):
1308 """Returns tags list given in command line.
1311 @return: string containing comma-separated tags
1314 return self.options.tags
1316 def _ParseNicOptions(self):
1317 """Parses network options given in a command line or as a dictionary.
1320 @return: dictionary of network-related options
1323 assert self.options.nics
1325 for (nic_id, nic_desc) in self.options.nics:
1326 results["nic%s_mode" % nic_id] = \
1327 nic_desc.get("mode", constants.VALUE_AUTO)
1328 results["nic%s_mac" % nic_id] = nic_desc.get("mac", constants.VALUE_AUTO)
1329 results["nic%s_link" % nic_id] = \
1330 nic_desc.get("link", constants.VALUE_AUTO)
1331 if nic_desc.get("mode") == "bridged":
1332 results["nic%s_ip" % nic_id] = constants.VALUE_NONE
1334 results["nic%s_ip" % nic_id] = constants.VALUE_AUTO
1335 results["nic_count"] = str(len(self.options.nics))
1338 def _ParseDiskOptions(self):
1339 """Parses disk options given in a command line.
1342 @return: dictionary of disk-related options
1344 @raise errors.OpPrereqError: disk description does not contain size
1345 information or size information is invalid or creation failed
1349 assert self.options.disks
1351 for (disk_id, disk_desc) in self.options.disks:
1352 results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id
1353 if disk_desc.get("size"):
1355 disk_size = utils.ParseUnit(disk_desc["size"])
1357 raise errors.OpPrereqError("Invalid disk size for disk %s: %s" %
1358 (disk_id, disk_desc["size"]),
1360 new_path = utils.PathJoin(self.output_dir, str(disk_id))
1362 constants.QEMUIMG_PATH,
1369 run_result = utils.RunCmd(args)
1370 if run_result.failed:
1371 raise errors.OpPrereqError("Creation of disk %s failed, output was:"
1372 " %s" % (new_path, run_result.stderr),
1373 errors.ECODE_ENVIRON)
1374 results["disk%s_size" % disk_id] = str(disk_size)
1375 results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id
1377 raise errors.OpPrereqError("Disks created for import must have their"
1380 results["disk_count"] = str(len(self.options.disks))
1383 def _GetDiskInfo(self):
1384 """Gathers information about disks used by instance, perfomes conversion.
1387 @return: dictionary of disk-related options
1389 @raise errors.OpPrereqError: disk is not in the same directory as .ovf file
1393 disks_list = self.ovf_reader.GetDisksNames()
1394 for (counter, (disk_name, disk_compression)) in enumerate(disks_list):
1395 if os.path.dirname(disk_name):
1396 raise errors.OpPrereqError("Disks are not allowed to have absolute"
1397 " paths or paths outside main OVF"
1398 " directory", errors.ECODE_ENVIRON)
1399 disk, _ = os.path.splitext(disk_name)
1400 disk_path = utils.PathJoin(self.input_dir, disk_name)
1401 if disk_compression not in NO_COMPRESSION:
1402 _, disk_path = self._CompressDisk(disk_path, disk_compression,
1404 disk, _ = os.path.splitext(disk)
1405 if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw":
1406 logging.info("Conversion to raw format is required")
1407 ext, new_disk_path = self._ConvertDisk("raw", disk_path)
1409 final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext,
1410 directory=self.output_dir)
1411 final_name = os.path.basename(final_disk_path)
1412 disk_size = os.path.getsize(final_disk_path) / (1024 * 1024)
1413 results["disk%s_dump" % counter] = final_name
1414 results["disk%s_size" % counter] = str(disk_size)
1415 results["disk%s_ivname" % counter] = "disk/%s" % str(counter)
1417 results["disk_count"] = str(len(disks_list))
1421 """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
1423 @raise errors.OpPrereqError: when saving to config file failed
1426 logging.info("Conversion was succesfull, saving %s in %s directory",
1427 constants.EXPORT_CONF_FILE, self.output_dir)
1429 constants.INISECT_INS: {},
1430 constants.INISECT_BEP: {},
1431 constants.INISECT_EXP: {},
1432 constants.INISECT_OSP: {},
1433 constants.INISECT_HYP: {},
1436 results[constants.INISECT_INS].update(self.results_disk)
1437 results[constants.INISECT_INS].update(self.results_network)
1438 results[constants.INISECT_INS]["hypervisor"] = \
1439 self.results_hypervisor["hypervisor_name"]
1440 results[constants.INISECT_INS]["name"] = self.results_name
1441 if self.results_template:
1442 results[constants.INISECT_INS]["disk_template"] = self.results_template
1443 if self.results_tags:
1444 results[constants.INISECT_INS]["tags"] = self.results_tags
1446 results[constants.INISECT_BEP].update(self.results_backend)
1448 results[constants.INISECT_EXP]["os"] = self.results_os["os_name"]
1449 results[constants.INISECT_EXP]["version"] = self.results_version
1451 del self.results_os["os_name"]
1452 results[constants.INISECT_OSP].update(self.results_os)
1454 del self.results_hypervisor["hypervisor_name"]
1455 results[constants.INISECT_HYP].update(self.results_hypervisor)
1457 output_file_name = utils.PathJoin(self.output_dir,
1458 constants.EXPORT_CONF_FILE)
1461 for section, options in results.iteritems():
1462 output.append("[%s]" % section)
1463 for name, value in options.iteritems():
1466 output.append("%s = %s" % (name, value))
1468 output_contents = "\n".join(output)
1471 utils.WriteFile(output_file_name, data=output_contents)
1472 except errors.ProgrammerError, err:
1473 raise errors.OpPrereqError("Saving the config file failed: %s" % err,
1474 errors.ECODE_ENVIRON)
1479 class ConfigParserWithDefaults(ConfigParser.SafeConfigParser):
1480 """This is just a wrapper on SafeConfigParser, that uses default values
1483 def get(self, section, options, raw=None, vars=None): # pylint: disable=W0622
1485 result = ConfigParser.SafeConfigParser.get(self, section, options,
1487 except ConfigParser.NoOptionError:
1491 def getint(self, section, options):
1493 result = ConfigParser.SafeConfigParser.get(self, section, options)
1494 except ConfigParser.NoOptionError:
1499 class OVFExporter(Converter):
1500 """Converter from Ganeti config file to OVF
1502 @type input_dir: string
1503 @ivar input_dir: directory in which the config.ini file resides
1504 @type output_dir: string
1505 @ivar output_dir: directory to which the results of conversion shall be
1507 @type packed_dir: string
1508 @ivar packed_dir: if we want OVA package, this points to the real (i.e. not
1509 temp) output directory
1510 @type input_path: string
1511 @ivar input_path: complete path to the config.ini file
1512 @type output_path: string
1513 @ivar output_path: complete path to .ovf file
1514 @type config_parser: L{ConfigParserWithDefaults}
1515 @ivar config_parser: parser for the config.ini file
1516 @type reference_files: list
1517 @ivar reference_files: files referenced in the ovf file
1518 @type results_disk: list
1519 @ivar results_disk: list of dictionaries of disk options from config.ini
1520 @type results_network: list
1521 @ivar results_network: list of dictionaries of network options form config.ini
1522 @type results_name: string
1523 @ivar results_name: name of the instance
1524 @type results_vcpus: string
1525 @ivar results_vcpus: number of VCPUs
1526 @type results_memory: string
1527 @ivar results_memory: RAM memory in MB
1528 @type results_ganeti: dict
1529 @ivar results_ganeti: dictionary of Ganeti-specific options from config.ini
1532 def _ReadInputData(self, input_path):
1533 """Reads the data on which the conversion will take place.
1535 @type input_path: string
1536 @param input_path: absolute path to the config.ini input file
1538 @raise errors.OpPrereqError: error when reading the config file
1541 input_dir = os.path.dirname(input_path)
1542 self.input_path = input_path
1543 self.input_dir = input_dir
1544 if self.options.output_dir:
1545 self.output_dir = os.path.abspath(self.options.output_dir)
1547 self.output_dir = input_dir
1548 self.config_parser = ConfigParserWithDefaults()
1549 logging.info("Reading configuration from %s file", input_path)
1551 self.config_parser.read(input_path)
1552 except ConfigParser.MissingSectionHeaderError, err:
1553 raise errors.OpPrereqError("Error when trying to read %s: %s" %
1554 (input_path, err), errors.ECODE_ENVIRON)
1555 if self.options.ova_package:
1556 self.temp_dir = tempfile.mkdtemp()
1557 self.packed_dir = self.output_dir
1558 self.output_dir = self.temp_dir
1560 self.ovf_writer = OVFWriter(not self.options.ext_usage)
1562 def _ParseName(self):
1563 """Parses name from command line options or config file.
1566 @return: name of Ganeti instance
1568 @raise errors.OpPrereqError: if name of the instance is not provided
1571 if self.options.name:
1572 name = self.options.name
1574 name = self.config_parser.get(constants.INISECT_INS, NAME)
1576 raise errors.OpPrereqError("No instance name found",
1577 errors.ECODE_ENVIRON)
1580 def _ParseVCPUs(self):
1581 """Parses vcpus number from config file.
1584 @return: number of virtual CPUs
1586 @raise errors.OpPrereqError: if number of VCPUs equals 0
1589 vcpus = self.config_parser.getint(constants.INISECT_BEP, VCPUS)
1591 raise errors.OpPrereqError("No CPU information found",
1592 errors.ECODE_ENVIRON)
1595 def _ParseMemory(self):
1596 """Parses vcpus number from config file.
1599 @return: amount of memory in MB
1601 @raise errors.OpPrereqError: if amount of memory equals 0
1604 memory = self.config_parser.getint(constants.INISECT_BEP, MEMORY)
1606 raise errors.OpPrereqError("No memory information found",
1607 errors.ECODE_ENVIRON)
1610 def _ParseGaneti(self):
1611 """Parses Ganeti data from config file.
1614 @return: dictionary of Ganeti-specific options
1619 results["hypervisor"] = {}
1620 hyp_name = self.config_parser.get(constants.INISECT_INS, HYPERV)
1621 if hyp_name is None:
1622 raise errors.OpPrereqError("No hypervisor information found",
1623 errors.ECODE_ENVIRON)
1624 results["hypervisor"]["name"] = hyp_name
1625 pairs = self.config_parser.items(constants.INISECT_HYP)
1626 for (name, value) in pairs:
1627 results["hypervisor"][name] = value
1630 os_name = self.config_parser.get(constants.INISECT_EXP, OS)
1632 raise errors.OpPrereqError("No operating system information found",
1633 errors.ECODE_ENVIRON)
1634 results["os"]["name"] = os_name
1635 pairs = self.config_parser.items(constants.INISECT_OSP)
1636 for (name, value) in pairs:
1637 results["os"][name] = value
1640 (constants.INISECT_INS, DISK_TEMPLATE, "disk_template"),
1641 (constants.INISECT_BEP, AUTO_BALANCE, "auto_balance"),
1642 (constants.INISECT_INS, TAGS, "tags"),
1643 (constants.INISECT_EXP, VERSION, "version"),
1645 for (section, element, name) in others:
1646 results[name] = self.config_parser.get(section, element)
1649 def _ParseNetworks(self):
1650 """Parses network data from config file.
1653 @return: list of dictionaries of network options
1655 @raise errors.OpPrereqError: then network mode is not recognized
1662 self.config_parser.get(constants.INISECT_INS, "nic%s_link" % counter)
1663 if data_link is None:
1666 "mode": self.config_parser.get(constants.INISECT_INS,
1667 "nic%s_mode" % counter),
1668 "mac": self.config_parser.get(constants.INISECT_INS,
1669 "nic%s_mac" % counter),
1670 "ip": self.config_parser.get(constants.INISECT_INS,
1671 "nic%s_ip" % counter),
1674 if results[counter]["mode"] not in constants.NIC_VALID_MODES:
1675 raise errors.OpPrereqError("Network mode %s not recognized"
1676 % results[counter]["mode"],
1681 def _GetDiskOptions(self, disk_file, compression):
1682 """Convert the disk and gather disk info for .ovf file.
1684 @type disk_file: string
1685 @param disk_file: name of the disk (without the full path)
1686 @type compression: bool
1687 @param compression: whether the disk should be compressed or not
1689 @raise errors.OpPrereqError: when disk image does not exist
1692 disk_path = utils.PathJoin(self.input_dir, disk_file)
1694 if not os.path.isfile(disk_path):
1695 raise errors.OpPrereqError("Disk image does not exist: %s" % disk_path,
1696 errors.ECODE_ENVIRON)
1697 if os.path.dirname(disk_file):
1698 raise errors.OpPrereqError("Path for the disk: %s contains a directory"
1699 " name" % disk_path, errors.ECODE_ENVIRON)
1700 disk_name, _ = os.path.splitext(disk_file)
1701 ext, new_disk_path = self._ConvertDisk(self.options.disk_format, disk_path)
1702 results["format"] = self.options.disk_format
1703 results["virt-size"] = self._GetDiskQemuInfo(
1704 new_disk_path, "virtual size: \S+ \((\d+) bytes\)")
1706 ext2, new_disk_path = self._CompressDisk(new_disk_path, "gzip",
1708 disk_name, _ = os.path.splitext(disk_name)
1709 results["compression"] = "gzip"
1711 final_disk_path = LinkFile(new_disk_path, prefix=disk_name, suffix=ext,
1712 directory=self.output_dir)
1713 final_disk_name = os.path.basename(final_disk_path)
1714 results["real-size"] = os.path.getsize(final_disk_path)
1715 results["path"] = final_disk_name
1716 self.references_files.append(final_disk_path)
1719 def _ParseDisks(self):
1720 """Parses disk data from config file.
1723 @return: list of dictionaries of disk options
1730 self.config_parser.get(constants.INISECT_INS, "disk%s_dump" % counter)
1731 if disk_file is None:
1733 results.append(self._GetDiskOptions(disk_file, self.options.compression))
1738 """Parses the data and creates a structure containing all required info.
1742 utils.Makedirs(self.output_dir)
1743 except OSError, err:
1744 raise errors.OpPrereqError("Failed to create directory %s: %s" %
1745 (self.output_dir, err), errors.ECODE_ENVIRON)
1747 self.references_files = []
1748 self.results_name = self._ParseName()
1749 self.results_vcpus = self._ParseVCPUs()
1750 self.results_memory = self._ParseMemory()
1751 if not self.options.ext_usage:
1752 self.results_ganeti = self._ParseGaneti()
1753 self.results_network = self._ParseNetworks()
1754 self.results_disk = self._ParseDisks()
1756 def _PrepareManifest(self, path):
1757 """Creates manifest for all the files in OVF package.
1760 @param path: path to manifesto file
1762 @raise errors.OpPrereqError: if error occurs when writing file
1765 logging.info("Preparing manifest for the OVF package")
1767 files_list = [self.output_path]
1768 files_list.extend(self.references_files)
1769 logging.warning("Calculating SHA1 checksums, this may take a while")
1770 sha1_sums = utils.FingerprintFiles(files_list)
1771 for file_path, value in sha1_sums.iteritems():
1772 file_name = os.path.basename(file_path)
1773 lines.append("SHA1(%s)= %s" % (file_name, value))
1775 data = "\n".join(lines)
1777 utils.WriteFile(path, data=data)
1778 except errors.ProgrammerError, err:
1779 raise errors.OpPrereqError("Saving the manifest file failed: %s" % err,
1780 errors.ECODE_ENVIRON)
1783 def _PrepareTarFile(tar_path, files_list):
1784 """Creates tarfile from the files in OVF package.
1786 @type tar_path: string
1787 @param tar_path: path to the resulting file
1788 @type files_list: list
1789 @param files_list: list of files in the OVF package
1792 logging.info("Preparing tarball for the OVF package")
1793 open(tar_path, mode="w").close()
1794 ova_package = tarfile.open(name=tar_path, mode="w")
1795 for file_path in files_list:
1796 file_name = os.path.basename(file_path)
1797 ova_package.add(file_path, arcname=file_name)
1801 """Saves the gathered configuration in an apropriate format.
1803 @raise errors.OpPrereqError: if unable to create output directory
1806 output_file = "%s%s" % (self.results_name, OVF_EXT)
1807 output_path = utils.PathJoin(self.output_dir, output_file)
1808 self.ovf_writer = OVFWriter(not self.options.ext_usage)
1809 logging.info("Saving read data to %s", output_path)
1811 self.output_path = utils.PathJoin(self.output_dir, output_file)
1812 files_list = [self.output_path]
1814 self.ovf_writer.SaveDisksData(self.results_disk)
1815 self.ovf_writer.SaveNetworksData(self.results_network)
1816 if not self.options.ext_usage:
1817 self.ovf_writer.SaveGanetiData(self.results_ganeti, self.results_network)
1819 self.ovf_writer.SaveVirtualSystemData(self.results_name, self.results_vcpus,
1820 self.results_memory)
1822 data = self.ovf_writer.PrettyXmlDump()
1823 utils.WriteFile(self.output_path, data=data)
1825 manifest_file = "%s%s" % (self.results_name, MF_EXT)
1826 manifest_path = utils.PathJoin(self.output_dir, manifest_file)
1827 self._PrepareManifest(manifest_path)
1828 files_list.append(manifest_path)
1830 files_list.extend(self.references_files)
1832 if self.options.ova_package:
1833 ova_file = "%s%s" % (self.results_name, OVA_EXT)
1834 packed_path = utils.PathJoin(self.packed_dir, ova_file)
1836 utils.Makedirs(self.packed_dir)
1837 except OSError, err:
1838 raise errors.OpPrereqError("Failed to create directory %s: %s" %
1839 (self.packed_dir, err),
1840 errors.ECODE_ENVIRON)
1841 self._PrepareTarFile(packed_path, files_list)
1842 logging.info("Creation of the OVF package was successfull")