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