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