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", "network"]
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" %
540 ganeti_data["network"] = network_ganeti_data.findtext("{%s}Net" %
544 mac_data = network_data.findtext("{%s}Address" % RASD_SCHEMA)
546 network_name = network_name.lower()
548 # First, some not Ganeti-specific information is collected
549 if constants.NIC_MODE_BRIDGED in network_name:
550 results["nic%s_mode" % counter] = "bridged"
551 elif constants.NIC_MODE_ROUTED in network_name:
552 results["nic%s_mode" % counter] = "routed"
553 results["nic%s_mac" % counter] = mac_data
555 # GanetiSection data overrides 'manually' collected data
556 for name, value in ganeti_data.iteritems():
557 results["nic%s_%s" % (counter, name)] = value
559 # Bridged network has no IP - unless specifically stated otherwise
560 if (results.get("nic%s_mode" % counter) == "bridged" and
561 not results.get("nic%s_ip" % counter)):
562 results["nic%s_ip" % counter] = constants.VALUE_NONE
564 for option in required:
565 if not results.get("nic%s_%s" % (counter, option)):
566 results["nic%s_%s" % (counter, option)] = constants.VALUE_AUTO
569 results["nic_count"] = str(len(network_names))
572 def GetDisksNames(self):
573 """Provides list of file names for the disks used by the instance.
576 @return: list of file names, as referenced in .ovf file
580 disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA)
581 disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA)
582 for disk in disk_ids:
583 disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA)
584 disk_match = ("{%s}id" % OVF_SCHEMA, disk)
585 disk_elem = self._GetElementMatchingAttr(disk_search, disk_match)
586 if disk_elem is None:
587 raise errors.OpPrereqError("%s file corrupted - disk %s not found in"
588 " references" % (OVF_EXT, disk),
589 errors.ECODE_ENVIRON)
590 disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA)
591 disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA)
592 results.append((disk_name, disk_compression))
596 def SubElementText(parent, tag, text, attrib={}, **extra):
597 # pylint: disable=W0102
598 """This is just a wrapper on ET.SubElement that always has text content.
603 elem = ET.SubElement(parent, tag, attrib=attrib, **extra)
604 elem.text = str(text)
608 class OVFWriter(object):
609 """Writer class for OVF files.
611 @type tree: ET.ElementTree
612 @ivar tree: XML tree that we are constructing
613 @type virtual_system_type: string
614 @ivar virtual_system_type: value of vssd:VirtualSystemType, for external usage
615 in VMWare this requires to be vmx
616 @type hardware_list: list
617 @ivar hardware_list: list of items prepared for VirtualHardwareSection
618 @type next_instance_id: int
619 @ivar next_instance_id: next instance id to be used when creating elements on
623 def __init__(self, has_gnt_section):
624 """Initialize the writer - set the top element.
626 @type has_gnt_section: bool
627 @param has_gnt_section: if the Ganeti schema should be added - i.e. this
628 means that Ganeti section will be present
632 "xmlns:xsi": XML_SCHEMA,
633 "xmlns:vssd": VSSD_SCHEMA,
634 "xmlns:rasd": RASD_SCHEMA,
635 "xmlns:ovf": OVF_SCHEMA,
640 env_attribs["xmlns:gnt"] = GANETI_SCHEMA
641 self.virtual_system_type = VS_TYPE["ganeti"]
643 self.virtual_system_type = VS_TYPE["external"]
644 self.tree = ET.Element("Envelope", attrib=env_attribs)
645 self.hardware_list = []
646 # INSTANCE_ID contains statically assigned IDs, starting from 0
647 self.next_instance_id = len(INSTANCE_ID) # FIXME: hackish
649 def SaveDisksData(self, disks):
650 """Convert disk information to certain OVF sections.
653 @param disks: list of dictionaries of disk options from config.ini
656 references = ET.SubElement(self.tree, "References")
657 disk_section = ET.SubElement(self.tree, "DiskSection")
658 SubElementText(disk_section, "Info", "Virtual disk information")
659 for counter, disk in enumerate(disks):
660 file_id = "file%s" % counter
661 disk_id = "disk%s" % counter
663 "ovf:href": disk["path"],
664 "ovf:size": str(disk["real-size"]),
668 "ovf:capacity": str(disk["virt-size"]),
669 "ovf:diskId": disk_id,
670 "ovf:fileRef": file_id,
671 "ovf:format": DISK_FORMAT.get(disk["format"], disk["format"]),
673 if "compression" in disk:
674 file_attribs["ovf:compression"] = disk["compression"]
675 ET.SubElement(references, "File", attrib=file_attribs)
676 ET.SubElement(disk_section, "Disk", attrib=disk_attribs)
678 # Item in VirtualHardwareSection creation
679 disk_item = ET.Element("Item")
680 SubElementText(disk_item, "rasd:ElementName", disk_id)
681 SubElementText(disk_item, "rasd:HostResource", "ovf:/disk/%s" % disk_id)
682 SubElementText(disk_item, "rasd:InstanceID", self.next_instance_id)
683 SubElementText(disk_item, "rasd:Parent", INSTANCE_ID["scsi"])
684 SubElementText(disk_item, "rasd:ResourceType", RASD_TYPE["disk"])
685 self.hardware_list.append(disk_item)
686 self.next_instance_id += 1
688 def SaveNetworksData(self, networks):
689 """Convert network information to NetworkSection.
692 @param networks: list of dictionaries of network options form config.ini
695 network_section = ET.SubElement(self.tree, "NetworkSection")
696 SubElementText(network_section, "Info", "List of logical networks")
697 for counter, network in enumerate(networks):
698 network_name = "%s%s" % (network["mode"], counter)
699 network_attrib = {"ovf:name": network_name}
700 ET.SubElement(network_section, "Network", attrib=network_attrib)
702 # Item in VirtualHardwareSection creation
703 network_item = ET.Element("Item")
704 SubElementText(network_item, "rasd:Address", network["mac"])
705 SubElementText(network_item, "rasd:Connection", network_name)
706 SubElementText(network_item, "rasd:ElementName", network_name)
707 SubElementText(network_item, "rasd:InstanceID", self.next_instance_id)
708 SubElementText(network_item, "rasd:ResourceType",
709 RASD_TYPE["ethernet-adapter"])
710 self.hardware_list.append(network_item)
711 self.next_instance_id += 1
714 def _SaveNameAndParams(root, data):
715 """Save name and parameters information under root using data.
717 @type root: ET.Element
718 @param root: root element for the Name and Parameters
720 @param data: data from which we gather the values
723 assert(data.get("name"))
724 name = SubElementText(root, "gnt:Name", data["name"])
725 params = ET.SubElement(root, "gnt:Parameters")
726 for name, value in data.iteritems():
728 SubElementText(params, "gnt:%s" % name, value)
730 def SaveGanetiData(self, ganeti, networks):
731 """Convert Ganeti-specific information to GanetiSection.
734 @param ganeti: dictionary of Ganeti-specific options from config.ini
736 @param networks: list of dictionaries of network options form config.ini
739 ganeti_section = ET.SubElement(self.tree, "gnt:GanetiSection")
741 SubElementText(ganeti_section, "gnt:Version", ganeti.get("version"))
742 SubElementText(ganeti_section, "gnt:DiskTemplate",
743 ganeti.get("disk_template"))
744 SubElementText(ganeti_section, "gnt:AutoBalance",
745 ganeti.get("auto_balance"))
746 SubElementText(ganeti_section, "gnt:Tags", ganeti.get("tags"))
748 osys = ET.SubElement(ganeti_section, "gnt:OperatingSystem")
749 self._SaveNameAndParams(osys, ganeti["os"])
751 hypervisor = ET.SubElement(ganeti_section, "gnt:Hypervisor")
752 self._SaveNameAndParams(hypervisor, ganeti["hypervisor"])
754 network_section = ET.SubElement(ganeti_section, "gnt:Network")
755 for counter, network in enumerate(networks):
756 network_name = "%s%s" % (network["mode"], counter)
757 nic_attrib = {"ovf:name": network_name}
758 nic = ET.SubElement(network_section, "gnt:Nic", attrib=nic_attrib)
759 SubElementText(nic, "gnt:Mode", network["mode"])
760 SubElementText(nic, "gnt:MACAddress", network["mac"])
761 SubElementText(nic, "gnt:IPAddress", network["ip"])
762 SubElementText(nic, "gnt:Link", network["link"])
763 SubElementText(nic, "gnt:Net", network["network"])
765 def SaveVirtualSystemData(self, name, vcpus, memory):
766 """Convert virtual system information to OVF sections.
769 @param name: name of the instance
771 @param vcpus: number of VCPUs
773 @param memory: RAM memory in MB
778 vs_attrib = {"ovf:id": name}
779 virtual_system = ET.SubElement(self.tree, "VirtualSystem", attrib=vs_attrib)
780 SubElementText(virtual_system, "Info", "A virtual machine")
782 name_section = ET.SubElement(virtual_system, "Name")
783 name_section.text = name
784 os_attrib = {"ovf:id": "0"}
785 os_section = ET.SubElement(virtual_system, "OperatingSystemSection",
787 SubElementText(os_section, "Info", "Installed guest operating system")
788 hardware_section = ET.SubElement(virtual_system, "VirtualHardwareSection")
789 SubElementText(hardware_section, "Info", "Virtual hardware requirements")
792 system = ET.SubElement(hardware_section, "System")
793 SubElementText(system, "vssd:ElementName", "Virtual Hardware Family")
794 SubElementText(system, "vssd:InstanceID", INSTANCE_ID["system"])
795 SubElementText(system, "vssd:VirtualSystemIdentifier", name)
796 SubElementText(system, "vssd:VirtualSystemType", self.virtual_system_type)
799 vcpus_item = ET.SubElement(hardware_section, "Item")
800 SubElementText(vcpus_item, "rasd:ElementName",
801 "%s virtual CPU(s)" % vcpus)
802 SubElementText(vcpus_item, "rasd:InstanceID", INSTANCE_ID["vcpus"])
803 SubElementText(vcpus_item, "rasd:ResourceType", RASD_TYPE["vcpus"])
804 SubElementText(vcpus_item, "rasd:VirtualQuantity", vcpus)
807 memory_item = ET.SubElement(hardware_section, "Item")
808 SubElementText(memory_item, "rasd:AllocationUnits", "byte * 2^20")
809 SubElementText(memory_item, "rasd:ElementName", "%sMB of memory" % memory)
810 SubElementText(memory_item, "rasd:InstanceID", INSTANCE_ID["memory"])
811 SubElementText(memory_item, "rasd:ResourceType", RASD_TYPE["memory"])
812 SubElementText(memory_item, "rasd:VirtualQuantity", memory)
814 # Item for scsi controller
815 scsi_item = ET.SubElement(hardware_section, "Item")
816 SubElementText(scsi_item, "rasd:Address", INSTANCE_ID["system"])
817 SubElementText(scsi_item, "rasd:ElementName", "scsi_controller0")
818 SubElementText(scsi_item, "rasd:InstanceID", INSTANCE_ID["scsi"])
819 SubElementText(scsi_item, "rasd:ResourceSubType", SCSI_SUBTYPE)
820 SubElementText(scsi_item, "rasd:ResourceType", RASD_TYPE["scsi-controller"])
822 # Other items - from self.hardware_list
823 for item in self.hardware_list:
824 hardware_section.append(item)
826 def PrettyXmlDump(self):
827 """Formatter of the XML file.
830 @return: XML tree in the form of nicely-formatted string
833 raw_string = ET.tostring(self.tree)
834 parsed_xml = xml.dom.minidom.parseString(raw_string)
835 xml_string = parsed_xml.toprettyxml(indent=" ")
836 text_re = re.compile(r">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL)
837 return text_re.sub(r">\g<1></", xml_string)
840 class Converter(object):
841 """Converter class for OVF packages.
843 Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is
844 to provide a common interface for the two.
846 @type options: optparse.Values
847 @ivar options: options parsed from the command line
848 @type output_dir: string
849 @ivar output_dir: directory to which the results of conversion shall be
851 @type temp_file_manager: L{utils.TemporaryFileManager}
852 @ivar temp_file_manager: container for temporary files created during
854 @type temp_dir: string
855 @ivar temp_dir: temporary directory created then we deal with OVA
858 def __init__(self, input_path, options):
859 """Initialize the converter.
861 @type input_path: string
862 @param input_path: path to the Converter input file
863 @type options: optparse.Values
864 @param options: command line options
866 @raise errors.OpPrereqError: if file does not exist
869 input_path = os.path.abspath(input_path)
870 if not os.path.isfile(input_path):
871 raise errors.OpPrereqError("File does not exist: %s" % input_path,
872 errors.ECODE_ENVIRON)
873 self.options = options
874 self.temp_file_manager = utils.TemporaryFileManager()
876 self.output_dir = None
877 self._ReadInputData(input_path)
879 def _ReadInputData(self, input_path):
880 """Reads the data on which the conversion will take place.
882 @type input_path: string
883 @param input_path: absolute path to the Converter input file
886 raise NotImplementedError()
888 def _CompressDisk(self, disk_path, compression, action):
889 """Performs (de)compression on the disk and returns the new path
891 @type disk_path: string
892 @param disk_path: path to the disk
893 @type compression: string
894 @param compression: compression type
896 @param action: whether the action is compression or decompression
898 @return: new disk path after (de)compression
900 @raise errors.OpPrereqError: disk (de)compression failed or "compression"
904 assert(action in ALLOWED_ACTIONS)
905 # For now we only support gzip, as it is used in ovftool
906 if compression != COMPRESSION_TYPE:
907 raise errors.OpPrereqError("Unsupported compression type: %s"
908 % compression, errors.ECODE_INVAL)
909 disk_file = os.path.basename(disk_path)
910 if action == DECOMPRESS:
911 (disk_name, _) = os.path.splitext(disk_file)
913 elif action == COMPRESS:
915 new_path = utils.GetClosedTempfile(suffix=COMPRESSION_EXT, prefix=prefix,
917 self.temp_file_manager.Add(new_path)
918 args = ["gzip", "-c", disk_path]
919 run_result = utils.RunCmd(args, output=new_path)
920 if run_result.failed:
921 raise errors.OpPrereqError("Disk %s failed with output: %s"
922 % (action, run_result.stderr),
923 errors.ECODE_ENVIRON)
924 logging.info("The %s of the disk is completed", action)
925 return (COMPRESSION_EXT, new_path)
927 def _ConvertDisk(self, disk_format, disk_path):
928 """Performes conversion to specified format.
930 @type disk_format: string
931 @param disk_format: format to which the disk should be converted
932 @type disk_path: string
933 @param disk_path: path to the disk that should be converted
935 @return path to the output disk
937 @raise errors.OpPrereqError: convertion of the disk failed
941 disk_file = os.path.basename(disk_path)
942 (disk_name, disk_extension) = os.path.splitext(disk_file)
943 if disk_extension != disk_format:
944 logging.warning("Conversion of disk image to %s format, this may take"
945 " a while", disk_format)
947 new_disk_path = utils.GetClosedTempfile(
948 suffix=".%s" % disk_format, prefix=disk_name, dir=self.output_dir)
949 self.temp_file_manager.Add(new_disk_path)
951 constants.QEMUIMG_PATH,
958 run_result = utils.RunCmd(args, cwd=os.getcwd())
959 if run_result.failed:
960 raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was"
961 ": %s" % (disk_format, run_result.stderr),
962 errors.ECODE_ENVIRON)
963 return (".%s" % disk_format, new_disk_path)
966 def _GetDiskQemuInfo(disk_path, regexp):
967 """Figures out some information of the disk using qemu-img.
969 @type disk_path: string
970 @param disk_path: path to the disk we want to know the format of
972 @param regexp: string that has to be matched, it has to contain one group
976 @raise errors.OpPrereqError: format information cannot be retrieved
980 args = [constants.QEMUIMG_PATH, "info", disk_path]
981 run_result = utils.RunCmd(args, cwd=os.getcwd())
982 if run_result.failed:
983 raise errors.OpPrereqError("Gathering info about the disk using qemu-img"
984 " failed, output was: %s" % run_result.stderr,
985 errors.ECODE_ENVIRON)
986 result = run_result.output
987 regexp = r"%s" % regexp
988 match = re.search(regexp, result)
990 disk_format = match.group(1)
992 raise errors.OpPrereqError("No file information matching %s found in:"
993 " %s" % (regexp, result),
994 errors.ECODE_ENVIRON)
998 """Parses the data and creates a structure containing all required info.
1001 raise NotImplementedError()
1004 """Saves the gathered configuration in an apropriate format.
1007 raise NotImplementedError()
1010 """Cleans the temporary directory, if one was created.
1013 self.temp_file_manager.Cleanup()
1015 shutil.rmtree(self.temp_dir)
1016 self.temp_dir = None
1019 class OVFImporter(Converter):
1020 """Converter from OVF to Ganeti config file.
1022 @type input_dir: string
1023 @ivar input_dir: directory in which the .ovf file resides
1024 @type output_dir: string
1025 @ivar output_dir: directory to which the results of conversion shall be
1027 @type input_path: string
1028 @ivar input_path: complete path to the .ovf file
1029 @type ovf_reader: L{OVFReader}
1030 @ivar ovf_reader: OVF reader instance collects data from .ovf file
1031 @type results_name: string
1032 @ivar results_name: name of imported instance
1033 @type results_template: string
1034 @ivar results_template: disk template read from .ovf file or command line
1036 @type results_hypervisor: dict
1037 @ivar results_hypervisor: hypervisor information gathered from .ovf file or
1038 command line arguments
1039 @type results_os: dict
1040 @ivar results_os: operating system information gathered from .ovf file or
1041 command line arguments
1042 @type results_backend: dict
1043 @ivar results_backend: backend information gathered from .ovf file or
1044 command line arguments
1045 @type results_tags: string
1046 @ivar results_tags: string containing instance-specific tags
1047 @type results_version: string
1048 @ivar results_version: version as required by Ganeti import
1049 @type results_network: dict
1050 @ivar results_network: network information gathered from .ovf file or command
1052 @type results_disk: dict
1053 @ivar results_disk: disk information gathered from .ovf file or command line
1057 def _ReadInputData(self, input_path):
1058 """Reads the data on which the conversion will take place.
1060 @type input_path: string
1061 @param input_path: absolute path to the .ovf or .ova input file
1063 @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
1066 (input_dir, input_file) = os.path.split(input_path)
1067 (_, input_extension) = os.path.splitext(input_file)
1069 if input_extension == OVF_EXT:
1070 logging.info("%s file extension found, no unpacking necessary", OVF_EXT)
1071 self.input_dir = input_dir
1072 self.input_path = input_path
1073 self.temp_dir = None
1074 elif input_extension == OVA_EXT:
1075 logging.info("%s file extension found, proceeding to unpacking", OVA_EXT)
1076 self._UnpackOVA(input_path)
1078 raise errors.OpPrereqError("Unknown file extension; expected %s or %s"
1079 " file" % (OVA_EXT, OVF_EXT),
1081 assert ((input_extension == OVA_EXT and self.temp_dir) or
1082 (input_extension == OVF_EXT and not self.temp_dir))
1083 assert self.input_dir in self.input_path
1085 if self.options.output_dir:
1086 self.output_dir = os.path.abspath(self.options.output_dir)
1087 if (os.path.commonprefix([pathutils.EXPORT_DIR, self.output_dir]) !=
1088 pathutils.EXPORT_DIR):
1089 logging.warning("Export path is not under %s directory, import to"
1090 " Ganeti using gnt-backup may fail",
1091 pathutils.EXPORT_DIR)
1093 self.output_dir = pathutils.EXPORT_DIR
1095 self.ovf_reader = OVFReader(self.input_path)
1096 self.ovf_reader.VerifyManifest()
1098 def _UnpackOVA(self, input_path):
1099 """Unpacks the .ova package into temporary directory.
1101 @type input_path: string
1102 @param input_path: path to the .ova package file
1104 @raise errors.OpPrereqError: if file is not a proper tarball, one of the
1105 files in the archive seem malicious (e.g. path starts with '../') or
1106 .ova package does not contain .ovf file
1110 if not tarfile.is_tarfile(input_path):
1111 raise errors.OpPrereqError("The provided %s file is not a proper tar"
1112 " archive" % OVA_EXT, errors.ECODE_ENVIRON)
1113 ova_content = tarfile.open(input_path)
1114 temp_dir = tempfile.mkdtemp()
1115 self.temp_dir = temp_dir
1116 for file_name in ova_content.getnames():
1117 file_normname = os.path.normpath(file_name)
1119 utils.PathJoin(temp_dir, file_normname)
1120 except ValueError, err:
1121 raise errors.OpPrereqError("File %s inside %s package is not safe" %
1122 (file_name, OVA_EXT), errors.ECODE_ENVIRON)
1123 if file_name.endswith(OVF_EXT):
1124 input_name = file_name
1126 raise errors.OpPrereqError("No %s file in %s package found" %
1127 (OVF_EXT, OVA_EXT), errors.ECODE_ENVIRON)
1128 logging.warning("Unpacking the %s archive, this may take a while",
1130 self.input_dir = temp_dir
1131 self.input_path = utils.PathJoin(self.temp_dir, input_name)
1134 extract = ova_content.extractall
1135 except AttributeError:
1136 # This is a prehistorical case of using python < 2.5
1137 for member in ova_content.getmembers():
1138 ova_content.extract(member, path=self.temp_dir)
1140 extract(self.temp_dir)
1141 except tarfile.TarError, err:
1142 raise errors.OpPrereqError("Error while extracting %s archive: %s" %
1143 (OVA_EXT, err), errors.ECODE_ENVIRON)
1144 logging.info("OVA package extracted to %s directory", self.temp_dir)
1147 """Parses the data and creates a structure containing all required info.
1149 The method reads the information given either as a command line option or as
1150 a part of the OVF description.
1152 @raise errors.OpPrereqError: if some required part of the description of
1153 virtual instance is missing or unable to create output directory
1156 self.results_name = self._GetInfo("instance name", self.options.name,
1157 self._ParseNameOptions,
1158 self.ovf_reader.GetInstanceName)
1159 if not self.results_name:
1160 raise errors.OpPrereqError("Name of instance not provided",
1163 self.output_dir = utils.PathJoin(self.output_dir, self.results_name)
1165 utils.Makedirs(self.output_dir)
1166 except OSError, err:
1167 raise errors.OpPrereqError("Failed to create directory %s: %s" %
1168 (self.output_dir, err), errors.ECODE_ENVIRON)
1170 self.results_template = self._GetInfo(
1171 "disk template", self.options.disk_template, self._ParseTemplateOptions,
1172 self.ovf_reader.GetDiskTemplate)
1173 if not self.results_template:
1174 logging.info("Disk template not given")
1176 self.results_hypervisor = self._GetInfo(
1177 "hypervisor", self.options.hypervisor, self._ParseHypervisorOptions,
1178 self.ovf_reader.GetHypervisorData)
1179 assert self.results_hypervisor["hypervisor_name"]
1180 if self.results_hypervisor["hypervisor_name"] == constants.VALUE_AUTO:
1181 logging.debug("Default hypervisor settings from the cluster will be used")
1183 self.results_os = self._GetInfo(
1184 "OS", self.options.os, self._ParseOSOptions, self.ovf_reader.GetOSData)
1185 if not self.results_os.get("os_name"):
1186 raise errors.OpPrereqError("OS name must be provided",
1189 self.results_backend = self._GetInfo(
1190 "backend", self.options.beparams,
1191 self._ParseBackendOptions, self.ovf_reader.GetBackendData)
1192 assert self.results_backend.get("vcpus")
1193 assert self.results_backend.get("memory")
1194 assert self.results_backend.get("auto_balance") is not None
1196 self.results_tags = self._GetInfo(
1197 "tags", self.options.tags, self._ParseTags, self.ovf_reader.GetTagsData)
1199 ovf_version = self.ovf_reader.GetVersionData()
1201 self.results_version = ovf_version
1203 self.results_version = constants.EXPORT_VERSION
1205 self.results_network = self._GetInfo(
1206 "network", self.options.nics, self._ParseNicOptions,
1207 self.ovf_reader.GetNetworkData, ignore_test=self.options.no_nics)
1209 self.results_disk = self._GetInfo(
1210 "disk", self.options.disks, self._ParseDiskOptions, self._GetDiskInfo,
1211 ignore_test=self.results_template == constants.DT_DISKLESS)
1213 if not self.results_disk and not self.results_network:
1214 raise errors.OpPrereqError("Either disk specification or network"
1215 " description must be present",
1219 def _GetInfo(name, cmd_arg, cmd_function, nocmd_function,
1221 """Get information about some section - e.g. disk, network, hypervisor.
1224 @param name: name of the section
1226 @param cmd_arg: command line argument specific for section 'name'
1227 @type cmd_function: callable
1228 @param cmd_function: function to call if 'cmd_args' exists
1229 @type nocmd_function: callable
1230 @param nocmd_function: function to call if 'cmd_args' is not there
1234 logging.info("Information for %s will be ignored", name)
1237 logging.info("Information for %s will be parsed from command line", name)
1238 results = cmd_function()
1240 logging.info("Information for %s will be parsed from %s file",
1242 results = nocmd_function()
1243 logging.info("Options for %s were succesfully read", name)
1246 def _ParseNameOptions(self):
1247 """Returns name if one was given in command line.
1250 @return: name of an instance
1253 return self.options.name
1255 def _ParseTemplateOptions(self):
1256 """Returns disk template if one was given in command line.
1259 @return: disk template name
1262 return self.options.disk_template
1264 def _ParseHypervisorOptions(self):
1265 """Parses hypervisor options given in a command line.
1268 @return: dictionary containing name of the chosen hypervisor and all the
1272 assert type(self.options.hypervisor) is tuple
1273 assert len(self.options.hypervisor) == 2
1275 if self.options.hypervisor[0]:
1276 results["hypervisor_name"] = self.options.hypervisor[0]
1278 results["hypervisor_name"] = constants.VALUE_AUTO
1279 results.update(self.options.hypervisor[1])
1282 def _ParseOSOptions(self):
1283 """Parses OS options given in command line.
1286 @return: dictionary containing name of chosen OS and all its options
1289 assert self.options.os
1291 results["os_name"] = self.options.os
1292 results.update(self.options.osparams)
1295 def _ParseBackendOptions(self):
1296 """Parses backend options given in command line.
1299 @return: dictionary containing vcpus, memory and auto-balance options
1302 assert self.options.beparams
1304 backend.update(self.options.beparams)
1305 must_contain = ["vcpus", "memory", "auto_balance"]
1306 for element in must_contain:
1307 if backend.get(element) is None:
1308 backend[element] = constants.VALUE_AUTO
1311 def _ParseTags(self):
1312 """Returns tags list given in command line.
1315 @return: string containing comma-separated tags
1318 return self.options.tags
1320 def _ParseNicOptions(self):
1321 """Parses network options given in a command line or as a dictionary.
1324 @return: dictionary of network-related options
1327 assert self.options.nics
1329 for (nic_id, nic_desc) in self.options.nics:
1330 results["nic%s_mode" % nic_id] = \
1331 nic_desc.get("mode", constants.VALUE_AUTO)
1332 results["nic%s_mac" % nic_id] = nic_desc.get("mac", constants.VALUE_AUTO)
1333 results["nic%s_link" % nic_id] = \
1334 nic_desc.get("link", constants.VALUE_AUTO)
1335 results["nic%s_network" % nic_id] = \
1336 nic_desc.get("network", constants.VALUE_AUTO)
1337 if nic_desc.get("mode") == "bridged":
1338 results["nic%s_ip" % nic_id] = constants.VALUE_NONE
1340 results["nic%s_ip" % nic_id] = constants.VALUE_AUTO
1341 results["nic_count"] = str(len(self.options.nics))
1344 def _ParseDiskOptions(self):
1345 """Parses disk options given in a command line.
1348 @return: dictionary of disk-related options
1350 @raise errors.OpPrereqError: disk description does not contain size
1351 information or size information is invalid or creation failed
1355 assert self.options.disks
1357 for (disk_id, disk_desc) in self.options.disks:
1358 results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id
1359 if disk_desc.get("size"):
1361 disk_size = utils.ParseUnit(disk_desc["size"])
1363 raise errors.OpPrereqError("Invalid disk size for disk %s: %s" %
1364 (disk_id, disk_desc["size"]),
1366 new_path = utils.PathJoin(self.output_dir, str(disk_id))
1368 constants.QEMUIMG_PATH,
1375 run_result = utils.RunCmd(args)
1376 if run_result.failed:
1377 raise errors.OpPrereqError("Creation of disk %s failed, output was:"
1378 " %s" % (new_path, run_result.stderr),
1379 errors.ECODE_ENVIRON)
1380 results["disk%s_size" % disk_id] = str(disk_size)
1381 results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id
1383 raise errors.OpPrereqError("Disks created for import must have their"
1386 results["disk_count"] = str(len(self.options.disks))
1389 def _GetDiskInfo(self):
1390 """Gathers information about disks used by instance, perfomes conversion.
1393 @return: dictionary of disk-related options
1395 @raise errors.OpPrereqError: disk is not in the same directory as .ovf file
1399 disks_list = self.ovf_reader.GetDisksNames()
1400 for (counter, (disk_name, disk_compression)) in enumerate(disks_list):
1401 if os.path.dirname(disk_name):
1402 raise errors.OpPrereqError("Disks are not allowed to have absolute"
1403 " paths or paths outside main OVF"
1404 " directory", errors.ECODE_ENVIRON)
1405 disk, _ = os.path.splitext(disk_name)
1406 disk_path = utils.PathJoin(self.input_dir, disk_name)
1407 if disk_compression not in NO_COMPRESSION:
1408 _, disk_path = self._CompressDisk(disk_path, disk_compression,
1410 disk, _ = os.path.splitext(disk)
1411 if self._GetDiskQemuInfo(disk_path, r"file format: (\S+)") != "raw":
1412 logging.info("Conversion to raw format is required")
1413 ext, new_disk_path = self._ConvertDisk("raw", disk_path)
1415 final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext,
1416 directory=self.output_dir)
1417 final_name = os.path.basename(final_disk_path)
1418 disk_size = os.path.getsize(final_disk_path) / (1024 * 1024)
1419 results["disk%s_dump" % counter] = final_name
1420 results["disk%s_size" % counter] = str(disk_size)
1421 results["disk%s_ivname" % counter] = "disk/%s" % str(counter)
1423 results["disk_count"] = str(len(disks_list))
1427 """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
1429 @raise errors.OpPrereqError: when saving to config file failed
1432 logging.info("Conversion was succesfull, saving %s in %s directory",
1433 constants.EXPORT_CONF_FILE, self.output_dir)
1435 constants.INISECT_INS: {},
1436 constants.INISECT_BEP: {},
1437 constants.INISECT_EXP: {},
1438 constants.INISECT_OSP: {},
1439 constants.INISECT_HYP: {},
1442 results[constants.INISECT_INS].update(self.results_disk)
1443 results[constants.INISECT_INS].update(self.results_network)
1444 results[constants.INISECT_INS]["hypervisor"] = \
1445 self.results_hypervisor["hypervisor_name"]
1446 results[constants.INISECT_INS]["name"] = self.results_name
1447 if self.results_template:
1448 results[constants.INISECT_INS]["disk_template"] = self.results_template
1449 if self.results_tags:
1450 results[constants.INISECT_INS]["tags"] = self.results_tags
1452 results[constants.INISECT_BEP].update(self.results_backend)
1454 results[constants.INISECT_EXP]["os"] = self.results_os["os_name"]
1455 results[constants.INISECT_EXP]["version"] = self.results_version
1457 del self.results_os["os_name"]
1458 results[constants.INISECT_OSP].update(self.results_os)
1460 del self.results_hypervisor["hypervisor_name"]
1461 results[constants.INISECT_HYP].update(self.results_hypervisor)
1463 output_file_name = utils.PathJoin(self.output_dir,
1464 constants.EXPORT_CONF_FILE)
1467 for section, options in results.iteritems():
1468 output.append("[%s]" % section)
1469 for name, value in options.iteritems():
1472 output.append("%s = %s" % (name, value))
1474 output_contents = "\n".join(output)
1477 utils.WriteFile(output_file_name, data=output_contents)
1478 except errors.ProgrammerError, err:
1479 raise errors.OpPrereqError("Saving the config file failed: %s" % err,
1480 errors.ECODE_ENVIRON)
1485 class ConfigParserWithDefaults(ConfigParser.SafeConfigParser):
1486 """This is just a wrapper on SafeConfigParser, that uses default values
1489 def get(self, section, options, raw=None, vars=None): # pylint: disable=W0622
1491 result = ConfigParser.SafeConfigParser.get(self, section, options,
1493 except ConfigParser.NoOptionError:
1497 def getint(self, section, options):
1499 result = ConfigParser.SafeConfigParser.get(self, section, options)
1500 except ConfigParser.NoOptionError:
1505 class OVFExporter(Converter):
1506 """Converter from Ganeti config file to OVF
1508 @type input_dir: string
1509 @ivar input_dir: directory in which the config.ini file resides
1510 @type output_dir: string
1511 @ivar output_dir: directory to which the results of conversion shall be
1513 @type packed_dir: string
1514 @ivar packed_dir: if we want OVA package, this points to the real (i.e. not
1515 temp) output directory
1516 @type input_path: string
1517 @ivar input_path: complete path to the config.ini file
1518 @type output_path: string
1519 @ivar output_path: complete path to .ovf file
1520 @type config_parser: L{ConfigParserWithDefaults}
1521 @ivar config_parser: parser for the config.ini file
1522 @type reference_files: list
1523 @ivar reference_files: files referenced in the ovf file
1524 @type results_disk: list
1525 @ivar results_disk: list of dictionaries of disk options from config.ini
1526 @type results_network: list
1527 @ivar results_network: list of dictionaries of network options form config.ini
1528 @type results_name: string
1529 @ivar results_name: name of the instance
1530 @type results_vcpus: string
1531 @ivar results_vcpus: number of VCPUs
1532 @type results_memory: string
1533 @ivar results_memory: RAM memory in MB
1534 @type results_ganeti: dict
1535 @ivar results_ganeti: dictionary of Ganeti-specific options from config.ini
1538 def _ReadInputData(self, input_path):
1539 """Reads the data on which the conversion will take place.
1541 @type input_path: string
1542 @param input_path: absolute path to the config.ini input file
1544 @raise errors.OpPrereqError: error when reading the config file
1547 input_dir = os.path.dirname(input_path)
1548 self.input_path = input_path
1549 self.input_dir = input_dir
1550 if self.options.output_dir:
1551 self.output_dir = os.path.abspath(self.options.output_dir)
1553 self.output_dir = input_dir
1554 self.config_parser = ConfigParserWithDefaults()
1555 logging.info("Reading configuration from %s file", input_path)
1557 self.config_parser.read(input_path)
1558 except ConfigParser.MissingSectionHeaderError, err:
1559 raise errors.OpPrereqError("Error when trying to read %s: %s" %
1560 (input_path, err), errors.ECODE_ENVIRON)
1561 if self.options.ova_package:
1562 self.temp_dir = tempfile.mkdtemp()
1563 self.packed_dir = self.output_dir
1564 self.output_dir = self.temp_dir
1566 self.ovf_writer = OVFWriter(not self.options.ext_usage)
1568 def _ParseName(self):
1569 """Parses name from command line options or config file.
1572 @return: name of Ganeti instance
1574 @raise errors.OpPrereqError: if name of the instance is not provided
1577 if self.options.name:
1578 name = self.options.name
1580 name = self.config_parser.get(constants.INISECT_INS, NAME)
1582 raise errors.OpPrereqError("No instance name found",
1583 errors.ECODE_ENVIRON)
1586 def _ParseVCPUs(self):
1587 """Parses vcpus number from config file.
1590 @return: number of virtual CPUs
1592 @raise errors.OpPrereqError: if number of VCPUs equals 0
1595 vcpus = self.config_parser.getint(constants.INISECT_BEP, VCPUS)
1597 raise errors.OpPrereqError("No CPU information found",
1598 errors.ECODE_ENVIRON)
1601 def _ParseMemory(self):
1602 """Parses vcpus number from config file.
1605 @return: amount of memory in MB
1607 @raise errors.OpPrereqError: if amount of memory equals 0
1610 memory = self.config_parser.getint(constants.INISECT_BEP, MEMORY)
1612 raise errors.OpPrereqError("No memory information found",
1613 errors.ECODE_ENVIRON)
1616 def _ParseGaneti(self):
1617 """Parses Ganeti data from config file.
1620 @return: dictionary of Ganeti-specific options
1625 results["hypervisor"] = {}
1626 hyp_name = self.config_parser.get(constants.INISECT_INS, HYPERV)
1627 if hyp_name is None:
1628 raise errors.OpPrereqError("No hypervisor information found",
1629 errors.ECODE_ENVIRON)
1630 results["hypervisor"]["name"] = hyp_name
1631 pairs = self.config_parser.items(constants.INISECT_HYP)
1632 for (name, value) in pairs:
1633 results["hypervisor"][name] = value
1636 os_name = self.config_parser.get(constants.INISECT_EXP, OS)
1638 raise errors.OpPrereqError("No operating system information found",
1639 errors.ECODE_ENVIRON)
1640 results["os"]["name"] = os_name
1641 pairs = self.config_parser.items(constants.INISECT_OSP)
1642 for (name, value) in pairs:
1643 results["os"][name] = value
1646 (constants.INISECT_INS, DISK_TEMPLATE, "disk_template"),
1647 (constants.INISECT_BEP, AUTO_BALANCE, "auto_balance"),
1648 (constants.INISECT_INS, TAGS, "tags"),
1649 (constants.INISECT_EXP, VERSION, "version"),
1651 for (section, element, name) in others:
1652 results[name] = self.config_parser.get(section, element)
1655 def _ParseNetworks(self):
1656 """Parses network data from config file.
1659 @return: list of dictionaries of network options
1661 @raise errors.OpPrereqError: then network mode is not recognized
1668 self.config_parser.get(constants.INISECT_INS,
1669 "nic%s_link" % counter)
1670 if data_link is None:
1673 "mode": self.config_parser.get(constants.INISECT_INS,
1674 "nic%s_mode" % counter),
1675 "mac": self.config_parser.get(constants.INISECT_INS,
1676 "nic%s_mac" % counter),
1677 "ip": self.config_parser.get(constants.INISECT_INS,
1678 "nic%s_ip" % counter),
1679 "network": self.config_parser.get(constants.INISECT_INS,
1680 "nic%s_network" % counter),
1683 if results[counter]["mode"] not in constants.NIC_VALID_MODES:
1684 raise errors.OpPrereqError("Network mode %s not recognized"
1685 % results[counter]["mode"],
1690 def _GetDiskOptions(self, disk_file, compression):
1691 """Convert the disk and gather disk info for .ovf file.
1693 @type disk_file: string
1694 @param disk_file: name of the disk (without the full path)
1695 @type compression: bool
1696 @param compression: whether the disk should be compressed or not
1698 @raise errors.OpPrereqError: when disk image does not exist
1701 disk_path = utils.PathJoin(self.input_dir, disk_file)
1703 if not os.path.isfile(disk_path):
1704 raise errors.OpPrereqError("Disk image does not exist: %s" % disk_path,
1705 errors.ECODE_ENVIRON)
1706 if os.path.dirname(disk_file):
1707 raise errors.OpPrereqError("Path for the disk: %s contains a directory"
1708 " name" % disk_path, errors.ECODE_ENVIRON)
1709 disk_name, _ = os.path.splitext(disk_file)
1710 ext, new_disk_path = self._ConvertDisk(self.options.disk_format, disk_path)
1711 results["format"] = self.options.disk_format
1712 results["virt-size"] = self._GetDiskQemuInfo(
1713 new_disk_path, r"virtual size: \S+ \((\d+) bytes\)")
1715 ext2, new_disk_path = self._CompressDisk(new_disk_path, "gzip",
1717 disk_name, _ = os.path.splitext(disk_name)
1718 results["compression"] = "gzip"
1720 final_disk_path = LinkFile(new_disk_path, prefix=disk_name, suffix=ext,
1721 directory=self.output_dir)
1722 final_disk_name = os.path.basename(final_disk_path)
1723 results["real-size"] = os.path.getsize(final_disk_path)
1724 results["path"] = final_disk_name
1725 self.references_files.append(final_disk_path)
1728 def _ParseDisks(self):
1729 """Parses disk data from config file.
1732 @return: list of dictionaries of disk options
1739 self.config_parser.get(constants.INISECT_INS, "disk%s_dump" % counter)
1740 if disk_file is None:
1742 results.append(self._GetDiskOptions(disk_file, self.options.compression))
1747 """Parses the data and creates a structure containing all required info.
1751 utils.Makedirs(self.output_dir)
1752 except OSError, err:
1753 raise errors.OpPrereqError("Failed to create directory %s: %s" %
1754 (self.output_dir, err), errors.ECODE_ENVIRON)
1756 self.references_files = []
1757 self.results_name = self._ParseName()
1758 self.results_vcpus = self._ParseVCPUs()
1759 self.results_memory = self._ParseMemory()
1760 if not self.options.ext_usage:
1761 self.results_ganeti = self._ParseGaneti()
1762 self.results_network = self._ParseNetworks()
1763 self.results_disk = self._ParseDisks()
1765 def _PrepareManifest(self, path):
1766 """Creates manifest for all the files in OVF package.
1769 @param path: path to manifesto file
1771 @raise errors.OpPrereqError: if error occurs when writing file
1774 logging.info("Preparing manifest for the OVF package")
1776 files_list = [self.output_path]
1777 files_list.extend(self.references_files)
1778 logging.warning("Calculating SHA1 checksums, this may take a while")
1779 sha1_sums = utils.FingerprintFiles(files_list)
1780 for file_path, value in sha1_sums.iteritems():
1781 file_name = os.path.basename(file_path)
1782 lines.append("SHA1(%s)= %s" % (file_name, value))
1784 data = "\n".join(lines)
1786 utils.WriteFile(path, data=data)
1787 except errors.ProgrammerError, err:
1788 raise errors.OpPrereqError("Saving the manifest file failed: %s" % err,
1789 errors.ECODE_ENVIRON)
1792 def _PrepareTarFile(tar_path, files_list):
1793 """Creates tarfile from the files in OVF package.
1795 @type tar_path: string
1796 @param tar_path: path to the resulting file
1797 @type files_list: list
1798 @param files_list: list of files in the OVF package
1801 logging.info("Preparing tarball for the OVF package")
1802 open(tar_path, mode="w").close()
1803 ova_package = tarfile.open(name=tar_path, mode="w")
1804 for file_path in files_list:
1805 file_name = os.path.basename(file_path)
1806 ova_package.add(file_path, arcname=file_name)
1810 """Saves the gathered configuration in an apropriate format.
1812 @raise errors.OpPrereqError: if unable to create output directory
1815 output_file = "%s%s" % (self.results_name, OVF_EXT)
1816 output_path = utils.PathJoin(self.output_dir, output_file)
1817 self.ovf_writer = OVFWriter(not self.options.ext_usage)
1818 logging.info("Saving read data to %s", output_path)
1820 self.output_path = utils.PathJoin(self.output_dir, output_file)
1821 files_list = [self.output_path]
1823 self.ovf_writer.SaveDisksData(self.results_disk)
1824 self.ovf_writer.SaveNetworksData(self.results_network)
1825 if not self.options.ext_usage:
1826 self.ovf_writer.SaveGanetiData(self.results_ganeti, self.results_network)
1828 self.ovf_writer.SaveVirtualSystemData(self.results_name, self.results_vcpus,
1829 self.results_memory)
1831 data = self.ovf_writer.PrettyXmlDump()
1832 utils.WriteFile(self.output_path, data=data)
1834 manifest_file = "%s%s" % (self.results_name, MF_EXT)
1835 manifest_path = utils.PathJoin(self.output_dir, manifest_file)
1836 self._PrepareManifest(manifest_path)
1837 files_list.append(manifest_path)
1839 files_list.extend(self.references_files)
1841 if self.options.ova_package:
1842 ova_file = "%s%s" % (self.results_name, OVA_EXT)
1843 packed_path = utils.PathJoin(self.packed_dir, ova_file)
1845 utils.Makedirs(self.packed_dir)
1846 except OSError, err:
1847 raise errors.OpPrereqError("Failed to create directory %s: %s" %
1848 (self.packed_dir, err),
1849 errors.ECODE_ENVIRON)
1850 self._PrepareTarFile(packed_path, files_list)
1851 logging.info("Creation of the OVF package was successfull")