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
56 from ganeti import pathutils
59 # Schemas used in OVF format
60 GANETI_SCHEMA = "http://ganeti"
61 OVF_SCHEMA = "http://schemas.dmtf.org/ovf/envelope/1"
62 RASD_SCHEMA = ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/"
63 "CIM_ResourceAllocationSettingData")
64 VSSD_SCHEMA = ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/"
65 "CIM_VirtualSystemSettingData")
66 XML_SCHEMA = "http://www.w3.org/2001/XMLSchema-instance"
68 # File extensions in OVF package
73 COMPRESSION_EXT = ".gz"
80 COMPRESSION_TYPE = "gzip"
81 NO_COMPRESSION = [None, "identity"]
82 COMPRESS = "compression"
83 DECOMPRESS = "decompression"
84 ALLOWED_ACTIONS = [COMPRESS, DECOMPRESS]
89 ALLOWED_FORMATS = [RAW, COW, VMDK]
95 "scsi-controller": "6",
96 "ethernet-adapter": "10",
100 SCSI_SUBTYPE = "lsilogic"
102 "ganeti": "ganeti-ovf",
103 "external": "vmx-04",
106 # AllocationUnits values and conversion
109 "kb": ["kilobytes", "kb", "byte * 2^10", "kibibytes", "kib"],
110 "mb": ["megabytes", "mb", "byte * 2^20", "mebibytes", "mib"],
111 "gb": ["gigabytes", "gb", "byte * 2^30", "gibibytes", "gib"],
113 CONVERT_UNITS_TO_MB = {
114 "b": lambda x: x / (1024 * 1024),
115 "kb": lambda x: x / 1024,
117 "gb": lambda x: x * 1024,
120 # Names of the config fields
123 HYPERV = "hypervisor"
126 AUTO_BALANCE = "auto_balance"
127 DISK_TEMPLATE = "disk_template"
131 # Instance IDs of System and SCSI controller
139 # Disk format descriptions
141 RAW: "http://en.wikipedia.org/wiki/Byte",
142 VMDK: "http://www.vmware.com/interfaces/specifications/vmdk.html"
144 COW: "http://www.gnome.org/~markmc/qcow-image-format.html",
149 """ Make sure that qemu-img is present before performing operations.
151 @raise errors.OpPrereqError: when qemu-img was not found in the system
154 if not constants.QEMUIMG_PATH:
155 raise errors.OpPrereqError("qemu-img not found at build time, unable"
156 " to continue", errors.ECODE_STATE)
159 def LinkFile(old_path, prefix=None, suffix=None, directory=None):
160 """Create link with a given prefix and suffix.
162 This is a wrapper over os.link. It tries to create a hard link for given file,
163 but instead of rising error when file exists, the function changes the name
166 @type old_path:string
167 @param old_path: path to the file that is to be linked
169 @param prefix: prefix of filename for the link
171 @param suffix: suffix of the filename for the link
172 @type directory: string
173 @param directory: directory of the link
175 @raise errors.OpPrereqError: when error on linking is different than
179 assert(prefix is not None or suffix is not None)
180 if directory is None:
181 directory = os.getcwd()
182 new_path = utils.PathJoin(directory, "%s%s" % (prefix, suffix))
186 os.link(old_path, new_path)
189 if err.errno == errno.EEXIST:
190 new_path = utils.PathJoin(directory,
191 "%s_%s%s" % (prefix, counter, suffix))
194 raise errors.OpPrereqError("Error moving the file %s to %s location:"
195 " %s" % (old_path, new_path, err),
196 errors.ECODE_ENVIRON)
200 class OVFReader(object):
201 """Reader class for OVF files.
203 @type files_list: list
204 @ivar files_list: list of files in the OVF package
205 @type tree: ET.ElementTree
206 @ivar tree: XML tree of the .ovf file
207 @type schema_name: string
208 @ivar schema_name: name of the .ovf file
209 @type input_dir: string
210 @ivar input_dir: directory in which the .ovf file resides
213 def __init__(self, input_path):
214 """Initialiaze the reader - load the .ovf file to XML parser.
216 It is assumed that names of manifesto (.mf), certificate (.cert) and ovf
217 files are the same. In order to account any other files as part of the ovf
218 package, they have to be explicitly mentioned in the Resources section
221 @type input_path: string
222 @param input_path: absolute path to the .ovf file
224 @raise errors.OpPrereqError: when .ovf file is not a proper XML file or some
225 of the files mentioned in Resources section do not exist
228 self.tree = ET.ElementTree()
230 self.tree.parse(input_path)
231 except (ParseError, xml.parsers.expat.ExpatError), err:
232 raise errors.OpPrereqError("Error while reading %s file: %s" %
233 (OVF_EXT, err), errors.ECODE_ENVIRON)
235 # Create a list of all files in the OVF package
236 (input_dir, input_file) = os.path.split(input_path)
237 (input_name, _) = os.path.splitext(input_file)
238 files_directory = utils.ListVisibleFiles(input_dir)
240 for file_name in files_directory:
241 (name, extension) = os.path.splitext(file_name)
242 if extension in FILE_EXTENSIONS and name == input_name:
243 files_list.append(file_name)
244 files_list += self._GetAttributes("{%s}References/{%s}File" %
245 (OVF_SCHEMA, OVF_SCHEMA),
246 "{%s}href" % OVF_SCHEMA)
247 for file_name in files_list:
248 file_path = utils.PathJoin(input_dir, file_name)
249 if not os.path.exists(file_path):
250 raise errors.OpPrereqError("File does not exist: %s" % file_path,
251 errors.ECODE_ENVIRON)
252 logging.info("Files in the OVF package: %s", " ".join(files_list))
253 self.files_list = files_list
254 self.input_dir = input_dir
255 self.schema_name = input_name
257 def _GetAttributes(self, path, attribute):
258 """Get specified attribute from all nodes accessible using given path.
260 Function follows the path from root node to the desired tags using path,
261 then reads the apropriate attribute values.
264 @param path: path of nodes to visit
265 @type attribute: string
266 @param attribute: attribute for which we gather the information
268 @return: for each accessible tag with the attribute value set, value of the
272 current_list = self.tree.findall(path)
273 results = [x.get(attribute) for x in current_list]
274 return filter(None, results)
276 def _GetElementMatchingAttr(self, path, match_attr):
277 """Searches for element on a path that matches certain attribute value.
279 Function follows the path from root node to the desired tags using path,
280 then searches for the first one matching the attribute value.
283 @param path: path of nodes to visit
284 @type match_attr: tuple
285 @param match_attr: pair (attribute, value) for which we search
286 @rtype: ET.ElementTree or None
287 @return: first element matching match_attr or None if nothing matches
290 potential_elements = self.tree.findall(path)
291 (attr, val) = match_attr
292 for elem in potential_elements:
293 if elem.get(attr) == val:
297 def _GetElementMatchingText(self, path, match_text):
298 """Searches for element on a path that matches certain text value.
300 Function follows the path from root node to the desired tags using path,
301 then searches for the first one matching the text value.
304 @param path: path of nodes to visit
305 @type match_text: tuple
306 @param match_text: pair (node, text) for which we search
307 @rtype: ET.ElementTree or None
308 @return: first element matching match_text or None if nothing matches
311 potential_elements = self.tree.findall(path)
312 (node, text) = match_text
313 for elem in potential_elements:
314 if elem.findtext(node) == text:
319 def _GetDictParameters(root, schema):
320 """Reads text in all children and creates the dictionary from the contents.
322 @type root: ET.ElementTree or None
323 @param root: father of the nodes we want to collect data about
325 @param schema: schema name to be removed from the tag
327 @return: dictionary containing tags and their text contents, tags have their
328 schema fragment removed or empty dictionary, when root is None
334 for element in list(root):
335 pref_len = len("{%s}" % schema)
336 assert(schema in element.tag)
337 tag = element.tag[pref_len:]
338 results[tag] = element.text
341 def VerifyManifest(self):
342 """Verifies manifest for the OVF package, if one is given.
344 @raise errors.OpPrereqError: if SHA1 checksums do not match
347 if "%s%s" % (self.schema_name, MF_EXT) in self.files_list:
348 logging.warning("Verifying SHA1 checksums, this may take a while")
349 manifest_filename = "%s%s" % (self.schema_name, MF_EXT)
350 manifest_path = utils.PathJoin(self.input_dir, manifest_filename)
351 manifest_content = utils.ReadFile(manifest_path).splitlines()
353 regexp = r"SHA1\((\S+)\)= (\S+)"
354 for line in manifest_content:
355 match = re.match(regexp, line)
357 file_name = match.group(1)
358 sha1_sum = match.group(2)
359 manifest_files[file_name] = sha1_sum
360 files_with_paths = [utils.PathJoin(self.input_dir, file_name)
361 for file_name in self.files_list]
362 sha1_sums = utils.FingerprintFiles(files_with_paths)
363 for file_name, value in manifest_files.iteritems():
364 if sha1_sums.get(utils.PathJoin(self.input_dir, file_name)) != value:
365 raise errors.OpPrereqError("SHA1 checksum of %s does not match the"
366 " value in manifest file" % file_name,
367 errors.ECODE_ENVIRON)
368 logging.info("SHA1 checksums verified")
370 def GetInstanceName(self):
371 """Provides information about instance name.
374 @return: instance name string
377 find_name = "{%s}VirtualSystem/{%s}Name" % (OVF_SCHEMA, OVF_SCHEMA)
378 return self.tree.findtext(find_name)
380 def GetDiskTemplate(self):
381 """Returns disk template from .ovf file
383 @rtype: string or None
384 @return: name of the template
386 find_template = ("{%s}GanetiSection/{%s}DiskTemplate" %
387 (GANETI_SCHEMA, GANETI_SCHEMA))
388 return self.tree.findtext(find_template)
390 def GetHypervisorData(self):
391 """Provides hypervisor information - hypervisor name and options.
394 @return: dictionary containing name of the used hypervisor and all the
398 hypervisor_search = ("{%s}GanetiSection/{%s}Hypervisor" %
399 (GANETI_SCHEMA, GANETI_SCHEMA))
400 hypervisor_data = self.tree.find(hypervisor_search)
401 if not hypervisor_data:
402 return {"hypervisor_name": constants.VALUE_AUTO}
404 "hypervisor_name": hypervisor_data.findtext("{%s}Name" % GANETI_SCHEMA,
405 default=constants.VALUE_AUTO),
407 parameters = hypervisor_data.find("{%s}Parameters" % GANETI_SCHEMA)
408 results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
412 """ Provides operating system information - os name and options.
415 @return: dictionary containing name and options for the chosen OS
419 os_search = ("{%s}GanetiSection/{%s}OperatingSystem" %
420 (GANETI_SCHEMA, GANETI_SCHEMA))
421 os_data = self.tree.find(os_search)
423 results["os_name"] = os_data.findtext("{%s}Name" % GANETI_SCHEMA)
424 parameters = os_data.find("{%s}Parameters" % GANETI_SCHEMA)
425 results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
428 def GetBackendData(self):
429 """ Provides backend information - vcpus, memory, auto balancing options.
432 @return: dictionary containing options for vcpus, memory and auto balance
438 find_vcpus = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item" %
439 (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
440 match_vcpus = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["vcpus"])
441 vcpus = self._GetElementMatchingText(find_vcpus, match_vcpus)
443 vcpus_count = vcpus.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
444 default=constants.VALUE_AUTO)
446 vcpus_count = constants.VALUE_AUTO
447 results["vcpus"] = str(vcpus_count)
449 find_memory = find_vcpus
450 match_memory = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["memory"])
451 memory = self._GetElementMatchingText(find_memory, match_memory)
454 alloc_units = memory.findtext("{%s}AllocationUnits" % RASD_SCHEMA)
455 matching_units = [units for units, variants in ALLOCATION_UNITS.items()
456 if alloc_units.lower() in variants]
457 if matching_units == []:
458 raise errors.OpPrereqError("Unit %s for RAM memory unknown" %
459 alloc_units, errors.ECODE_INVAL)
460 units = matching_units[0]
461 memory_raw = int(memory.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
462 default=constants.VALUE_AUTO))
463 memory_count = CONVERT_UNITS_TO_MB[units](memory_raw)
465 memory_count = constants.VALUE_AUTO
466 results["memory"] = str(memory_count)
468 find_balance = ("{%s}GanetiSection/{%s}AutoBalance" %
469 (GANETI_SCHEMA, GANETI_SCHEMA))
470 balance = self.tree.findtext(find_balance, default=constants.VALUE_AUTO)
471 results["auto_balance"] = balance
475 def GetTagsData(self):
476 """Provides tags information for instance.
478 @rtype: string or None
479 @return: string of comma-separated tags for the instance
482 find_tags = "{%s}GanetiSection/{%s}Tags" % (GANETI_SCHEMA, GANETI_SCHEMA)
483 results = self.tree.findtext(find_tags)
489 def GetVersionData(self):
490 """Provides version number read from .ovf file
493 @return: string containing the version number
496 find_version = ("{%s}GanetiSection/{%s}Version" %
497 (GANETI_SCHEMA, GANETI_SCHEMA))
498 return self.tree.findtext(find_version)
500 def GetNetworkData(self):
501 """Provides data about the network in the OVF instance.
503 The method gathers the data about networks used by OVF instance. It assumes
504 that 'name' tag means something - in essence, if it contains one of the
505 words 'bridged' or 'routed' then that will be the mode of this network in
506 Ganeti. The information about the network can be either in GanetiSection or
507 VirtualHardwareSection.
510 @return: dictionary containing all the network information
514 networks_search = ("{%s}NetworkSection/{%s}Network" %
515 (OVF_SCHEMA, OVF_SCHEMA))
516 network_names = self._GetAttributes(networks_search,
517 "{%s}name" % OVF_SCHEMA)
518 required = ["ip", "mac", "link", "mode"]
519 for (counter, network_name) in enumerate(network_names):
520 network_search = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item"
521 % (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
522 ganeti_search = ("{%s}GanetiSection/{%s}Network/{%s}Nic" %
523 (GANETI_SCHEMA, GANETI_SCHEMA, GANETI_SCHEMA))
524 network_match = ("{%s}Connection" % RASD_SCHEMA, network_name)
525 ganeti_match = ("{%s}name" % OVF_SCHEMA, network_name)
526 network_data = self._GetElementMatchingText(network_search, network_match)
527 network_ganeti_data = self._GetElementMatchingAttr(ganeti_search,
531 if network_ganeti_data:
532 ganeti_data["mode"] = network_ganeti_data.findtext("{%s}Mode" %
534 ganeti_data["mac"] = network_ganeti_data.findtext("{%s}MACAddress" %
536 ganeti_data["ip"] = network_ganeti_data.findtext("{%s}IPAddress" %
538 ganeti_data["link"] = network_ganeti_data.findtext("{%s}Link" %
542 mac_data = network_data.findtext("{%s}Address" % RASD_SCHEMA)
544 network_name = network_name.lower()
546 # First, some not Ganeti-specific information is collected
547 if constants.NIC_MODE_BRIDGED in network_name:
548 results["nic%s_mode" % counter] = "bridged"
549 elif constants.NIC_MODE_ROUTED in network_name:
550 results["nic%s_mode" % counter] = "routed"
551 results["nic%s_mac" % counter] = mac_data
553 # GanetiSection data overrides 'manually' collected data
554 for name, value in ganeti_data.iteritems():
555 results["nic%s_%s" % (counter, name)] = value
557 # Bridged network has no IP - unless specifically stated otherwise
558 if (results.get("nic%s_mode" % counter) == "bridged" and
559 not results.get("nic%s_ip" % counter)):
560 results["nic%s_ip" % counter] = constants.VALUE_NONE
562 for option in required:
563 if not results.get("nic%s_%s" % (counter, option)):
564 results["nic%s_%s" % (counter, option)] = constants.VALUE_AUTO
567 results["nic_count"] = str(len(network_names))
570 def GetDisksNames(self):
571 """Provides list of file names for the disks used by the instance.
574 @return: list of file names, as referenced in .ovf file
578 disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA)
579 disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA)
580 for disk in disk_ids:
581 disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA)
582 disk_match = ("{%s}id" % OVF_SCHEMA, disk)
583 disk_elem = self._GetElementMatchingAttr(disk_search, disk_match)
584 if disk_elem is None:
585 raise errors.OpPrereqError("%s file corrupted - disk %s not found in"
586 " references" % (OVF_EXT, disk),
587 errors.ECODE_ENVIRON)
588 disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA)
589 disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA)
590 results.append((disk_name, disk_compression))
594 def SubElementText(parent, tag, text, attrib={}, **extra):
595 # pylint: disable=W0102
596 """This is just a wrapper on ET.SubElement that always has text content.
601 elem = ET.SubElement(parent, tag, attrib=attrib, **extra)
602 elem.text = str(text)
606 class OVFWriter(object):
607 """Writer class for OVF files.
609 @type tree: ET.ElementTree
610 @ivar tree: XML tree that we are constructing
611 @type virtual_system_type: string
612 @ivar virtual_system_type: value of vssd:VirtualSystemType, for external usage
613 in VMWare this requires to be vmx
614 @type hardware_list: list
615 @ivar hardware_list: list of items prepared for VirtualHardwareSection
616 @type next_instance_id: int
617 @ivar next_instance_id: next instance id to be used when creating elements on
621 def __init__(self, has_gnt_section):
622 """Initialize the writer - set the top element.
624 @type has_gnt_section: bool
625 @param has_gnt_section: if the Ganeti schema should be added - i.e. this
626 means that Ganeti section will be present
630 "xmlns:xsi": XML_SCHEMA,
631 "xmlns:vssd": VSSD_SCHEMA,
632 "xmlns:rasd": RASD_SCHEMA,
633 "xmlns:ovf": OVF_SCHEMA,
638 env_attribs["xmlns:gnt"] = GANETI_SCHEMA
639 self.virtual_system_type = VS_TYPE["ganeti"]
641 self.virtual_system_type = VS_TYPE["external"]
642 self.tree = ET.Element("Envelope", attrib=env_attribs)
643 self.hardware_list = []
644 # INSTANCE_ID contains statically assigned IDs, starting from 0
645 self.next_instance_id = len(INSTANCE_ID) # FIXME: hackish
647 def SaveDisksData(self, disks):
648 """Convert disk information to certain OVF sections.
651 @param disks: list of dictionaries of disk options from config.ini
654 references = ET.SubElement(self.tree, "References")
655 disk_section = ET.SubElement(self.tree, "DiskSection")
656 SubElementText(disk_section, "Info", "Virtual disk information")
657 for counter, disk in enumerate(disks):
658 file_id = "file%s" % counter
659 disk_id = "disk%s" % counter
661 "ovf:href": disk["path"],
662 "ovf:size": str(disk["real-size"]),
666 "ovf:capacity": str(disk["virt-size"]),
667 "ovf:diskId": disk_id,
668 "ovf:fileRef": file_id,
669 "ovf:format": DISK_FORMAT.get(disk["format"], disk["format"]),
671 if "compression" in disk:
672 file_attribs["ovf:compression"] = disk["compression"]
673 ET.SubElement(references, "File", attrib=file_attribs)
674 ET.SubElement(disk_section, "Disk", attrib=disk_attribs)
676 # Item in VirtualHardwareSection creation
677 disk_item = ET.Element("Item")
678 SubElementText(disk_item, "rasd:ElementName", disk_id)
679 SubElementText(disk_item, "rasd:HostResource", "ovf:/disk/%s" % disk_id)
680 SubElementText(disk_item, "rasd:InstanceID", self.next_instance_id)
681 SubElementText(disk_item, "rasd:Parent", INSTANCE_ID["scsi"])
682 SubElementText(disk_item, "rasd:ResourceType", RASD_TYPE["disk"])
683 self.hardware_list.append(disk_item)
684 self.next_instance_id += 1
686 def SaveNetworksData(self, networks):
687 """Convert network information to NetworkSection.
690 @param networks: list of dictionaries of network options form config.ini
693 network_section = ET.SubElement(self.tree, "NetworkSection")
694 SubElementText(network_section, "Info", "List of logical networks")
695 for counter, network in enumerate(networks):
696 network_name = "%s%s" % (network["mode"], counter)
697 network_attrib = {"ovf:name": network_name}
698 ET.SubElement(network_section, "Network", attrib=network_attrib)
700 # Item in VirtualHardwareSection creation
701 network_item = ET.Element("Item")
702 SubElementText(network_item, "rasd:Address", network["mac"])
703 SubElementText(network_item, "rasd:Connection", network_name)
704 SubElementText(network_item, "rasd:ElementName", network_name)
705 SubElementText(network_item, "rasd:InstanceID", self.next_instance_id)
706 SubElementText(network_item, "rasd:ResourceType",
707 RASD_TYPE["ethernet-adapter"])
708 self.hardware_list.append(network_item)
709 self.next_instance_id += 1
712 def _SaveNameAndParams(root, data):
713 """Save name and parameters information under root using data.
715 @type root: ET.Element
716 @param root: root element for the Name and Parameters
718 @param data: data from which we gather the values
721 assert(data.get("name"))
722 name = SubElementText(root, "gnt:Name", data["name"])
723 params = ET.SubElement(root, "gnt:Parameters")
724 for name, value in data.iteritems():
726 SubElementText(params, "gnt:%s" % name, value)
728 def SaveGanetiData(self, ganeti, networks):
729 """Convert Ganeti-specific information to GanetiSection.
732 @param ganeti: dictionary of Ganeti-specific options from config.ini
734 @param networks: list of dictionaries of network options form config.ini
737 ganeti_section = ET.SubElement(self.tree, "gnt:GanetiSection")
739 SubElementText(ganeti_section, "gnt:Version", ganeti.get("version"))
740 SubElementText(ganeti_section, "gnt:DiskTemplate",
741 ganeti.get("disk_template"))
742 SubElementText(ganeti_section, "gnt:AutoBalance",
743 ganeti.get("auto_balance"))
744 SubElementText(ganeti_section, "gnt:Tags", ganeti.get("tags"))
746 osys = ET.SubElement(ganeti_section, "gnt:OperatingSystem")
747 self._SaveNameAndParams(osys, ganeti["os"])
749 hypervisor = ET.SubElement(ganeti_section, "gnt:Hypervisor")
750 self._SaveNameAndParams(hypervisor, ganeti["hypervisor"])
752 network_section = ET.SubElement(ganeti_section, "gnt:Network")
753 for counter, network in enumerate(networks):
754 network_name = "%s%s" % (network["mode"], counter)
755 nic_attrib = {"ovf:name": network_name}
756 nic = ET.SubElement(network_section, "gnt:Nic", attrib=nic_attrib)
757 SubElementText(nic, "gnt:Mode", network["mode"])
758 SubElementText(nic, "gnt:MACAddress", network["mac"])
759 SubElementText(nic, "gnt:IPAddress", network["ip"])
760 SubElementText(nic, "gnt:Link", network["link"])
762 def SaveVirtualSystemData(self, name, vcpus, memory):
763 """Convert virtual system information to OVF sections.
766 @param name: name of the instance
768 @param vcpus: number of VCPUs
770 @param memory: RAM memory in MB
775 vs_attrib = {"ovf:id": name}
776 virtual_system = ET.SubElement(self.tree, "VirtualSystem", attrib=vs_attrib)
777 SubElementText(virtual_system, "Info", "A virtual machine")
779 name_section = ET.SubElement(virtual_system, "Name")
780 name_section.text = name
781 os_attrib = {"ovf:id": "0"}
782 os_section = ET.SubElement(virtual_system, "OperatingSystemSection",
784 SubElementText(os_section, "Info", "Installed guest operating system")
785 hardware_section = ET.SubElement(virtual_system, "VirtualHardwareSection")
786 SubElementText(hardware_section, "Info", "Virtual hardware requirements")
789 system = ET.SubElement(hardware_section, "System")
790 SubElementText(system, "vssd:ElementName", "Virtual Hardware Family")
791 SubElementText(system, "vssd:InstanceID", INSTANCE_ID["system"])
792 SubElementText(system, "vssd:VirtualSystemIdentifier", name)
793 SubElementText(system, "vssd:VirtualSystemType", self.virtual_system_type)
796 vcpus_item = ET.SubElement(hardware_section, "Item")
797 SubElementText(vcpus_item, "rasd:ElementName",
798 "%s virtual CPU(s)" % vcpus)
799 SubElementText(vcpus_item, "rasd:InstanceID", INSTANCE_ID["vcpus"])
800 SubElementText(vcpus_item, "rasd:ResourceType", RASD_TYPE["vcpus"])
801 SubElementText(vcpus_item, "rasd:VirtualQuantity", vcpus)
804 memory_item = ET.SubElement(hardware_section, "Item")
805 SubElementText(memory_item, "rasd:AllocationUnits", "byte * 2^20")
806 SubElementText(memory_item, "rasd:ElementName", "%sMB of memory" % memory)
807 SubElementText(memory_item, "rasd:InstanceID", INSTANCE_ID["memory"])
808 SubElementText(memory_item, "rasd:ResourceType", RASD_TYPE["memory"])
809 SubElementText(memory_item, "rasd:VirtualQuantity", memory)
811 # Item for scsi controller
812 scsi_item = ET.SubElement(hardware_section, "Item")
813 SubElementText(scsi_item, "rasd:Address", INSTANCE_ID["system"])
814 SubElementText(scsi_item, "rasd:ElementName", "scsi_controller0")
815 SubElementText(scsi_item, "rasd:InstanceID", INSTANCE_ID["scsi"])
816 SubElementText(scsi_item, "rasd:ResourceSubType", SCSI_SUBTYPE)
817 SubElementText(scsi_item, "rasd:ResourceType", RASD_TYPE["scsi-controller"])
819 # Other items - from self.hardware_list
820 for item in self.hardware_list:
821 hardware_section.append(item)
823 def PrettyXmlDump(self):
824 """Formatter of the XML file.
827 @return: XML tree in the form of nicely-formatted string
830 raw_string = ET.tostring(self.tree)
831 parsed_xml = xml.dom.minidom.parseString(raw_string)
832 xml_string = parsed_xml.toprettyxml(indent=" ")
833 text_re = re.compile(">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL)
834 return text_re.sub(">\g<1></", xml_string)
837 class Converter(object):
838 """Converter class for OVF packages.
840 Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is
841 to provide a common interface for the two.
843 @type options: optparse.Values
844 @ivar options: options parsed from the command line
845 @type output_dir: string
846 @ivar output_dir: directory to which the results of conversion shall be
848 @type temp_file_manager: L{utils.TemporaryFileManager}
849 @ivar temp_file_manager: container for temporary files created during
851 @type temp_dir: string
852 @ivar temp_dir: temporary directory created then we deal with OVA
855 def __init__(self, input_path, options):
856 """Initialize the converter.
858 @type input_path: string
859 @param input_path: path to the Converter input file
860 @type options: optparse.Values
861 @param options: command line options
863 @raise errors.OpPrereqError: if file does not exist
866 input_path = os.path.abspath(input_path)
867 if not os.path.isfile(input_path):
868 raise errors.OpPrereqError("File does not exist: %s" % input_path,
869 errors.ECODE_ENVIRON)
870 self.options = options
871 self.temp_file_manager = utils.TemporaryFileManager()
873 self.output_dir = None
874 self._ReadInputData(input_path)
876 def _ReadInputData(self, input_path):
877 """Reads the data on which the conversion will take place.
879 @type input_path: string
880 @param input_path: absolute path to the Converter input file
883 raise NotImplementedError()
885 def _CompressDisk(self, disk_path, compression, action):
886 """Performs (de)compression on the disk and returns the new path
888 @type disk_path: string
889 @param disk_path: path to the disk
890 @type compression: string
891 @param compression: compression type
893 @param action: whether the action is compression or decompression
895 @return: new disk path after (de)compression
897 @raise errors.OpPrereqError: disk (de)compression failed or "compression"
901 assert(action in ALLOWED_ACTIONS)
902 # For now we only support gzip, as it is used in ovftool
903 if compression != COMPRESSION_TYPE:
904 raise errors.OpPrereqError("Unsupported compression type: %s"
905 % compression, errors.ECODE_INVAL)
906 disk_file = os.path.basename(disk_path)
907 if action == DECOMPRESS:
908 (disk_name, _) = os.path.splitext(disk_file)
910 elif action == COMPRESS:
912 new_path = utils.GetClosedTempfile(suffix=COMPRESSION_EXT, prefix=prefix,
914 self.temp_file_manager.Add(new_path)
915 args = ["gzip", "-c", disk_path]
916 run_result = utils.RunCmd(args, output=new_path)
917 if run_result.failed:
918 raise errors.OpPrereqError("Disk %s failed with output: %s"
919 % (action, run_result.stderr),
920 errors.ECODE_ENVIRON)
921 logging.info("The %s of the disk is completed", action)
922 return (COMPRESSION_EXT, new_path)
924 def _ConvertDisk(self, disk_format, disk_path):
925 """Performes conversion to specified format.
927 @type disk_format: string
928 @param disk_format: format to which the disk should be converted
929 @type disk_path: string
930 @param disk_path: path to the disk that should be converted
932 @return path to the output disk
934 @raise errors.OpPrereqError: convertion of the disk failed
938 disk_file = os.path.basename(disk_path)
939 (disk_name, disk_extension) = os.path.splitext(disk_file)
940 if disk_extension != disk_format:
941 logging.warning("Conversion of disk image to %s format, this may take"
942 " a while", disk_format)
944 new_disk_path = utils.GetClosedTempfile(
945 suffix=".%s" % disk_format, prefix=disk_name, dir=self.output_dir)
946 self.temp_file_manager.Add(new_disk_path)
948 constants.QEMUIMG_PATH,
955 run_result = utils.RunCmd(args, cwd=os.getcwd())
956 if run_result.failed:
957 raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was"
958 ": %s" % (disk_format, run_result.stderr),
959 errors.ECODE_ENVIRON)
960 return (".%s" % disk_format, new_disk_path)
963 def _GetDiskQemuInfo(disk_path, regexp):
964 """Figures out some information of the disk using qemu-img.
966 @type disk_path: string
967 @param disk_path: path to the disk we want to know the format of
969 @param regexp: string that has to be matched, it has to contain one group
973 @raise errors.OpPrereqError: format information cannot be retrieved
977 args = [constants.QEMUIMG_PATH, "info", disk_path]
978 run_result = utils.RunCmd(args, cwd=os.getcwd())
979 if run_result.failed:
980 raise errors.OpPrereqError("Gathering info about the disk using qemu-img"
981 " failed, output was: %s" % run_result.stderr,
982 errors.ECODE_ENVIRON)
983 result = run_result.output
984 regexp = r"%s" % regexp
985 match = re.search(regexp, result)
987 disk_format = match.group(1)
989 raise errors.OpPrereqError("No file information matching %s found in:"
990 " %s" % (regexp, result),
991 errors.ECODE_ENVIRON)
995 """Parses the data and creates a structure containing all required info.
998 raise NotImplementedError()
1001 """Saves the gathered configuration in an apropriate format.
1004 raise NotImplementedError()
1007 """Cleans the temporary directory, if one was created.
1010 self.temp_file_manager.Cleanup()
1012 shutil.rmtree(self.temp_dir)
1013 self.temp_dir = None
1016 class OVFImporter(Converter):
1017 """Converter from OVF to Ganeti config file.
1019 @type input_dir: string
1020 @ivar input_dir: directory in which the .ovf file resides
1021 @type output_dir: string
1022 @ivar output_dir: directory to which the results of conversion shall be
1024 @type input_path: string
1025 @ivar input_path: complete path to the .ovf file
1026 @type ovf_reader: L{OVFReader}
1027 @ivar ovf_reader: OVF reader instance collects data from .ovf file
1028 @type results_name: string
1029 @ivar results_name: name of imported instance
1030 @type results_template: string
1031 @ivar results_template: disk template read from .ovf file or command line
1033 @type results_hypervisor: dict
1034 @ivar results_hypervisor: hypervisor information gathered from .ovf file or
1035 command line arguments
1036 @type results_os: dict
1037 @ivar results_os: operating system information gathered from .ovf file or
1038 command line arguments
1039 @type results_backend: dict
1040 @ivar results_backend: backend information gathered from .ovf file or
1041 command line arguments
1042 @type results_tags: string
1043 @ivar results_tags: string containing instance-specific tags
1044 @type results_version: string
1045 @ivar results_version: version as required by Ganeti import
1046 @type results_network: dict
1047 @ivar results_network: network information gathered from .ovf file or command
1049 @type results_disk: dict
1050 @ivar results_disk: disk information gathered from .ovf file or command line
1054 def _ReadInputData(self, input_path):
1055 """Reads the data on which the conversion will take place.
1057 @type input_path: string
1058 @param input_path: absolute path to the .ovf or .ova input file
1060 @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
1063 (input_dir, input_file) = os.path.split(input_path)
1064 (_, input_extension) = os.path.splitext(input_file)
1066 if input_extension == OVF_EXT:
1067 logging.info("%s file extension found, no unpacking necessary", OVF_EXT)
1068 self.input_dir = input_dir
1069 self.input_path = input_path
1070 self.temp_dir = None
1071 elif input_extension == OVA_EXT:
1072 logging.info("%s file extension found, proceeding to unpacking", OVA_EXT)
1073 self._UnpackOVA(input_path)
1075 raise errors.OpPrereqError("Unknown file extension; expected %s or %s"
1076 " file" % (OVA_EXT, OVF_EXT),
1078 assert ((input_extension == OVA_EXT and self.temp_dir) or
1079 (input_extension == OVF_EXT and not self.temp_dir))
1080 assert self.input_dir in self.input_path
1082 if self.options.output_dir:
1083 self.output_dir = os.path.abspath(self.options.output_dir)
1084 if (os.path.commonprefix([pathutils.EXPORT_DIR, self.output_dir]) !=
1085 pathutils.EXPORT_DIR):
1086 logging.warning("Export path is not under %s directory, import to"
1087 " Ganeti using gnt-backup may fail",
1088 pathutils.EXPORT_DIR)
1090 self.output_dir = pathutils.EXPORT_DIR
1092 self.ovf_reader = OVFReader(self.input_path)
1093 self.ovf_reader.VerifyManifest()
1095 def _UnpackOVA(self, input_path):
1096 """Unpacks the .ova package into temporary directory.
1098 @type input_path: string
1099 @param input_path: path to the .ova package file
1101 @raise errors.OpPrereqError: if file is not a proper tarball, one of the
1102 files in the archive seem malicious (e.g. path starts with '../') or
1103 .ova package does not contain .ovf file
1107 if not tarfile.is_tarfile(input_path):
1108 raise errors.OpPrereqError("The provided %s file is not a proper tar"
1109 " archive" % OVA_EXT, errors.ECODE_ENVIRON)
1110 ova_content = tarfile.open(input_path)
1111 temp_dir = tempfile.mkdtemp()
1112 self.temp_dir = temp_dir
1113 for file_name in ova_content.getnames():
1114 file_normname = os.path.normpath(file_name)
1116 utils.PathJoin(temp_dir, file_normname)
1117 except ValueError, err:
1118 raise errors.OpPrereqError("File %s inside %s package is not safe" %
1119 (file_name, OVA_EXT), errors.ECODE_ENVIRON)
1120 if file_name.endswith(OVF_EXT):
1121 input_name = file_name
1123 raise errors.OpPrereqError("No %s file in %s package found" %
1124 (OVF_EXT, OVA_EXT), errors.ECODE_ENVIRON)
1125 logging.warning("Unpacking the %s archive, this may take a while",
1127 self.input_dir = temp_dir
1128 self.input_path = utils.PathJoin(self.temp_dir, input_name)
1131 extract = ova_content.extractall
1132 except AttributeError:
1133 # This is a prehistorical case of using python < 2.5
1134 for member in ova_content.getmembers():
1135 ova_content.extract(member, path=self.temp_dir)
1137 extract(self.temp_dir)
1138 except tarfile.TarError, err:
1139 raise errors.OpPrereqError("Error while extracting %s archive: %s" %
1140 (OVA_EXT, err), errors.ECODE_ENVIRON)
1141 logging.info("OVA package extracted to %s directory", self.temp_dir)
1144 """Parses the data and creates a structure containing all required info.
1146 The method reads the information given either as a command line option or as
1147 a part of the OVF description.
1149 @raise errors.OpPrereqError: if some required part of the description of
1150 virtual instance is missing or unable to create output directory
1153 self.results_name = self._GetInfo("instance name", self.options.name,
1154 self._ParseNameOptions,
1155 self.ovf_reader.GetInstanceName)
1156 if not self.results_name:
1157 raise errors.OpPrereqError("Name of instance not provided",
1160 self.output_dir = utils.PathJoin(self.output_dir, self.results_name)
1162 utils.Makedirs(self.output_dir)
1163 except OSError, err:
1164 raise errors.OpPrereqError("Failed to create directory %s: %s" %
1165 (self.output_dir, err), errors.ECODE_ENVIRON)
1167 self.results_template = self._GetInfo(
1168 "disk template", self.options.disk_template, self._ParseTemplateOptions,
1169 self.ovf_reader.GetDiskTemplate)
1170 if not self.results_template:
1171 logging.info("Disk template not given")
1173 self.results_hypervisor = self._GetInfo(
1174 "hypervisor", self.options.hypervisor, self._ParseHypervisorOptions,
1175 self.ovf_reader.GetHypervisorData)
1176 assert self.results_hypervisor["hypervisor_name"]
1177 if self.results_hypervisor["hypervisor_name"] == constants.VALUE_AUTO:
1178 logging.debug("Default hypervisor settings from the cluster will be used")
1180 self.results_os = self._GetInfo(
1181 "OS", self.options.os, self._ParseOSOptions, self.ovf_reader.GetOSData)
1182 if not self.results_os.get("os_name"):
1183 raise errors.OpPrereqError("OS name must be provided",
1186 self.results_backend = self._GetInfo(
1187 "backend", self.options.beparams,
1188 self._ParseBackendOptions, self.ovf_reader.GetBackendData)
1189 assert self.results_backend.get("vcpus")
1190 assert self.results_backend.get("memory")
1191 assert self.results_backend.get("auto_balance") is not None
1193 self.results_tags = self._GetInfo(
1194 "tags", self.options.tags, self._ParseTags, self.ovf_reader.GetTagsData)
1196 ovf_version = self.ovf_reader.GetVersionData()
1198 self.results_version = ovf_version
1200 self.results_version = constants.EXPORT_VERSION
1202 self.results_network = self._GetInfo(
1203 "network", self.options.nics, self._ParseNicOptions,
1204 self.ovf_reader.GetNetworkData, ignore_test=self.options.no_nics)
1206 self.results_disk = self._GetInfo(
1207 "disk", self.options.disks, self._ParseDiskOptions, self._GetDiskInfo,
1208 ignore_test=self.results_template == constants.DT_DISKLESS)
1210 if not self.results_disk and not self.results_network:
1211 raise errors.OpPrereqError("Either disk specification or network"
1212 " description must be present",
1216 def _GetInfo(name, cmd_arg, cmd_function, nocmd_function,
1218 """Get information about some section - e.g. disk, network, hypervisor.
1221 @param name: name of the section
1223 @param cmd_arg: command line argument specific for section 'name'
1224 @type cmd_function: callable
1225 @param cmd_function: function to call if 'cmd_args' exists
1226 @type nocmd_function: callable
1227 @param nocmd_function: function to call if 'cmd_args' is not there
1231 logging.info("Information for %s will be ignored", name)
1234 logging.info("Information for %s will be parsed from command line", name)
1235 results = cmd_function()
1237 logging.info("Information for %s will be parsed from %s file",
1239 results = nocmd_function()
1240 logging.info("Options for %s were succesfully read", name)
1243 def _ParseNameOptions(self):
1244 """Returns name if one was given in command line.
1247 @return: name of an instance
1250 return self.options.name
1252 def _ParseTemplateOptions(self):
1253 """Returns disk template if one was given in command line.
1256 @return: disk template name
1259 return self.options.disk_template
1261 def _ParseHypervisorOptions(self):
1262 """Parses hypervisor options given in a command line.
1265 @return: dictionary containing name of the chosen hypervisor and all the
1269 assert type(self.options.hypervisor) is tuple
1270 assert len(self.options.hypervisor) == 2
1272 if self.options.hypervisor[0]:
1273 results["hypervisor_name"] = self.options.hypervisor[0]
1275 results["hypervisor_name"] = constants.VALUE_AUTO
1276 results.update(self.options.hypervisor[1])
1279 def _ParseOSOptions(self):
1280 """Parses OS options given in command line.
1283 @return: dictionary containing name of chosen OS and all its options
1286 assert self.options.os
1288 results["os_name"] = self.options.os
1289 results.update(self.options.osparams)
1292 def _ParseBackendOptions(self):
1293 """Parses backend options given in command line.
1296 @return: dictionary containing vcpus, memory and auto-balance options
1299 assert self.options.beparams
1301 backend.update(self.options.beparams)
1302 must_contain = ["vcpus", "memory", "auto_balance"]
1303 for element in must_contain:
1304 if backend.get(element) is None:
1305 backend[element] = constants.VALUE_AUTO
1308 def _ParseTags(self):
1309 """Returns tags list given in command line.
1312 @return: string containing comma-separated tags
1315 return self.options.tags
1317 def _ParseNicOptions(self):
1318 """Parses network options given in a command line or as a dictionary.
1321 @return: dictionary of network-related options
1324 assert self.options.nics
1326 for (nic_id, nic_desc) in self.options.nics:
1327 results["nic%s_mode" % nic_id] = \
1328 nic_desc.get("mode", constants.VALUE_AUTO)
1329 results["nic%s_mac" % nic_id] = nic_desc.get("mac", constants.VALUE_AUTO)
1330 results["nic%s_link" % nic_id] = \
1331 nic_desc.get("link", constants.VALUE_AUTO)
1332 if nic_desc.get("mode") == "bridged":
1333 results["nic%s_ip" % nic_id] = constants.VALUE_NONE
1335 results["nic%s_ip" % nic_id] = constants.VALUE_AUTO
1336 results["nic_count"] = str(len(self.options.nics))
1339 def _ParseDiskOptions(self):
1340 """Parses disk options given in a command line.
1343 @return: dictionary of disk-related options
1345 @raise errors.OpPrereqError: disk description does not contain size
1346 information or size information is invalid or creation failed
1350 assert self.options.disks
1352 for (disk_id, disk_desc) in self.options.disks:
1353 results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id
1354 if disk_desc.get("size"):
1356 disk_size = utils.ParseUnit(disk_desc["size"])
1358 raise errors.OpPrereqError("Invalid disk size for disk %s: %s" %
1359 (disk_id, disk_desc["size"]),
1361 new_path = utils.PathJoin(self.output_dir, str(disk_id))
1363 constants.QEMUIMG_PATH,
1370 run_result = utils.RunCmd(args)
1371 if run_result.failed:
1372 raise errors.OpPrereqError("Creation of disk %s failed, output was:"
1373 " %s" % (new_path, run_result.stderr),
1374 errors.ECODE_ENVIRON)
1375 results["disk%s_size" % disk_id] = str(disk_size)
1376 results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id
1378 raise errors.OpPrereqError("Disks created for import must have their"
1381 results["disk_count"] = str(len(self.options.disks))
1384 def _GetDiskInfo(self):
1385 """Gathers information about disks used by instance, perfomes conversion.
1388 @return: dictionary of disk-related options
1390 @raise errors.OpPrereqError: disk is not in the same directory as .ovf file
1394 disks_list = self.ovf_reader.GetDisksNames()
1395 for (counter, (disk_name, disk_compression)) in enumerate(disks_list):
1396 if os.path.dirname(disk_name):
1397 raise errors.OpPrereqError("Disks are not allowed to have absolute"
1398 " paths or paths outside main OVF"
1399 " directory", errors.ECODE_ENVIRON)
1400 disk, _ = os.path.splitext(disk_name)
1401 disk_path = utils.PathJoin(self.input_dir, disk_name)
1402 if disk_compression not in NO_COMPRESSION:
1403 _, disk_path = self._CompressDisk(disk_path, disk_compression,
1405 disk, _ = os.path.splitext(disk)
1406 if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw":
1407 logging.info("Conversion to raw format is required")
1408 ext, new_disk_path = self._ConvertDisk("raw", disk_path)
1410 final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext,
1411 directory=self.output_dir)
1412 final_name = os.path.basename(final_disk_path)
1413 disk_size = os.path.getsize(final_disk_path) / (1024 * 1024)
1414 results["disk%s_dump" % counter] = final_name
1415 results["disk%s_size" % counter] = str(disk_size)
1416 results["disk%s_ivname" % counter] = "disk/%s" % str(counter)
1418 results["disk_count"] = str(len(disks_list))
1422 """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
1424 @raise errors.OpPrereqError: when saving to config file failed
1427 logging.info("Conversion was succesfull, saving %s in %s directory",
1428 constants.EXPORT_CONF_FILE, self.output_dir)
1430 constants.INISECT_INS: {},
1431 constants.INISECT_BEP: {},
1432 constants.INISECT_EXP: {},
1433 constants.INISECT_OSP: {},
1434 constants.INISECT_HYP: {},
1437 results[constants.INISECT_INS].update(self.results_disk)
1438 results[constants.INISECT_INS].update(self.results_network)
1439 results[constants.INISECT_INS]["hypervisor"] = \
1440 self.results_hypervisor["hypervisor_name"]
1441 results[constants.INISECT_INS]["name"] = self.results_name
1442 if self.results_template:
1443 results[constants.INISECT_INS]["disk_template"] = self.results_template
1444 if self.results_tags:
1445 results[constants.INISECT_INS]["tags"] = self.results_tags
1447 results[constants.INISECT_BEP].update(self.results_backend)
1449 results[constants.INISECT_EXP]["os"] = self.results_os["os_name"]
1450 results[constants.INISECT_EXP]["version"] = self.results_version
1452 del self.results_os["os_name"]
1453 results[constants.INISECT_OSP].update(self.results_os)
1455 del self.results_hypervisor["hypervisor_name"]
1456 results[constants.INISECT_HYP].update(self.results_hypervisor)
1458 output_file_name = utils.PathJoin(self.output_dir,
1459 constants.EXPORT_CONF_FILE)
1462 for section, options in results.iteritems():
1463 output.append("[%s]" % section)
1464 for name, value in options.iteritems():
1467 output.append("%s = %s" % (name, value))
1469 output_contents = "\n".join(output)
1472 utils.WriteFile(output_file_name, data=output_contents)
1473 except errors.ProgrammerError, err:
1474 raise errors.OpPrereqError("Saving the config file failed: %s" % err,
1475 errors.ECODE_ENVIRON)
1480 class ConfigParserWithDefaults(ConfigParser.SafeConfigParser):
1481 """This is just a wrapper on SafeConfigParser, that uses default values
1484 def get(self, section, options, raw=None, vars=None): # pylint: disable=W0622
1486 result = ConfigParser.SafeConfigParser.get(self, section, options,
1488 except ConfigParser.NoOptionError:
1492 def getint(self, section, options):
1494 result = ConfigParser.SafeConfigParser.get(self, section, options)
1495 except ConfigParser.NoOptionError:
1500 class OVFExporter(Converter):
1501 """Converter from Ganeti config file to OVF
1503 @type input_dir: string
1504 @ivar input_dir: directory in which the config.ini file resides
1505 @type output_dir: string
1506 @ivar output_dir: directory to which the results of conversion shall be
1508 @type packed_dir: string
1509 @ivar packed_dir: if we want OVA package, this points to the real (i.e. not
1510 temp) output directory
1511 @type input_path: string
1512 @ivar input_path: complete path to the config.ini file
1513 @type output_path: string
1514 @ivar output_path: complete path to .ovf file
1515 @type config_parser: L{ConfigParserWithDefaults}
1516 @ivar config_parser: parser for the config.ini file
1517 @type reference_files: list
1518 @ivar reference_files: files referenced in the ovf file
1519 @type results_disk: list
1520 @ivar results_disk: list of dictionaries of disk options from config.ini
1521 @type results_network: list
1522 @ivar results_network: list of dictionaries of network options form config.ini
1523 @type results_name: string
1524 @ivar results_name: name of the instance
1525 @type results_vcpus: string
1526 @ivar results_vcpus: number of VCPUs
1527 @type results_memory: string
1528 @ivar results_memory: RAM memory in MB
1529 @type results_ganeti: dict
1530 @ivar results_ganeti: dictionary of Ganeti-specific options from config.ini
1533 def _ReadInputData(self, input_path):
1534 """Reads the data on which the conversion will take place.
1536 @type input_path: string
1537 @param input_path: absolute path to the config.ini input file
1539 @raise errors.OpPrereqError: error when reading the config file
1542 input_dir = os.path.dirname(input_path)
1543 self.input_path = input_path
1544 self.input_dir = input_dir
1545 if self.options.output_dir:
1546 self.output_dir = os.path.abspath(self.options.output_dir)
1548 self.output_dir = input_dir
1549 self.config_parser = ConfigParserWithDefaults()
1550 logging.info("Reading configuration from %s file", input_path)
1552 self.config_parser.read(input_path)
1553 except ConfigParser.MissingSectionHeaderError, err:
1554 raise errors.OpPrereqError("Error when trying to read %s: %s" %
1555 (input_path, err), errors.ECODE_ENVIRON)
1556 if self.options.ova_package:
1557 self.temp_dir = tempfile.mkdtemp()
1558 self.packed_dir = self.output_dir
1559 self.output_dir = self.temp_dir
1561 self.ovf_writer = OVFWriter(not self.options.ext_usage)
1563 def _ParseName(self):
1564 """Parses name from command line options or config file.
1567 @return: name of Ganeti instance
1569 @raise errors.OpPrereqError: if name of the instance is not provided
1572 if self.options.name:
1573 name = self.options.name
1575 name = self.config_parser.get(constants.INISECT_INS, NAME)
1577 raise errors.OpPrereqError("No instance name found",
1578 errors.ECODE_ENVIRON)
1581 def _ParseVCPUs(self):
1582 """Parses vcpus number from config file.
1585 @return: number of virtual CPUs
1587 @raise errors.OpPrereqError: if number of VCPUs equals 0
1590 vcpus = self.config_parser.getint(constants.INISECT_BEP, VCPUS)
1592 raise errors.OpPrereqError("No CPU information found",
1593 errors.ECODE_ENVIRON)
1596 def _ParseMemory(self):
1597 """Parses vcpus number from config file.
1600 @return: amount of memory in MB
1602 @raise errors.OpPrereqError: if amount of memory equals 0
1605 memory = self.config_parser.getint(constants.INISECT_BEP, MEMORY)
1607 raise errors.OpPrereqError("No memory information found",
1608 errors.ECODE_ENVIRON)
1611 def _ParseGaneti(self):
1612 """Parses Ganeti data from config file.
1615 @return: dictionary of Ganeti-specific options
1620 results["hypervisor"] = {}
1621 hyp_name = self.config_parser.get(constants.INISECT_INS, HYPERV)
1622 if hyp_name is None:
1623 raise errors.OpPrereqError("No hypervisor information found",
1624 errors.ECODE_ENVIRON)
1625 results["hypervisor"]["name"] = hyp_name
1626 pairs = self.config_parser.items(constants.INISECT_HYP)
1627 for (name, value) in pairs:
1628 results["hypervisor"][name] = value
1631 os_name = self.config_parser.get(constants.INISECT_EXP, OS)
1633 raise errors.OpPrereqError("No operating system information found",
1634 errors.ECODE_ENVIRON)
1635 results["os"]["name"] = os_name
1636 pairs = self.config_parser.items(constants.INISECT_OSP)
1637 for (name, value) in pairs:
1638 results["os"][name] = value
1641 (constants.INISECT_INS, DISK_TEMPLATE, "disk_template"),
1642 (constants.INISECT_BEP, AUTO_BALANCE, "auto_balance"),
1643 (constants.INISECT_INS, TAGS, "tags"),
1644 (constants.INISECT_EXP, VERSION, "version"),
1646 for (section, element, name) in others:
1647 results[name] = self.config_parser.get(section, element)
1650 def _ParseNetworks(self):
1651 """Parses network data from config file.
1654 @return: list of dictionaries of network options
1656 @raise errors.OpPrereqError: then network mode is not recognized
1663 self.config_parser.get(constants.INISECT_INS, "nic%s_link" % counter)
1664 if data_link is None:
1667 "mode": self.config_parser.get(constants.INISECT_INS,
1668 "nic%s_mode" % counter),
1669 "mac": self.config_parser.get(constants.INISECT_INS,
1670 "nic%s_mac" % counter),
1671 "ip": self.config_parser.get(constants.INISECT_INS,
1672 "nic%s_ip" % counter),
1675 if results[counter]["mode"] not in constants.NIC_VALID_MODES:
1676 raise errors.OpPrereqError("Network mode %s not recognized"
1677 % results[counter]["mode"],
1682 def _GetDiskOptions(self, disk_file, compression):
1683 """Convert the disk and gather disk info for .ovf file.
1685 @type disk_file: string
1686 @param disk_file: name of the disk (without the full path)
1687 @type compression: bool
1688 @param compression: whether the disk should be compressed or not
1690 @raise errors.OpPrereqError: when disk image does not exist
1693 disk_path = utils.PathJoin(self.input_dir, disk_file)
1695 if not os.path.isfile(disk_path):
1696 raise errors.OpPrereqError("Disk image does not exist: %s" % disk_path,
1697 errors.ECODE_ENVIRON)
1698 if os.path.dirname(disk_file):
1699 raise errors.OpPrereqError("Path for the disk: %s contains a directory"
1700 " name" % disk_path, errors.ECODE_ENVIRON)
1701 disk_name, _ = os.path.splitext(disk_file)
1702 ext, new_disk_path = self._ConvertDisk(self.options.disk_format, disk_path)
1703 results["format"] = self.options.disk_format
1704 results["virt-size"] = self._GetDiskQemuInfo(
1705 new_disk_path, "virtual size: \S+ \((\d+) bytes\)")
1707 ext2, new_disk_path = self._CompressDisk(new_disk_path, "gzip",
1709 disk_name, _ = os.path.splitext(disk_name)
1710 results["compression"] = "gzip"
1712 final_disk_path = LinkFile(new_disk_path, prefix=disk_name, suffix=ext,
1713 directory=self.output_dir)
1714 final_disk_name = os.path.basename(final_disk_path)
1715 results["real-size"] = os.path.getsize(final_disk_path)
1716 results["path"] = final_disk_name
1717 self.references_files.append(final_disk_path)
1720 def _ParseDisks(self):
1721 """Parses disk data from config file.
1724 @return: list of dictionaries of disk options
1731 self.config_parser.get(constants.INISECT_INS, "disk%s_dump" % counter)
1732 if disk_file is None:
1734 results.append(self._GetDiskOptions(disk_file, self.options.compression))
1739 """Parses the data and creates a structure containing all required info.
1743 utils.Makedirs(self.output_dir)
1744 except OSError, err:
1745 raise errors.OpPrereqError("Failed to create directory %s: %s" %
1746 (self.output_dir, err), errors.ECODE_ENVIRON)
1748 self.references_files = []
1749 self.results_name = self._ParseName()
1750 self.results_vcpus = self._ParseVCPUs()
1751 self.results_memory = self._ParseMemory()
1752 if not self.options.ext_usage:
1753 self.results_ganeti = self._ParseGaneti()
1754 self.results_network = self._ParseNetworks()
1755 self.results_disk = self._ParseDisks()
1757 def _PrepareManifest(self, path):
1758 """Creates manifest for all the files in OVF package.
1761 @param path: path to manifesto file
1763 @raise errors.OpPrereqError: if error occurs when writing file
1766 logging.info("Preparing manifest for the OVF package")
1768 files_list = [self.output_path]
1769 files_list.extend(self.references_files)
1770 logging.warning("Calculating SHA1 checksums, this may take a while")
1771 sha1_sums = utils.FingerprintFiles(files_list)
1772 for file_path, value in sha1_sums.iteritems():
1773 file_name = os.path.basename(file_path)
1774 lines.append("SHA1(%s)= %s" % (file_name, value))
1776 data = "\n".join(lines)
1778 utils.WriteFile(path, data=data)
1779 except errors.ProgrammerError, err:
1780 raise errors.OpPrereqError("Saving the manifest file failed: %s" % err,
1781 errors.ECODE_ENVIRON)
1784 def _PrepareTarFile(tar_path, files_list):
1785 """Creates tarfile from the files in OVF package.
1787 @type tar_path: string
1788 @param tar_path: path to the resulting file
1789 @type files_list: list
1790 @param files_list: list of files in the OVF package
1793 logging.info("Preparing tarball for the OVF package")
1794 open(tar_path, mode="w").close()
1795 ova_package = tarfile.open(name=tar_path, mode="w")
1796 for file_path in files_list:
1797 file_name = os.path.basename(file_path)
1798 ova_package.add(file_path, arcname=file_name)
1802 """Saves the gathered configuration in an apropriate format.
1804 @raise errors.OpPrereqError: if unable to create output directory
1807 output_file = "%s%s" % (self.results_name, OVF_EXT)
1808 output_path = utils.PathJoin(self.output_dir, output_file)
1809 self.ovf_writer = OVFWriter(not self.options.ext_usage)
1810 logging.info("Saving read data to %s", output_path)
1812 self.output_path = utils.PathJoin(self.output_dir, output_file)
1813 files_list = [self.output_path]
1815 self.ovf_writer.SaveDisksData(self.results_disk)
1816 self.ovf_writer.SaveNetworksData(self.results_network)
1817 if not self.options.ext_usage:
1818 self.ovf_writer.SaveGanetiData(self.results_ganeti, self.results_network)
1820 self.ovf_writer.SaveVirtualSystemData(self.results_name, self.results_vcpus,
1821 self.results_memory)
1823 data = self.ovf_writer.PrettyXmlDump()
1824 utils.WriteFile(self.output_path, data=data)
1826 manifest_file = "%s%s" % (self.results_name, MF_EXT)
1827 manifest_path = utils.PathJoin(self.output_dir, manifest_file)
1828 self._PrepareManifest(manifest_path)
1829 files_list.append(manifest_path)
1831 files_list.extend(self.references_files)
1833 if self.options.ova_package:
1834 ova_file = "%s%s" % (self.results_name, OVA_EXT)
1835 packed_path = utils.PathJoin(self.packed_dir, ova_file)
1837 utils.Makedirs(self.packed_dir)
1838 except OSError, err:
1839 raise errors.OpPrereqError("Failed to create directory %s: %s" %
1840 (self.packed_dir, err),
1841 errors.ECODE_ENVIRON)
1842 self._PrepareTarFile(packed_path, files_list)
1843 logging.info("Creation of the OVF package was successfull")