Disable pylint warnings for unknown members
[ganeti-local] / lib / ovf.py
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2011, 2012 Google Inc.
5 #
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.
10 #
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.
15 #
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
19 # 02110-1301, USA.
20
21
22 """Converter tools between ovf and ganeti config file
23
24 """
25
26 # pylint: disable=F0401, E1101
27
28 # F0401 because ElementTree is not default for python 2.4
29 # E1101 makes no sense - pylint assumes that ElementTree object is a tuple
30
31
32 import ConfigParser
33 import errno
34 import logging
35 import os
36 import os.path
37 import re
38 import shutil
39 import tarfile
40 import tempfile
41 import xml.dom.minidom
42 import xml.parsers.expat
43 try:
44   import xml.etree.ElementTree as ET
45 except ImportError:
46   import elementtree.ElementTree as ET
47
48 try:
49   ParseError = ET.ParseError # pylint: disable=E1103
50 except AttributeError:
51   ParseError = None
52
53 from ganeti import constants
54 from ganeti import errors
55 from ganeti import utils
56
57
58 # Schemas used in OVF format
59 GANETI_SCHEMA = "http://ganeti"
60 OVF_SCHEMA = "http://schemas.dmtf.org/ovf/envelope/1"
61 RASD_SCHEMA = ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/"
62                "CIM_ResourceAllocationSettingData")
63 VSSD_SCHEMA = ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/"
64                "CIM_VirtualSystemSettingData")
65 XML_SCHEMA = "http://www.w3.org/2001/XMLSchema-instance"
66
67 # File extensions in OVF package
68 OVA_EXT = ".ova"
69 OVF_EXT = ".ovf"
70 MF_EXT = ".mf"
71 CERT_EXT = ".cert"
72 COMPRESSION_EXT = ".gz"
73 FILE_EXTENSIONS = [
74   OVF_EXT,
75   MF_EXT,
76   CERT_EXT,
77 ]
78
79 COMPRESSION_TYPE = "gzip"
80 NO_COMPRESSION = [None, "identity"]
81 COMPRESS = "compression"
82 DECOMPRESS = "decompression"
83 ALLOWED_ACTIONS = [COMPRESS, DECOMPRESS]
84
85 VMDK = "vmdk"
86 RAW = "raw"
87 COW = "cow"
88 ALLOWED_FORMATS = [RAW, COW, VMDK]
89
90 # ResourceType values
91 RASD_TYPE = {
92   "vcpus": "3",
93   "memory": "4",
94   "scsi-controller": "6",
95   "ethernet-adapter": "10",
96   "disk": "17",
97 }
98
99 SCSI_SUBTYPE = "lsilogic"
100 VS_TYPE = {
101   "ganeti": "ganeti-ovf",
102   "external": "vmx-04",
103 }
104
105 # AllocationUnits values and conversion
106 ALLOCATION_UNITS = {
107   "b": ["bytes", "b"],
108   "kb": ["kilobytes", "kb", "byte * 2^10", "kibibytes", "kib"],
109   "mb": ["megabytes", "mb", "byte * 2^20", "mebibytes", "mib"],
110   "gb": ["gigabytes", "gb", "byte * 2^30", "gibibytes", "gib"],
111 }
112 CONVERT_UNITS_TO_MB = {
113   "b": lambda x: x / (1024 * 1024),
114   "kb": lambda x: x / 1024,
115   "mb": lambda x: x,
116   "gb": lambda x: x * 1024,
117 }
118
119 # Names of the config fields
120 NAME = "name"
121 OS = "os"
122 HYPERV = "hypervisor"
123 VCPUS = "vcpus"
124 MEMORY = "memory"
125 AUTO_BALANCE = "auto_balance"
126 DISK_TEMPLATE = "disk_template"
127 TAGS = "tags"
128 VERSION = "version"
129
130 # Instance IDs of System and SCSI controller
131 INSTANCE_ID = {
132   "system": 0,
133   "vcpus": 1,
134   "memory": 2,
135   "scsi": 3,
136 }
137
138 # Disk format descriptions
139 DISK_FORMAT = {
140   RAW: "http://en.wikipedia.org/wiki/Byte",
141   VMDK: "http://www.vmware.com/interfaces/specifications/vmdk.html"
142           "#monolithicSparse",
143   COW: "http://www.gnome.org/~markmc/qcow-image-format.html",
144 }
145
146
147 def CheckQemuImg():
148   """ Make sure that qemu-img is present before performing operations.
149
150   @raise errors.OpPrereqError: when qemu-img was not found in the system
151
152   """
153   if not constants.QEMUIMG_PATH:
154     raise errors.OpPrereqError("qemu-img not found at build time, unable"
155                                " to continue", errors.ECODE_STATE)
156
157
158 def LinkFile(old_path, prefix=None, suffix=None, directory=None):
159   """Create link with a given prefix and suffix.
160
161   This is a wrapper over os.link. It tries to create a hard link for given file,
162   but instead of rising error when file exists, the function changes the name
163   a little bit.
164
165   @type old_path:string
166   @param old_path: path to the file that is to be linked
167   @type prefix: string
168   @param prefix: prefix of filename for the link
169   @type suffix: string
170   @param suffix: suffix of the filename for the link
171   @type directory: string
172   @param directory: directory of the link
173
174   @raise errors.OpPrereqError: when error on linking is different than
175     "File exists"
176
177   """
178   assert(prefix is not None or suffix is not None)
179   if directory is None:
180     directory = os.getcwd()
181   new_path = utils.PathJoin(directory, "%s%s" % (prefix, suffix))
182   counter = 1
183   while True:
184     try:
185       os.link(old_path, new_path)
186       break
187     except OSError, err:
188       if err.errno == errno.EEXIST:
189         new_path = utils.PathJoin(directory,
190                                   "%s_%s%s" % (prefix, counter, suffix))
191         counter += 1
192       else:
193         raise errors.OpPrereqError("Error moving the file %s to %s location:"
194                                    " %s" % (old_path, new_path, err),
195                                    errors.ECODE_ENVIRON)
196   return new_path
197
198
199 class OVFReader(object):
200   """Reader class for OVF files.
201
202   @type files_list: list
203   @ivar files_list: list of files in the OVF package
204   @type tree: ET.ElementTree
205   @ivar tree: XML tree of the .ovf file
206   @type schema_name: string
207   @ivar schema_name: name of the .ovf file
208   @type input_dir: string
209   @ivar input_dir: directory in which the .ovf file resides
210
211   """
212   def __init__(self, input_path):
213     """Initialiaze the reader - load the .ovf file to XML parser.
214
215     It is assumed that names of manifesto (.mf), certificate (.cert) and ovf
216     files are the same. In order to account any other files as part of the ovf
217     package, they have to be explicitly mentioned in the Resources section
218     of the .ovf file.
219
220     @type input_path: string
221     @param input_path: absolute path to the .ovf file
222
223     @raise errors.OpPrereqError: when .ovf file is not a proper XML file or some
224       of the files mentioned in Resources section do not exist
225
226     """
227     self.tree = ET.ElementTree()
228     try:
229       self.tree.parse(input_path)
230     except (ParseError, xml.parsers.expat.ExpatError), err:
231       raise errors.OpPrereqError("Error while reading %s file: %s" %
232                                  (OVF_EXT, err), errors.ECODE_ENVIRON)
233
234     # Create a list of all files in the OVF package
235     (input_dir, input_file) = os.path.split(input_path)
236     (input_name, _) = os.path.splitext(input_file)
237     files_directory = utils.ListVisibleFiles(input_dir)
238     files_list = []
239     for file_name in files_directory:
240       (name, extension) = os.path.splitext(file_name)
241       if extension in FILE_EXTENSIONS and name == input_name:
242         files_list.append(file_name)
243     files_list += self._GetAttributes("{%s}References/{%s}File" %
244                                       (OVF_SCHEMA, OVF_SCHEMA),
245                                       "{%s}href" % OVF_SCHEMA)
246     for file_name in files_list:
247       file_path = utils.PathJoin(input_dir, file_name)
248       if not os.path.exists(file_path):
249         raise errors.OpPrereqError("File does not exist: %s" % file_path,
250                                    errors.ECODE_ENVIRON)
251     logging.info("Files in the OVF package: %s", " ".join(files_list))
252     self.files_list = files_list
253     self.input_dir = input_dir
254     self.schema_name = input_name
255
256   def _GetAttributes(self, path, attribute):
257     """Get specified attribute from all nodes accessible using given path.
258
259     Function follows the path from root node to the desired tags using path,
260     then reads the apropriate attribute values.
261
262     @type path: string
263     @param path: path of nodes to visit
264     @type attribute: string
265     @param attribute: attribute for which we gather the information
266     @rtype: list
267     @return: for each accessible tag with the attribute value set, value of the
268       attribute
269
270     """
271     current_list = self.tree.findall(path)
272     results = [x.get(attribute) for x in current_list]
273     return filter(None, results)
274
275   def _GetElementMatchingAttr(self, path, match_attr):
276     """Searches for element on a path that matches certain attribute value.
277
278     Function follows the path from root node to the desired tags using path,
279     then searches for the first one matching the attribute value.
280
281     @type path: string
282     @param path: path of nodes to visit
283     @type match_attr: tuple
284     @param match_attr: pair (attribute, value) for which we search
285     @rtype: ET.ElementTree or None
286     @return: first element matching match_attr or None if nothing matches
287
288     """
289     potential_elements = self.tree.findall(path)
290     (attr, val) = match_attr
291     for elem in potential_elements:
292       if elem.get(attr) == val:
293         return elem
294     return None
295
296   def _GetElementMatchingText(self, path, match_text):
297     """Searches for element on a path that matches certain text value.
298
299     Function follows the path from root node to the desired tags using path,
300     then searches for the first one matching the text value.
301
302     @type path: string
303     @param path: path of nodes to visit
304     @type match_text: tuple
305     @param match_text: pair (node, text) for which we search
306     @rtype: ET.ElementTree or None
307     @return: first element matching match_text or None if nothing matches
308
309     """
310     potential_elements = self.tree.findall(path)
311     (node, text) = match_text
312     for elem in potential_elements:
313       if elem.findtext(node) == text:
314         return elem
315     return None
316
317   @staticmethod
318   def _GetDictParameters(root, schema):
319     """Reads text in all children and creates the dictionary from the contents.
320
321     @type root: ET.ElementTree or None
322     @param root: father of the nodes we want to collect data about
323     @type schema: string
324     @param schema: schema name to be removed from the tag
325     @rtype: dict
326     @return: dictionary containing tags and their text contents, tags have their
327       schema fragment removed or empty dictionary, when root is None
328
329     """
330     if not root:
331       return {}
332     results = {}
333     for element in list(root):
334       pref_len = len("{%s}" % schema)
335       assert(schema in element.tag)
336       tag = element.tag[pref_len:]
337       results[tag] = element.text
338     return results
339
340   def VerifyManifest(self):
341     """Verifies manifest for the OVF package, if one is given.
342
343     @raise errors.OpPrereqError: if SHA1 checksums do not match
344
345     """
346     if "%s%s" % (self.schema_name, MF_EXT) in self.files_list:
347       logging.warning("Verifying SHA1 checksums, this may take a while")
348       manifest_filename = "%s%s" % (self.schema_name, MF_EXT)
349       manifest_path = utils.PathJoin(self.input_dir, manifest_filename)
350       manifest_content = utils.ReadFile(manifest_path).splitlines()
351       manifest_files = {}
352       regexp = r"SHA1\((\S+)\)= (\S+)"
353       for line in manifest_content:
354         match = re.match(regexp, line)
355         if match:
356           file_name = match.group(1)
357           sha1_sum = match.group(2)
358           manifest_files[file_name] = sha1_sum
359       files_with_paths = [utils.PathJoin(self.input_dir, file_name)
360                           for file_name in self.files_list]
361       sha1_sums = utils.FingerprintFiles(files_with_paths)
362       for file_name, value in manifest_files.iteritems():
363         if sha1_sums.get(utils.PathJoin(self.input_dir, file_name)) != value:
364           raise errors.OpPrereqError("SHA1 checksum of %s does not match the"
365                                      " value in manifest file" % file_name,
366                                      errors.ECODE_ENVIRON)
367       logging.info("SHA1 checksums verified")
368
369   def GetInstanceName(self):
370     """Provides information about instance name.
371
372     @rtype: string
373     @return: instance name string
374
375     """
376     find_name = "{%s}VirtualSystem/{%s}Name" % (OVF_SCHEMA, OVF_SCHEMA)
377     return self.tree.findtext(find_name)
378
379   def GetDiskTemplate(self):
380     """Returns disk template from .ovf file
381
382     @rtype: string or None
383     @return: name of the template
384     """
385     find_template = ("{%s}GanetiSection/{%s}DiskTemplate" %
386                      (GANETI_SCHEMA, GANETI_SCHEMA))
387     return self.tree.findtext(find_template)
388
389   def GetHypervisorData(self):
390     """Provides hypervisor information - hypervisor name and options.
391
392     @rtype: dict
393     @return: dictionary containing name of the used hypervisor and all the
394       specified options
395
396     """
397     hypervisor_search = ("{%s}GanetiSection/{%s}Hypervisor" %
398                          (GANETI_SCHEMA, GANETI_SCHEMA))
399     hypervisor_data = self.tree.find(hypervisor_search)
400     if not hypervisor_data:
401       return {"hypervisor_name": constants.VALUE_AUTO}
402     results = {
403       "hypervisor_name": hypervisor_data.findtext("{%s}Name" % GANETI_SCHEMA,
404                                                   default=constants.VALUE_AUTO),
405     }
406     parameters = hypervisor_data.find("{%s}Parameters" % GANETI_SCHEMA)
407     results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
408     return results
409
410   def GetOSData(self):
411     """ Provides operating system information - os name and options.
412
413     @rtype: dict
414     @return: dictionary containing name and options for the chosen OS
415
416     """
417     results = {}
418     os_search = ("{%s}GanetiSection/{%s}OperatingSystem" %
419                  (GANETI_SCHEMA, GANETI_SCHEMA))
420     os_data = self.tree.find(os_search)
421     if os_data:
422       results["os_name"] = os_data.findtext("{%s}Name" % GANETI_SCHEMA)
423       parameters = os_data.find("{%s}Parameters" % GANETI_SCHEMA)
424       results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
425     return results
426
427   def GetBackendData(self):
428     """ Provides backend information - vcpus, memory, auto balancing options.
429
430     @rtype: dict
431     @return: dictionary containing options for vcpus, memory and auto balance
432       settings
433
434     """
435     results = {}
436
437     find_vcpus = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item" %
438                    (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
439     match_vcpus = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["vcpus"])
440     vcpus = self._GetElementMatchingText(find_vcpus, match_vcpus)
441     if vcpus:
442       vcpus_count = vcpus.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
443                                    default=constants.VALUE_AUTO)
444     else:
445       vcpus_count = constants.VALUE_AUTO
446     results["vcpus"] = str(vcpus_count)
447
448     find_memory = find_vcpus
449     match_memory = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["memory"])
450     memory = self._GetElementMatchingText(find_memory, match_memory)
451     memory_raw = None
452     if memory:
453       alloc_units = memory.findtext("{%s}AllocationUnits" % RASD_SCHEMA)
454       matching_units = [units for units, variants in ALLOCATION_UNITS.items()
455                         if alloc_units.lower() in variants]
456       if matching_units == []:
457         raise errors.OpPrereqError("Unit %s for RAM memory unknown" %
458                                    alloc_units, errors.ECODE_INVAL)
459       units = matching_units[0]
460       memory_raw = int(memory.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
461                                        default=constants.VALUE_AUTO))
462       memory_count = CONVERT_UNITS_TO_MB[units](memory_raw)
463     else:
464       memory_count = constants.VALUE_AUTO
465     results["memory"] = str(memory_count)
466
467     find_balance = ("{%s}GanetiSection/{%s}AutoBalance" %
468                     (GANETI_SCHEMA, GANETI_SCHEMA))
469     balance = self.tree.findtext(find_balance, default=constants.VALUE_AUTO)
470     results["auto_balance"] = balance
471
472     return results
473
474   def GetTagsData(self):
475     """Provides tags information for instance.
476
477     @rtype: string or None
478     @return: string of comma-separated tags for the instance
479
480     """
481     find_tags = "{%s}GanetiSection/{%s}Tags" % (GANETI_SCHEMA, GANETI_SCHEMA)
482     results = self.tree.findtext(find_tags)
483     if results:
484       return results
485     else:
486       return None
487
488   def GetVersionData(self):
489     """Provides version number read from .ovf file
490
491     @rtype: string
492     @return: string containing the version number
493
494     """
495     find_version = ("{%s}GanetiSection/{%s}Version" %
496                     (GANETI_SCHEMA, GANETI_SCHEMA))
497     return self.tree.findtext(find_version)
498
499   def GetNetworkData(self):
500     """Provides data about the network in the OVF instance.
501
502     The method gathers the data about networks used by OVF instance. It assumes
503     that 'name' tag means something - in essence, if it contains one of the
504     words 'bridged' or 'routed' then that will be the mode of this network in
505     Ganeti. The information about the network can be either in GanetiSection or
506     VirtualHardwareSection.
507
508     @rtype: dict
509     @return: dictionary containing all the network information
510
511     """
512     results = {}
513     networks_search = ("{%s}NetworkSection/{%s}Network" %
514                        (OVF_SCHEMA, OVF_SCHEMA))
515     network_names = self._GetAttributes(networks_search,
516                                         "{%s}name" % OVF_SCHEMA)
517     required = ["ip", "mac", "link", "mode"]
518     for (counter, network_name) in enumerate(network_names):
519       network_search = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item"
520                         % (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
521       ganeti_search = ("{%s}GanetiSection/{%s}Network/{%s}Nic" %
522                        (GANETI_SCHEMA, GANETI_SCHEMA, GANETI_SCHEMA))
523       network_match = ("{%s}Connection" % RASD_SCHEMA, network_name)
524       ganeti_match = ("{%s}name" % OVF_SCHEMA, network_name)
525       network_data = self._GetElementMatchingText(network_search, network_match)
526       network_ganeti_data = self._GetElementMatchingAttr(ganeti_search,
527                                                          ganeti_match)
528
529       ganeti_data = {}
530       if network_ganeti_data:
531         ganeti_data["mode"] = network_ganeti_data.findtext("{%s}Mode" %
532                                                            GANETI_SCHEMA)
533         ganeti_data["mac"] = network_ganeti_data.findtext("{%s}MACAddress" %
534                                                           GANETI_SCHEMA)
535         ganeti_data["ip"] = network_ganeti_data.findtext("{%s}IPAddress" %
536                                                          GANETI_SCHEMA)
537         ganeti_data["link"] = network_ganeti_data.findtext("{%s}Link" %
538                                                            GANETI_SCHEMA)
539       mac_data = None
540       if network_data:
541         mac_data = network_data.findtext("{%s}Address" % RASD_SCHEMA)
542
543       network_name = network_name.lower()
544
545       # First, some not Ganeti-specific information is collected
546       if constants.NIC_MODE_BRIDGED in network_name:
547         results["nic%s_mode" % counter] = "bridged"
548       elif constants.NIC_MODE_ROUTED in network_name:
549         results["nic%s_mode" % counter] = "routed"
550       results["nic%s_mac" % counter] = mac_data
551
552       # GanetiSection data overrides 'manually' collected data
553       for name, value in ganeti_data.iteritems():
554         results["nic%s_%s" % (counter, name)] = value
555
556       # Bridged network has no IP - unless specifically stated otherwise
557       if (results.get("nic%s_mode" % counter) == "bridged" and
558           not results.get("nic%s_ip" % counter)):
559         results["nic%s_ip" % counter] = constants.VALUE_NONE
560
561       for option in required:
562         if not results.get("nic%s_%s" % (counter, option)):
563           results["nic%s_%s" % (counter, option)] = constants.VALUE_AUTO
564
565     if network_names:
566       results["nic_count"] = str(len(network_names))
567     return results
568
569   def GetDisksNames(self):
570     """Provides list of file names for the disks used by the instance.
571
572     @rtype: list
573     @return: list of file names, as referenced in .ovf file
574
575     """
576     results = []
577     disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA)
578     disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA)
579     for disk in disk_ids:
580       disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA)
581       disk_match = ("{%s}id" % OVF_SCHEMA, disk)
582       disk_elem = self._GetElementMatchingAttr(disk_search, disk_match)
583       if disk_elem is None:
584         raise errors.OpPrereqError("%s file corrupted - disk %s not found in"
585                                    " references" % (OVF_EXT, disk),
586                                    errors.ECODE_ENVIRON)
587       disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA)
588       disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA)
589       results.append((disk_name, disk_compression))
590     return results
591
592
593 def SubElementText(parent, tag, text, attrib={}, **extra):
594 # pylint: disable=W0102
595   """This is just a wrapper on ET.SubElement that always has text content.
596
597   """
598   if text is None:
599     return None
600   elem = ET.SubElement(parent, tag, attrib=attrib, **extra)
601   elem.text = str(text)
602   return elem
603
604
605 class OVFWriter(object):
606   """Writer class for OVF files.
607
608   @type tree: ET.ElementTree
609   @ivar tree: XML tree that we are constructing
610   @type virtual_system_type: string
611   @ivar virtual_system_type: value of vssd:VirtualSystemType, for external usage
612     in VMWare this requires to be vmx
613   @type hardware_list: list
614   @ivar hardware_list: list of items prepared for VirtualHardwareSection
615   @type next_instance_id: int
616   @ivar next_instance_id: next instance id to be used when creating elements on
617     hardware_list
618
619   """
620   def __init__(self, has_gnt_section):
621     """Initialize the writer - set the top element.
622
623     @type has_gnt_section: bool
624     @param has_gnt_section: if the Ganeti schema should be added - i.e. this
625       means that Ganeti section will be present
626
627     """
628     env_attribs = {
629       "xmlns:xsi": XML_SCHEMA,
630       "xmlns:vssd": VSSD_SCHEMA,
631       "xmlns:rasd": RASD_SCHEMA,
632       "xmlns:ovf": OVF_SCHEMA,
633       "xmlns": OVF_SCHEMA,
634       "xml:lang": "en-US",
635     }
636     if has_gnt_section:
637       env_attribs["xmlns:gnt"] = GANETI_SCHEMA
638       self.virtual_system_type = VS_TYPE["ganeti"]
639     else:
640       self.virtual_system_type = VS_TYPE["external"]
641     self.tree = ET.Element("Envelope", attrib=env_attribs)
642     self.hardware_list = []
643     # INSTANCE_ID contains statically assigned IDs, starting from 0
644     self.next_instance_id = len(INSTANCE_ID) # FIXME: hackish
645
646   def SaveDisksData(self, disks):
647     """Convert disk information to certain OVF sections.
648
649     @type disks: list
650     @param disks: list of dictionaries of disk options from config.ini
651
652     """
653     references = ET.SubElement(self.tree, "References")
654     disk_section = ET.SubElement(self.tree, "DiskSection")
655     SubElementText(disk_section, "Info", "Virtual disk information")
656     for counter, disk in enumerate(disks):
657       file_id = "file%s" % counter
658       disk_id = "disk%s" % counter
659       file_attribs = {
660         "ovf:href": disk["path"],
661         "ovf:size": str(disk["real-size"]),
662         "ovf:id": file_id,
663       }
664       disk_attribs = {
665         "ovf:capacity": str(disk["virt-size"]),
666         "ovf:diskId": disk_id,
667         "ovf:fileRef": file_id,
668         "ovf:format": DISK_FORMAT.get(disk["format"], disk["format"]),
669       }
670       if "compression" in disk:
671         file_attribs["ovf:compression"] = disk["compression"]
672       ET.SubElement(references, "File", attrib=file_attribs)
673       ET.SubElement(disk_section, "Disk", attrib=disk_attribs)
674
675       # Item in VirtualHardwareSection creation
676       disk_item = ET.Element("Item")
677       SubElementText(disk_item, "rasd:ElementName", disk_id)
678       SubElementText(disk_item, "rasd:HostResource", "ovf:/disk/%s" % disk_id)
679       SubElementText(disk_item, "rasd:InstanceID", self.next_instance_id)
680       SubElementText(disk_item, "rasd:Parent", INSTANCE_ID["scsi"])
681       SubElementText(disk_item, "rasd:ResourceType", RASD_TYPE["disk"])
682       self.hardware_list.append(disk_item)
683       self.next_instance_id += 1
684
685   def SaveNetworksData(self, networks):
686     """Convert network information to NetworkSection.
687
688     @type networks: list
689     @param networks: list of dictionaries of network options form config.ini
690
691     """
692     network_section = ET.SubElement(self.tree, "NetworkSection")
693     SubElementText(network_section, "Info", "List of logical networks")
694     for counter, network in enumerate(networks):
695       network_name = "%s%s" % (network["mode"], counter)
696       network_attrib = {"ovf:name": network_name}
697       ET.SubElement(network_section, "Network", attrib=network_attrib)
698
699       # Item in VirtualHardwareSection creation
700       network_item = ET.Element("Item")
701       SubElementText(network_item, "rasd:Address", network["mac"])
702       SubElementText(network_item, "rasd:Connection", network_name)
703       SubElementText(network_item, "rasd:ElementName", network_name)
704       SubElementText(network_item, "rasd:InstanceID", self.next_instance_id)
705       SubElementText(network_item, "rasd:ResourceType",
706                      RASD_TYPE["ethernet-adapter"])
707       self.hardware_list.append(network_item)
708       self.next_instance_id += 1
709
710   @staticmethod
711   def _SaveNameAndParams(root, data):
712     """Save name and parameters information under root using data.
713
714     @type root: ET.Element
715     @param root: root element for the Name and Parameters
716     @type data: dict
717     @param data: data from which we gather the values
718
719     """
720     assert(data.get("name"))
721     name = SubElementText(root, "gnt:Name", data["name"])
722     params = ET.SubElement(root, "gnt:Parameters")
723     for name, value in data.iteritems():
724       if name != "name":
725         SubElementText(params, "gnt:%s" % name, value)
726
727   def SaveGanetiData(self, ganeti, networks):
728     """Convert Ganeti-specific information to GanetiSection.
729
730     @type ganeti: dict
731     @param ganeti: dictionary of Ganeti-specific options from config.ini
732     @type networks: list
733     @param networks: list of dictionaries of network options form config.ini
734
735     """
736     ganeti_section = ET.SubElement(self.tree, "gnt:GanetiSection")
737
738     SubElementText(ganeti_section, "gnt:Version", ganeti.get("version"))
739     SubElementText(ganeti_section, "gnt:DiskTemplate",
740                    ganeti.get("disk_template"))
741     SubElementText(ganeti_section, "gnt:AutoBalance",
742                    ganeti.get("auto_balance"))
743     SubElementText(ganeti_section, "gnt:Tags", ganeti.get("tags"))
744
745     osys = ET.SubElement(ganeti_section, "gnt:OperatingSystem")
746     self._SaveNameAndParams(osys, ganeti["os"])
747
748     hypervisor = ET.SubElement(ganeti_section, "gnt:Hypervisor")
749     self._SaveNameAndParams(hypervisor, ganeti["hypervisor"])
750
751     network_section = ET.SubElement(ganeti_section, "gnt:Network")
752     for counter, network in enumerate(networks):
753       network_name = "%s%s" % (network["mode"], counter)
754       nic_attrib = {"ovf:name": network_name}
755       nic = ET.SubElement(network_section, "gnt:Nic", attrib=nic_attrib)
756       SubElementText(nic, "gnt:Mode", network["mode"])
757       SubElementText(nic, "gnt:MACAddress", network["mac"])
758       SubElementText(nic, "gnt:IPAddress", network["ip"])
759       SubElementText(nic, "gnt:Link", network["link"])
760
761   def SaveVirtualSystemData(self, name, vcpus, memory):
762     """Convert virtual system information to OVF sections.
763
764     @type name: string
765     @param name: name of the instance
766     @type vcpus: int
767     @param vcpus: number of VCPUs
768     @type memory: int
769     @param memory: RAM memory in MB
770
771     """
772     assert(vcpus > 0)
773     assert(memory > 0)
774     vs_attrib = {"ovf:id": name}
775     virtual_system = ET.SubElement(self.tree, "VirtualSystem", attrib=vs_attrib)
776     SubElementText(virtual_system, "Info", "A virtual machine")
777
778     name_section = ET.SubElement(virtual_system, "Name")
779     name_section.text = name
780     os_attrib = {"ovf:id": "0"}
781     os_section = ET.SubElement(virtual_system, "OperatingSystemSection",
782                                attrib=os_attrib)
783     SubElementText(os_section, "Info", "Installed guest operating system")
784     hardware_section = ET.SubElement(virtual_system, "VirtualHardwareSection")
785     SubElementText(hardware_section, "Info", "Virtual hardware requirements")
786
787     # System description
788     system = ET.SubElement(hardware_section, "System")
789     SubElementText(system, "vssd:ElementName", "Virtual Hardware Family")
790     SubElementText(system, "vssd:InstanceID", INSTANCE_ID["system"])
791     SubElementText(system, "vssd:VirtualSystemIdentifier", name)
792     SubElementText(system, "vssd:VirtualSystemType", self.virtual_system_type)
793
794     # Item for vcpus
795     vcpus_item = ET.SubElement(hardware_section, "Item")
796     SubElementText(vcpus_item, "rasd:ElementName",
797                    "%s virtual CPU(s)" % vcpus)
798     SubElementText(vcpus_item, "rasd:InstanceID", INSTANCE_ID["vcpus"])
799     SubElementText(vcpus_item, "rasd:ResourceType", RASD_TYPE["vcpus"])
800     SubElementText(vcpus_item, "rasd:VirtualQuantity", vcpus)
801
802     # Item for memory
803     memory_item = ET.SubElement(hardware_section, "Item")
804     SubElementText(memory_item, "rasd:AllocationUnits", "byte * 2^20")
805     SubElementText(memory_item, "rasd:ElementName", "%sMB of memory" % memory)
806     SubElementText(memory_item, "rasd:InstanceID", INSTANCE_ID["memory"])
807     SubElementText(memory_item, "rasd:ResourceType", RASD_TYPE["memory"])
808     SubElementText(memory_item, "rasd:VirtualQuantity", memory)
809
810     # Item for scsi controller
811     scsi_item = ET.SubElement(hardware_section, "Item")
812     SubElementText(scsi_item, "rasd:Address", INSTANCE_ID["system"])
813     SubElementText(scsi_item, "rasd:ElementName", "scsi_controller0")
814     SubElementText(scsi_item, "rasd:InstanceID", INSTANCE_ID["scsi"])
815     SubElementText(scsi_item, "rasd:ResourceSubType", SCSI_SUBTYPE)
816     SubElementText(scsi_item, "rasd:ResourceType", RASD_TYPE["scsi-controller"])
817
818     # Other items - from self.hardware_list
819     for item in self.hardware_list:
820       hardware_section.append(item)
821
822   def PrettyXmlDump(self):
823     """Formatter of the XML file.
824
825     @rtype: string
826     @return: XML tree in the form of nicely-formatted string
827
828     """
829     raw_string = ET.tostring(self.tree)
830     parsed_xml = xml.dom.minidom.parseString(raw_string)
831     xml_string = parsed_xml.toprettyxml(indent="  ")
832     text_re = re.compile(">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL)
833     return text_re.sub(">\g<1></", xml_string)
834
835
836 class Converter(object):
837   """Converter class for OVF packages.
838
839   Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is
840   to provide a common interface for the two.
841
842   @type options: optparse.Values
843   @ivar options: options parsed from the command line
844   @type output_dir: string
845   @ivar output_dir: directory to which the results of conversion shall be
846     written
847   @type temp_file_manager: L{utils.TemporaryFileManager}
848   @ivar temp_file_manager: container for temporary files created during
849     conversion
850   @type temp_dir: string
851   @ivar temp_dir: temporary directory created then we deal with OVA
852
853   """
854   def __init__(self, input_path, options):
855     """Initialize the converter.
856
857     @type input_path: string
858     @param input_path: path to the Converter input file
859     @type options: optparse.Values
860     @param options: command line options
861
862     @raise errors.OpPrereqError: if file does not exist
863
864     """
865     input_path = os.path.abspath(input_path)
866     if not os.path.isfile(input_path):
867       raise errors.OpPrereqError("File does not exist: %s" % input_path,
868                                  errors.ECODE_ENVIRON)
869     self.options = options
870     self.temp_file_manager = utils.TemporaryFileManager()
871     self.temp_dir = None
872     self.output_dir = None
873     self._ReadInputData(input_path)
874
875   def _ReadInputData(self, input_path):
876     """Reads the data on which the conversion will take place.
877
878     @type input_path: string
879     @param input_path: absolute path to the Converter input file
880
881     """
882     raise NotImplementedError()
883
884   def _CompressDisk(self, disk_path, compression, action):
885     """Performs (de)compression on the disk and returns the new path
886
887     @type disk_path: string
888     @param disk_path: path to the disk
889     @type compression: string
890     @param compression: compression type
891     @type action: string
892     @param action: whether the action is compression or decompression
893     @rtype: string
894     @return: new disk path after (de)compression
895
896     @raise errors.OpPrereqError: disk (de)compression failed or "compression"
897       is not supported
898
899     """
900     assert(action in ALLOWED_ACTIONS)
901     # For now we only support gzip, as it is used in ovftool
902     if compression != COMPRESSION_TYPE:
903       raise errors.OpPrereqError("Unsupported compression type: %s"
904                                  % compression, errors.ECODE_INVAL)
905     disk_file = os.path.basename(disk_path)
906     if action == DECOMPRESS:
907       (disk_name, _) = os.path.splitext(disk_file)
908       prefix = disk_name
909     elif action == COMPRESS:
910       prefix = disk_file
911     new_path = utils.GetClosedTempfile(suffix=COMPRESSION_EXT, prefix=prefix,
912                                        dir=self.output_dir)
913     self.temp_file_manager.Add(new_path)
914     args = ["gzip", "-c", disk_path]
915     run_result = utils.RunCmd(args, output=new_path)
916     if run_result.failed:
917       raise errors.OpPrereqError("Disk %s failed with output: %s"
918                                  % (action, run_result.stderr),
919                                  errors.ECODE_ENVIRON)
920     logging.info("The %s of the disk is completed", action)
921     return (COMPRESSION_EXT, new_path)
922
923   def _ConvertDisk(self, disk_format, disk_path):
924     """Performes conversion to specified format.
925
926     @type disk_format: string
927     @param disk_format: format to which the disk should be converted
928     @type disk_path: string
929     @param disk_path: path to the disk that should be converted
930     @rtype: string
931     @return path to the output disk
932
933     @raise errors.OpPrereqError: convertion of the disk failed
934
935     """
936     CheckQemuImg()
937     disk_file = os.path.basename(disk_path)
938     (disk_name, disk_extension) = os.path.splitext(disk_file)
939     if disk_extension != disk_format:
940       logging.warning("Conversion of disk image to %s format, this may take"
941                       " a while", disk_format)
942
943     new_disk_path = utils.GetClosedTempfile(
944       suffix=".%s" % disk_format, prefix=disk_name, dir=self.output_dir)
945     self.temp_file_manager.Add(new_disk_path)
946     args = [
947       constants.QEMUIMG_PATH,
948       "convert",
949       "-O",
950       disk_format,
951       disk_path,
952       new_disk_path,
953     ]
954     run_result = utils.RunCmd(args, cwd=os.getcwd())
955     if run_result.failed:
956       raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was"
957                                  ": %s" % (disk_format, run_result.stderr),
958                                  errors.ECODE_ENVIRON)
959     return (".%s" % disk_format, new_disk_path)
960
961   @staticmethod
962   def _GetDiskQemuInfo(disk_path, regexp):
963     """Figures out some information of the disk using qemu-img.
964
965     @type disk_path: string
966     @param disk_path: path to the disk we want to know the format of
967     @type regexp: string
968     @param regexp: string that has to be matched, it has to contain one group
969     @rtype: string
970     @return: disk format
971
972     @raise errors.OpPrereqError: format information cannot be retrieved
973
974     """
975     CheckQemuImg()
976     args = [constants.QEMUIMG_PATH, "info", disk_path]
977     run_result = utils.RunCmd(args, cwd=os.getcwd())
978     if run_result.failed:
979       raise errors.OpPrereqError("Gathering info about the disk using qemu-img"
980                                  " failed, output was: %s" % run_result.stderr,
981                                  errors.ECODE_ENVIRON)
982     result = run_result.output
983     regexp = r"%s" % regexp
984     match = re.search(regexp, result)
985     if match:
986       disk_format = match.group(1)
987     else:
988       raise errors.OpPrereqError("No file information matching %s found in:"
989                                  " %s" % (regexp, result),
990                                  errors.ECODE_ENVIRON)
991     return disk_format
992
993   def Parse(self):
994     """Parses the data and creates a structure containing all required info.
995
996     """
997     raise NotImplementedError()
998
999   def Save(self):
1000     """Saves the gathered configuration in an apropriate format.
1001
1002     """
1003     raise NotImplementedError()
1004
1005   def Cleanup(self):
1006     """Cleans the temporary directory, if one was created.
1007
1008     """
1009     self.temp_file_manager.Cleanup()
1010     if self.temp_dir:
1011       shutil.rmtree(self.temp_dir)
1012       self.temp_dir = None
1013
1014
1015 class OVFImporter(Converter):
1016   """Converter from OVF to Ganeti config file.
1017
1018   @type input_dir: string
1019   @ivar input_dir: directory in which the .ovf file resides
1020   @type output_dir: string
1021   @ivar output_dir: directory to which the results of conversion shall be
1022     written
1023   @type input_path: string
1024   @ivar input_path: complete path to the .ovf file
1025   @type ovf_reader: L{OVFReader}
1026   @ivar ovf_reader: OVF reader instance collects data from .ovf file
1027   @type results_name: string
1028   @ivar results_name: name of imported instance
1029   @type results_template: string
1030   @ivar results_template: disk template read from .ovf file or command line
1031     arguments
1032   @type results_hypervisor: dict
1033   @ivar results_hypervisor: hypervisor information gathered from .ovf file or
1034     command line arguments
1035   @type results_os: dict
1036   @ivar results_os: operating system information gathered from .ovf file or
1037     command line arguments
1038   @type results_backend: dict
1039   @ivar results_backend: backend information gathered from .ovf file or
1040     command line arguments
1041   @type results_tags: string
1042   @ivar results_tags: string containing instance-specific tags
1043   @type results_version: string
1044   @ivar results_version: version as required by Ganeti import
1045   @type results_network: dict
1046   @ivar results_network: network information gathered from .ovf file or command
1047     line arguments
1048   @type results_disk: dict
1049   @ivar results_disk: disk information gathered from .ovf file or command line
1050     arguments
1051
1052   """
1053   def _ReadInputData(self, input_path):
1054     """Reads the data on which the conversion will take place.
1055
1056     @type input_path: string
1057     @param input_path: absolute path to the .ovf or .ova input file
1058
1059     @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
1060
1061     """
1062     (input_dir, input_file) = os.path.split(input_path)
1063     (_, input_extension) = os.path.splitext(input_file)
1064
1065     if input_extension == OVF_EXT:
1066       logging.info("%s file extension found, no unpacking necessary", OVF_EXT)
1067       self.input_dir = input_dir
1068       self.input_path = input_path
1069       self.temp_dir = None
1070     elif input_extension == OVA_EXT:
1071       logging.info("%s file extension found, proceeding to unpacking", OVA_EXT)
1072       self._UnpackOVA(input_path)
1073     else:
1074       raise errors.OpPrereqError("Unknown file extension; expected %s or %s"
1075                                  " file" % (OVA_EXT, OVF_EXT),
1076                                  errors.ECODE_INVAL)
1077     assert ((input_extension == OVA_EXT and self.temp_dir) or
1078             (input_extension == OVF_EXT and not self.temp_dir))
1079     assert self.input_dir in self.input_path
1080
1081     if self.options.output_dir:
1082       self.output_dir = os.path.abspath(self.options.output_dir)
1083       if (os.path.commonprefix([constants.EXPORT_DIR, self.output_dir]) !=
1084           constants.EXPORT_DIR):
1085         logging.warning("Export path is not under %s directory, import to"
1086                         " Ganeti using gnt-backup may fail",
1087                         constants.EXPORT_DIR)
1088     else:
1089       self.output_dir = constants.EXPORT_DIR
1090
1091     self.ovf_reader = OVFReader(self.input_path)
1092     self.ovf_reader.VerifyManifest()
1093
1094   def _UnpackOVA(self, input_path):
1095     """Unpacks the .ova package into temporary directory.
1096
1097     @type input_path: string
1098     @param input_path: path to the .ova package file
1099
1100     @raise errors.OpPrereqError: if file is not a proper tarball, one of the
1101         files in the archive seem malicious (e.g. path starts with '../') or
1102         .ova package does not contain .ovf file
1103
1104     """
1105     input_name = None
1106     if not tarfile.is_tarfile(input_path):
1107       raise errors.OpPrereqError("The provided %s file is not a proper tar"
1108                                  " archive" % OVA_EXT, errors.ECODE_ENVIRON)
1109     ova_content = tarfile.open(input_path)
1110     temp_dir = tempfile.mkdtemp()
1111     self.temp_dir = temp_dir
1112     for file_name in ova_content.getnames():
1113       file_normname = os.path.normpath(file_name)
1114       try:
1115         utils.PathJoin(temp_dir, file_normname)
1116       except ValueError, err:
1117         raise errors.OpPrereqError("File %s inside %s package is not safe" %
1118                                    (file_name, OVA_EXT), errors.ECODE_ENVIRON)
1119       if file_name.endswith(OVF_EXT):
1120         input_name = file_name
1121     if not input_name:
1122       raise errors.OpPrereqError("No %s file in %s package found" %
1123                                  (OVF_EXT, OVA_EXT), errors.ECODE_ENVIRON)
1124     logging.warning("Unpacking the %s archive, this may take a while",
1125                     input_path)
1126     self.input_dir = temp_dir
1127     self.input_path = utils.PathJoin(self.temp_dir, input_name)
1128     try:
1129       try:
1130         extract = ova_content.extractall
1131       except AttributeError:
1132         # This is a prehistorical case of using python < 2.5
1133         for member in ova_content.getmembers():
1134           ova_content.extract(member, path=self.temp_dir)
1135       else:
1136         extract(self.temp_dir)
1137     except tarfile.TarError, err:
1138       raise errors.OpPrereqError("Error while extracting %s archive: %s" %
1139                                  (OVA_EXT, err), errors.ECODE_ENVIRON)
1140     logging.info("OVA package extracted to %s directory", self.temp_dir)
1141
1142   def Parse(self):
1143     """Parses the data and creates a structure containing all required info.
1144
1145     The method reads the information given either as a command line option or as
1146     a part of the OVF description.
1147
1148     @raise errors.OpPrereqError: if some required part of the description of
1149       virtual instance is missing or unable to create output directory
1150
1151     """
1152     self.results_name = self._GetInfo("instance name", self.options.name,
1153                                       self._ParseNameOptions,
1154                                       self.ovf_reader.GetInstanceName)
1155     if not self.results_name:
1156       raise errors.OpPrereqError("Name of instance not provided",
1157                                  errors.ECODE_INVAL)
1158
1159     self.output_dir = utils.PathJoin(self.output_dir, self.results_name)
1160     try:
1161       utils.Makedirs(self.output_dir)
1162     except OSError, err:
1163       raise errors.OpPrereqError("Failed to create directory %s: %s" %
1164                                  (self.output_dir, err), errors.ECODE_ENVIRON)
1165
1166     self.results_template = self._GetInfo(
1167       "disk template", self.options.disk_template, self._ParseTemplateOptions,
1168       self.ovf_reader.GetDiskTemplate)
1169     if not self.results_template:
1170       logging.info("Disk template not given")
1171
1172     self.results_hypervisor = self._GetInfo(
1173       "hypervisor", self.options.hypervisor, self._ParseHypervisorOptions,
1174       self.ovf_reader.GetHypervisorData)
1175     assert self.results_hypervisor["hypervisor_name"]
1176     if self.results_hypervisor["hypervisor_name"] == constants.VALUE_AUTO:
1177       logging.debug("Default hypervisor settings from the cluster will be used")
1178
1179     self.results_os = self._GetInfo(
1180       "OS", self.options.os, self._ParseOSOptions, self.ovf_reader.GetOSData)
1181     if not self.results_os.get("os_name"):
1182       raise errors.OpPrereqError("OS name must be provided",
1183                                  errors.ECODE_INVAL)
1184
1185     self.results_backend = self._GetInfo(
1186       "backend", self.options.beparams,
1187       self._ParseBackendOptions, self.ovf_reader.GetBackendData)
1188     assert self.results_backend.get("vcpus")
1189     assert self.results_backend.get("memory")
1190     assert self.results_backend.get("auto_balance") is not None
1191
1192     self.results_tags = self._GetInfo(
1193       "tags", self.options.tags, self._ParseTags, self.ovf_reader.GetTagsData)
1194
1195     ovf_version = self.ovf_reader.GetVersionData()
1196     if ovf_version:
1197       self.results_version = ovf_version
1198     else:
1199       self.results_version = constants.EXPORT_VERSION
1200
1201     self.results_network = self._GetInfo(
1202       "network", self.options.nics, self._ParseNicOptions,
1203       self.ovf_reader.GetNetworkData, ignore_test=self.options.no_nics)
1204
1205     self.results_disk = self._GetInfo(
1206       "disk", self.options.disks, self._ParseDiskOptions, self._GetDiskInfo,
1207       ignore_test=self.results_template == constants.DT_DISKLESS)
1208
1209     if not self.results_disk and not self.results_network:
1210       raise errors.OpPrereqError("Either disk specification or network"
1211                                  " description must be present",
1212                                  errors.ECODE_STATE)
1213
1214   @staticmethod
1215   def _GetInfo(name, cmd_arg, cmd_function, nocmd_function,
1216                ignore_test=False):
1217     """Get information about some section - e.g. disk, network, hypervisor.
1218
1219     @type name: string
1220     @param name: name of the section
1221     @type cmd_arg: dict
1222     @param cmd_arg: command line argument specific for section 'name'
1223     @type cmd_function: callable
1224     @param cmd_function: function to call if 'cmd_args' exists
1225     @type nocmd_function: callable
1226     @param nocmd_function: function to call if 'cmd_args' is not there
1227
1228     """
1229     if ignore_test:
1230       logging.info("Information for %s will be ignored", name)
1231       return {}
1232     if cmd_arg:
1233       logging.info("Information for %s will be parsed from command line", name)
1234       results = cmd_function()
1235     else:
1236       logging.info("Information for %s will be parsed from %s file",
1237                    name, OVF_EXT)
1238       results = nocmd_function()
1239     logging.info("Options for %s were succesfully read", name)
1240     return results
1241
1242   def _ParseNameOptions(self):
1243     """Returns name if one was given in command line.
1244
1245     @rtype: string
1246     @return: name of an instance
1247
1248     """
1249     return self.options.name
1250
1251   def _ParseTemplateOptions(self):
1252     """Returns disk template if one was given in command line.
1253
1254     @rtype: string
1255     @return: disk template name
1256
1257     """
1258     return self.options.disk_template
1259
1260   def _ParseHypervisorOptions(self):
1261     """Parses hypervisor options given in a command line.
1262
1263     @rtype: dict
1264     @return: dictionary containing name of the chosen hypervisor and all the
1265       options
1266
1267     """
1268     assert type(self.options.hypervisor) is tuple
1269     assert len(self.options.hypervisor) == 2
1270     results = {}
1271     if self.options.hypervisor[0]:
1272       results["hypervisor_name"] = self.options.hypervisor[0]
1273     else:
1274       results["hypervisor_name"] = constants.VALUE_AUTO
1275     results.update(self.options.hypervisor[1])
1276     return results
1277
1278   def _ParseOSOptions(self):
1279     """Parses OS options given in command line.
1280
1281     @rtype: dict
1282     @return: dictionary containing name of chosen OS and all its options
1283
1284     """
1285     assert self.options.os
1286     results = {}
1287     results["os_name"] = self.options.os
1288     results.update(self.options.osparams)
1289     return results
1290
1291   def _ParseBackendOptions(self):
1292     """Parses backend options given in command line.
1293
1294     @rtype: dict
1295     @return: dictionary containing vcpus, memory and auto-balance options
1296
1297     """
1298     assert self.options.beparams
1299     backend = {}
1300     backend.update(self.options.beparams)
1301     must_contain = ["vcpus", "memory", "auto_balance"]
1302     for element in must_contain:
1303       if backend.get(element) is None:
1304         backend[element] = constants.VALUE_AUTO
1305     return backend
1306
1307   def _ParseTags(self):
1308     """Returns tags list given in command line.
1309
1310     @rtype: string
1311     @return: string containing comma-separated tags
1312
1313     """
1314     return self.options.tags
1315
1316   def _ParseNicOptions(self):
1317     """Parses network options given in a command line or as a dictionary.
1318
1319     @rtype: dict
1320     @return: dictionary of network-related options
1321
1322     """
1323     assert self.options.nics
1324     results = {}
1325     for (nic_id, nic_desc) in self.options.nics:
1326       results["nic%s_mode" % nic_id] = \
1327         nic_desc.get("mode", constants.VALUE_AUTO)
1328       results["nic%s_mac" % nic_id] = nic_desc.get("mac", constants.VALUE_AUTO)
1329       results["nic%s_link" % nic_id] = \
1330         nic_desc.get("link", constants.VALUE_AUTO)
1331       if nic_desc.get("mode") == "bridged":
1332         results["nic%s_ip" % nic_id] = constants.VALUE_NONE
1333       else:
1334         results["nic%s_ip" % nic_id] = constants.VALUE_AUTO
1335     results["nic_count"] = str(len(self.options.nics))
1336     return results
1337
1338   def _ParseDiskOptions(self):
1339     """Parses disk options given in a command line.
1340
1341     @rtype: dict
1342     @return: dictionary of disk-related options
1343
1344     @raise errors.OpPrereqError: disk description does not contain size
1345       information or size information is invalid or creation failed
1346
1347     """
1348     CheckQemuImg()
1349     assert self.options.disks
1350     results = {}
1351     for (disk_id, disk_desc) in self.options.disks:
1352       results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id
1353       if disk_desc.get("size"):
1354         try:
1355           disk_size = utils.ParseUnit(disk_desc["size"])
1356         except ValueError:
1357           raise errors.OpPrereqError("Invalid disk size for disk %s: %s" %
1358                                      (disk_id, disk_desc["size"]),
1359                                      errors.ECODE_INVAL)
1360         new_path = utils.PathJoin(self.output_dir, str(disk_id))
1361         args = [
1362           constants.QEMUIMG_PATH,
1363           "create",
1364           "-f",
1365           "raw",
1366           new_path,
1367           disk_size,
1368         ]
1369         run_result = utils.RunCmd(args)
1370         if run_result.failed:
1371           raise errors.OpPrereqError("Creation of disk %s failed, output was:"
1372                                      " %s" % (new_path, run_result.stderr),
1373                                      errors.ECODE_ENVIRON)
1374         results["disk%s_size" % disk_id] = str(disk_size)
1375         results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id
1376       else:
1377         raise errors.OpPrereqError("Disks created for import must have their"
1378                                    " size specified",
1379                                    errors.ECODE_INVAL)
1380     results["disk_count"] = str(len(self.options.disks))
1381     return results
1382
1383   def _GetDiskInfo(self):
1384     """Gathers information about disks used by instance, perfomes conversion.
1385
1386     @rtype: dict
1387     @return: dictionary of disk-related options
1388
1389     @raise errors.OpPrereqError: disk is not in the same directory as .ovf file
1390
1391     """
1392     results = {}
1393     disks_list = self.ovf_reader.GetDisksNames()
1394     for (counter, (disk_name, disk_compression)) in enumerate(disks_list):
1395       if os.path.dirname(disk_name):
1396         raise errors.OpPrereqError("Disks are not allowed to have absolute"
1397                                    " paths or paths outside main OVF"
1398                                    " directory", errors.ECODE_ENVIRON)
1399       disk, _ = os.path.splitext(disk_name)
1400       disk_path = utils.PathJoin(self.input_dir, disk_name)
1401       if disk_compression not in NO_COMPRESSION:
1402         _, disk_path = self._CompressDisk(disk_path, disk_compression,
1403                                           DECOMPRESS)
1404         disk, _ = os.path.splitext(disk)
1405       if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw":
1406         logging.info("Conversion to raw format is required")
1407       ext, new_disk_path = self._ConvertDisk("raw", disk_path)
1408
1409       final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext,
1410                                  directory=self.output_dir)
1411       final_name = os.path.basename(final_disk_path)
1412       disk_size = os.path.getsize(final_disk_path) / (1024 * 1024)
1413       results["disk%s_dump" % counter] = final_name
1414       results["disk%s_size" % counter] = str(disk_size)
1415       results["disk%s_ivname" % counter] = "disk/%s" % str(counter)
1416     if disks_list:
1417       results["disk_count"] = str(len(disks_list))
1418     return results
1419
1420   def Save(self):
1421     """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
1422
1423     @raise errors.OpPrereqError: when saving to config file failed
1424
1425     """
1426     logging.info("Conversion was succesfull, saving %s in %s directory",
1427                  constants.EXPORT_CONF_FILE, self.output_dir)
1428     results = {
1429       constants.INISECT_INS: {},
1430       constants.INISECT_BEP: {},
1431       constants.INISECT_EXP: {},
1432       constants.INISECT_OSP: {},
1433       constants.INISECT_HYP: {},
1434     }
1435
1436     results[constants.INISECT_INS].update(self.results_disk)
1437     results[constants.INISECT_INS].update(self.results_network)
1438     results[constants.INISECT_INS]["hypervisor"] = \
1439       self.results_hypervisor["hypervisor_name"]
1440     results[constants.INISECT_INS]["name"] = self.results_name
1441     if self.results_template:
1442       results[constants.INISECT_INS]["disk_template"] = self.results_template
1443     if self.results_tags:
1444       results[constants.INISECT_INS]["tags"] = self.results_tags
1445
1446     results[constants.INISECT_BEP].update(self.results_backend)
1447
1448     results[constants.INISECT_EXP]["os"] = self.results_os["os_name"]
1449     results[constants.INISECT_EXP]["version"] = self.results_version
1450
1451     del self.results_os["os_name"]
1452     results[constants.INISECT_OSP].update(self.results_os)
1453
1454     del self.results_hypervisor["hypervisor_name"]
1455     results[constants.INISECT_HYP].update(self.results_hypervisor)
1456
1457     output_file_name = utils.PathJoin(self.output_dir,
1458                                       constants.EXPORT_CONF_FILE)
1459
1460     output = []
1461     for section, options in results.iteritems():
1462       output.append("[%s]" % section)
1463       for name, value in options.iteritems():
1464         if value is None:
1465           value = ""
1466         output.append("%s = %s" % (name, value))
1467       output.append("")
1468     output_contents = "\n".join(output)
1469
1470     try:
1471       utils.WriteFile(output_file_name, data=output_contents)
1472     except errors.ProgrammerError, err:
1473       raise errors.OpPrereqError("Saving the config file failed: %s" % err,
1474                                  errors.ECODE_ENVIRON)
1475
1476     self.Cleanup()
1477
1478
1479 class ConfigParserWithDefaults(ConfigParser.SafeConfigParser):
1480   """This is just a wrapper on SafeConfigParser, that uses default values
1481
1482   """
1483   def get(self, section, options, raw=None, vars=None): # pylint: disable=W0622
1484     try:
1485       result = ConfigParser.SafeConfigParser.get(self, section, options,
1486                                                  raw=raw, vars=vars)
1487     except ConfigParser.NoOptionError:
1488       result = None
1489     return result
1490
1491   def getint(self, section, options):
1492     try:
1493       result = ConfigParser.SafeConfigParser.get(self, section, options)
1494     except ConfigParser.NoOptionError:
1495       result = 0
1496     return int(result)
1497
1498
1499 class OVFExporter(Converter):
1500   """Converter from Ganeti config file to OVF
1501
1502   @type input_dir: string
1503   @ivar input_dir: directory in which the config.ini file resides
1504   @type output_dir: string
1505   @ivar output_dir: directory to which the results of conversion shall be
1506     written
1507   @type packed_dir: string
1508   @ivar packed_dir: if we want OVA package, this points to the real (i.e. not
1509     temp) output directory
1510   @type input_path: string
1511   @ivar input_path: complete path to the config.ini file
1512   @type output_path: string
1513   @ivar output_path: complete path to .ovf file
1514   @type config_parser: L{ConfigParserWithDefaults}
1515   @ivar config_parser: parser for the config.ini file
1516   @type reference_files: list
1517   @ivar reference_files: files referenced in the ovf file
1518   @type results_disk: list
1519   @ivar results_disk: list of dictionaries of disk options from config.ini
1520   @type results_network: list
1521   @ivar results_network: list of dictionaries of network options form config.ini
1522   @type results_name: string
1523   @ivar results_name: name of the instance
1524   @type results_vcpus: string
1525   @ivar results_vcpus: number of VCPUs
1526   @type results_memory: string
1527   @ivar results_memory: RAM memory in MB
1528   @type results_ganeti: dict
1529   @ivar results_ganeti: dictionary of Ganeti-specific options from config.ini
1530
1531   """
1532   def _ReadInputData(self, input_path):
1533     """Reads the data on which the conversion will take place.
1534
1535     @type input_path: string
1536     @param input_path: absolute path to the config.ini input file
1537
1538     @raise errors.OpPrereqError: error when reading the config file
1539
1540     """
1541     input_dir = os.path.dirname(input_path)
1542     self.input_path = input_path
1543     self.input_dir = input_dir
1544     if self.options.output_dir:
1545       self.output_dir = os.path.abspath(self.options.output_dir)
1546     else:
1547       self.output_dir = input_dir
1548     self.config_parser = ConfigParserWithDefaults()
1549     logging.info("Reading configuration from %s file", input_path)
1550     try:
1551       self.config_parser.read(input_path)
1552     except ConfigParser.MissingSectionHeaderError, err:
1553       raise errors.OpPrereqError("Error when trying to read %s: %s" %
1554                                  (input_path, err), errors.ECODE_ENVIRON)
1555     if self.options.ova_package:
1556       self.temp_dir = tempfile.mkdtemp()
1557       self.packed_dir = self.output_dir
1558       self.output_dir = self.temp_dir
1559
1560     self.ovf_writer = OVFWriter(not self.options.ext_usage)
1561
1562   def _ParseName(self):
1563     """Parses name from command line options or config file.
1564
1565     @rtype: string
1566     @return: name of Ganeti instance
1567
1568     @raise errors.OpPrereqError: if name of the instance is not provided
1569
1570     """
1571     if self.options.name:
1572       name = self.options.name
1573     else:
1574       name = self.config_parser.get(constants.INISECT_INS, NAME)
1575     if name is None:
1576       raise errors.OpPrereqError("No instance name found",
1577                                  errors.ECODE_ENVIRON)
1578     return name
1579
1580   def _ParseVCPUs(self):
1581     """Parses vcpus number from config file.
1582
1583     @rtype: int
1584     @return: number of virtual CPUs
1585
1586     @raise errors.OpPrereqError: if number of VCPUs equals 0
1587
1588     """
1589     vcpus = self.config_parser.getint(constants.INISECT_BEP, VCPUS)
1590     if vcpus == 0:
1591       raise errors.OpPrereqError("No CPU information found",
1592                                  errors.ECODE_ENVIRON)
1593     return vcpus
1594
1595   def _ParseMemory(self):
1596     """Parses vcpus number from config file.
1597
1598     @rtype: int
1599     @return: amount of memory in MB
1600
1601     @raise errors.OpPrereqError: if amount of memory equals 0
1602
1603     """
1604     memory = self.config_parser.getint(constants.INISECT_BEP, MEMORY)
1605     if memory == 0:
1606       raise errors.OpPrereqError("No memory information found",
1607                                  errors.ECODE_ENVIRON)
1608     return memory
1609
1610   def _ParseGaneti(self):
1611     """Parses Ganeti data from config file.
1612
1613     @rtype: dictionary
1614     @return: dictionary of Ganeti-specific options
1615
1616     """
1617     results = {}
1618     # hypervisor
1619     results["hypervisor"] = {}
1620     hyp_name = self.config_parser.get(constants.INISECT_INS, HYPERV)
1621     if hyp_name is None:
1622       raise errors.OpPrereqError("No hypervisor information found",
1623                                  errors.ECODE_ENVIRON)
1624     results["hypervisor"]["name"] = hyp_name
1625     pairs = self.config_parser.items(constants.INISECT_HYP)
1626     for (name, value) in pairs:
1627       results["hypervisor"][name] = value
1628     # os
1629     results["os"] = {}
1630     os_name = self.config_parser.get(constants.INISECT_EXP, OS)
1631     if os_name is None:
1632       raise errors.OpPrereqError("No operating system information found",
1633                                  errors.ECODE_ENVIRON)
1634     results["os"]["name"] = os_name
1635     pairs = self.config_parser.items(constants.INISECT_OSP)
1636     for (name, value) in pairs:
1637       results["os"][name] = value
1638     # other
1639     others = [
1640       (constants.INISECT_INS, DISK_TEMPLATE, "disk_template"),
1641       (constants.INISECT_BEP, AUTO_BALANCE, "auto_balance"),
1642       (constants.INISECT_INS, TAGS, "tags"),
1643       (constants.INISECT_EXP, VERSION, "version"),
1644     ]
1645     for (section, element, name) in others:
1646       results[name] = self.config_parser.get(section, element)
1647     return results
1648
1649   def _ParseNetworks(self):
1650     """Parses network data from config file.
1651
1652     @rtype: list
1653     @return: list of dictionaries of network options
1654
1655     @raise errors.OpPrereqError: then network mode is not recognized
1656
1657     """
1658     results = []
1659     counter = 0
1660     while True:
1661       data_link = \
1662         self.config_parser.get(constants.INISECT_INS, "nic%s_link" % counter)
1663       if data_link is None:
1664         break
1665       results.append({
1666         "mode": self.config_parser.get(constants.INISECT_INS,
1667                                        "nic%s_mode" % counter),
1668         "mac": self.config_parser.get(constants.INISECT_INS,
1669                                       "nic%s_mac" % counter),
1670         "ip": self.config_parser.get(constants.INISECT_INS,
1671                                      "nic%s_ip" % counter),
1672         "link": data_link,
1673       })
1674       if results[counter]["mode"] not in constants.NIC_VALID_MODES:
1675         raise errors.OpPrereqError("Network mode %s not recognized"
1676                                    % results[counter]["mode"],
1677                                    errors.ECODE_INVAL)
1678       counter += 1
1679     return results
1680
1681   def _GetDiskOptions(self, disk_file, compression):
1682     """Convert the disk and gather disk info for .ovf file.
1683
1684     @type disk_file: string
1685     @param disk_file: name of the disk (without the full path)
1686     @type compression: bool
1687     @param compression: whether the disk should be compressed or not
1688
1689     @raise errors.OpPrereqError: when disk image does not exist
1690
1691     """
1692     disk_path = utils.PathJoin(self.input_dir, disk_file)
1693     results = {}
1694     if not os.path.isfile(disk_path):
1695       raise errors.OpPrereqError("Disk image does not exist: %s" % disk_path,
1696                                  errors.ECODE_ENVIRON)
1697     if os.path.dirname(disk_file):
1698       raise errors.OpPrereqError("Path for the disk: %s contains a directory"
1699                                  " name" % disk_path, errors.ECODE_ENVIRON)
1700     disk_name, _ = os.path.splitext(disk_file)
1701     ext, new_disk_path = self._ConvertDisk(self.options.disk_format, disk_path)
1702     results["format"] = self.options.disk_format
1703     results["virt-size"] = self._GetDiskQemuInfo(
1704       new_disk_path, "virtual size: \S+ \((\d+) bytes\)")
1705     if compression:
1706       ext2, new_disk_path = self._CompressDisk(new_disk_path, "gzip",
1707                                                COMPRESS)
1708       disk_name, _ = os.path.splitext(disk_name)
1709       results["compression"] = "gzip"
1710       ext += ext2
1711     final_disk_path = LinkFile(new_disk_path, prefix=disk_name, suffix=ext,
1712                                directory=self.output_dir)
1713     final_disk_name = os.path.basename(final_disk_path)
1714     results["real-size"] = os.path.getsize(final_disk_path)
1715     results["path"] = final_disk_name
1716     self.references_files.append(final_disk_path)
1717     return results
1718
1719   def _ParseDisks(self):
1720     """Parses disk data from config file.
1721
1722     @rtype: list
1723     @return: list of dictionaries of disk options
1724
1725     """
1726     results = []
1727     counter = 0
1728     while True:
1729       disk_file = \
1730         self.config_parser.get(constants.INISECT_INS, "disk%s_dump" % counter)
1731       if disk_file is None:
1732         break
1733       results.append(self._GetDiskOptions(disk_file, self.options.compression))
1734       counter += 1
1735     return results
1736
1737   def Parse(self):
1738     """Parses the data and creates a structure containing all required info.
1739
1740     """
1741     try:
1742       utils.Makedirs(self.output_dir)
1743     except OSError, err:
1744       raise errors.OpPrereqError("Failed to create directory %s: %s" %
1745                                  (self.output_dir, err), errors.ECODE_ENVIRON)
1746
1747     self.references_files = []
1748     self.results_name = self._ParseName()
1749     self.results_vcpus = self._ParseVCPUs()
1750     self.results_memory = self._ParseMemory()
1751     if not self.options.ext_usage:
1752       self.results_ganeti = self._ParseGaneti()
1753     self.results_network = self._ParseNetworks()
1754     self.results_disk = self._ParseDisks()
1755
1756   def _PrepareManifest(self, path):
1757     """Creates manifest for all the files in OVF package.
1758
1759     @type path: string
1760     @param path: path to manifesto file
1761
1762     @raise errors.OpPrereqError: if error occurs when writing file
1763
1764     """
1765     logging.info("Preparing manifest for the OVF package")
1766     lines = []
1767     files_list = [self.output_path]
1768     files_list.extend(self.references_files)
1769     logging.warning("Calculating SHA1 checksums, this may take a while")
1770     sha1_sums = utils.FingerprintFiles(files_list)
1771     for file_path, value in sha1_sums.iteritems():
1772       file_name = os.path.basename(file_path)
1773       lines.append("SHA1(%s)= %s" % (file_name, value))
1774     lines.append("")
1775     data = "\n".join(lines)
1776     try:
1777       utils.WriteFile(path, data=data)
1778     except errors.ProgrammerError, err:
1779       raise errors.OpPrereqError("Saving the manifest file failed: %s" % err,
1780                                  errors.ECODE_ENVIRON)
1781
1782   @staticmethod
1783   def _PrepareTarFile(tar_path, files_list):
1784     """Creates tarfile from the files in OVF package.
1785
1786     @type tar_path: string
1787     @param tar_path: path to the resulting file
1788     @type files_list: list
1789     @param files_list: list of files in the OVF package
1790
1791     """
1792     logging.info("Preparing tarball for the OVF package")
1793     open(tar_path, mode="w").close()
1794     ova_package = tarfile.open(name=tar_path, mode="w")
1795     for file_path in files_list:
1796       file_name = os.path.basename(file_path)
1797       ova_package.add(file_path, arcname=file_name)
1798     ova_package.close()
1799
1800   def Save(self):
1801     """Saves the gathered configuration in an apropriate format.
1802
1803     @raise errors.OpPrereqError: if unable to create output directory
1804
1805     """
1806     output_file = "%s%s" % (self.results_name, OVF_EXT)
1807     output_path = utils.PathJoin(self.output_dir, output_file)
1808     self.ovf_writer = OVFWriter(not self.options.ext_usage)
1809     logging.info("Saving read data to %s", output_path)
1810
1811     self.output_path = utils.PathJoin(self.output_dir, output_file)
1812     files_list = [self.output_path]
1813
1814     self.ovf_writer.SaveDisksData(self.results_disk)
1815     self.ovf_writer.SaveNetworksData(self.results_network)
1816     if not self.options.ext_usage:
1817       self.ovf_writer.SaveGanetiData(self.results_ganeti, self.results_network)
1818
1819     self.ovf_writer.SaveVirtualSystemData(self.results_name, self.results_vcpus,
1820                                           self.results_memory)
1821
1822     data = self.ovf_writer.PrettyXmlDump()
1823     utils.WriteFile(self.output_path, data=data)
1824
1825     manifest_file = "%s%s" % (self.results_name, MF_EXT)
1826     manifest_path = utils.PathJoin(self.output_dir, manifest_file)
1827     self._PrepareManifest(manifest_path)
1828     files_list.append(manifest_path)
1829
1830     files_list.extend(self.references_files)
1831
1832     if self.options.ova_package:
1833       ova_file = "%s%s" % (self.results_name, OVA_EXT)
1834       packed_path = utils.PathJoin(self.packed_dir, ova_file)
1835       try:
1836         utils.Makedirs(self.packed_dir)
1837       except OSError, err:
1838         raise errors.OpPrereqError("Failed to create directory %s: %s" %
1839                                    (self.packed_dir, err),
1840                                    errors.ECODE_ENVIRON)
1841       self._PrepareTarFile(packed_path, files_list)
1842     logging.info("Creation of the OVF package was successfull")
1843     self.Cleanup()