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