Import: networks
[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 errno
33 import logging
34 import os
35 import os.path
36 import re
37 import shutil
38 import tarfile
39 import tempfile
40 import xml.parsers.expat
41 try:
42   import xml.etree.ElementTree as ET
43 except ImportError:
44   import elementtree.ElementTree as ET
45
46 from ganeti import constants
47 from ganeti import errors
48 from ganeti import utils
49
50
51 # Schemas used in OVF format
52 GANETI_SCHEMA = "http://ganeti"
53 OVF_SCHEMA = "http://schemas.dmtf.org/ovf/envelope/1"
54 RASD_SCHEMA = ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/"
55                "CIM_ResourceAllocationSettingData")
56
57 # File extensions in OVF package
58 OVA_EXT = ".ova"
59 OVF_EXT = ".ovf"
60 MF_EXT = ".mf"
61 CERT_EXT = ".cert"
62 COMPRESSION_EXT = ".gz"
63 FILE_EXTENSIONS = [
64   OVF_EXT,
65   MF_EXT,
66   CERT_EXT,
67 ]
68
69 COMPRESSION_TYPE = "gzip"
70 COMPRESS = "compression"
71 DECOMPRESS = "decompression"
72 ALLOWED_ACTIONS = [COMPRESS, DECOMPRESS]
73
74
75 def LinkFile(old_path, prefix=None, suffix=None, directory=None):
76   """Create link with a given prefix and suffix.
77
78   This is a wrapper over os.link. It tries to create a hard link for given file,
79   but instead of rising error when file exists, the function changes the name
80   a little bit.
81
82   @type old_path:string
83   @param old_path: path to the file that is to be linked
84   @type prefix: string
85   @param prefix: prefix of filename for the link
86   @type suffix: string
87   @param suffix: suffix of the filename for the link
88   @type directory: string
89   @param directory: directory of the link
90
91   @raise errors.OpPrereqError: when error on linking is different than
92     "File exists"
93
94   """
95   assert(prefix is not None or suffix is not None)
96   if directory is None:
97     directory = os.getcwd()
98   new_path = utils.PathJoin(directory, "%s%s" % (prefix, suffix))
99   counter = 1
100   while True:
101     try:
102       os.link(old_path, new_path)
103       break
104     except OSError, err:
105       if err.errno == errno.EEXIST:
106         new_path = utils.PathJoin(directory,
107           "%s_%s%s" % (prefix, counter, suffix))
108         counter += 1
109       else:
110         raise errors.OpPrereqError("Error moving the file %s to %s location:"
111                                    " %s" % (old_path, new_path, err))
112   return new_path
113
114
115 class OVFReader(object):
116   """Reader class for OVF files.
117
118   @type files_list: list
119   @ivar files_list: list of files in the OVF package
120   @type tree: ET.ElementTree
121   @ivar tree: XML tree of the .ovf file
122   @type schema_name: string
123   @ivar schema_name: name of the .ovf file
124   @type input_dir: string
125   @ivar input_dir: directory in which the .ovf file resides
126
127   """
128   def __init__(self, input_path):
129     """Initialiaze the reader - load the .ovf file to XML parser.
130
131     It is assumed that names of manifesto (.mf), certificate (.cert) and ovf
132     files are the same. In order to account any other files as part of the ovf
133     package, they have to be explicitly mentioned in the Resources section
134     of the .ovf file.
135
136     @type input_path: string
137     @param input_path: absolute path to the .ovf file
138
139     @raise errors.OpPrereqError: when .ovf file is not a proper XML file or some
140       of the files mentioned in Resources section do not exist
141
142     """
143     self.tree = ET.ElementTree()
144     try:
145       self.tree.parse(input_path)
146     except xml.parsers.expat.ExpatError, err:
147       raise errors.OpPrereqError("Error while reading %s file: %s" %
148                                  (OVF_EXT, err))
149
150     # Create a list of all files in the OVF package
151     (input_dir, input_file) = os.path.split(input_path)
152     (input_name, _) = os.path.splitext(input_file)
153     files_directory = utils.ListVisibleFiles(input_dir)
154     files_list = []
155     for file_name in files_directory:
156       (name, extension) = os.path.splitext(file_name)
157       if extension in FILE_EXTENSIONS and name == input_name:
158         files_list.append(file_name)
159     files_list += self._GetAttributes("{%s}References/{%s}File" %
160                                       (OVF_SCHEMA, OVF_SCHEMA),
161                                       "{%s}href" % OVF_SCHEMA)
162     for file_name in files_list:
163       file_path = utils.PathJoin(input_dir, file_name)
164       if not os.path.exists(file_path):
165         raise errors.OpPrereqError("File does not exist: %s" % file_path)
166     logging.info("Files in the OVF package: %s", " ".join(files_list))
167     self.files_list = files_list
168     self.input_dir = input_dir
169     self.schema_name = input_name
170
171   def _GetAttributes(self, path, attribute):
172     """Get specified attribute from all nodes accessible using given path.
173
174     Function follows the path from root node to the desired tags using path,
175     then reads the apropriate attribute values.
176
177     @type path: string
178     @param path: path of nodes to visit
179     @type attribute: string
180     @param attribute: attribute for which we gather the information
181     @rtype: list
182     @return: for each accessible tag with the attribute value set, value of the
183       attribute
184
185     """
186     current_list = self.tree.findall(path)
187     results = [x.get(attribute) for x in current_list]
188     return filter(None, results)
189
190   def _GetElementMatchingAttr(self, path, match_attr):
191     """Searches for element on a path that matches certain attribute value.
192
193     Function follows the path from root node to the desired tags using path,
194     then searches for the first one matching the attribute value.
195
196     @type path: string
197     @param path: path of nodes to visit
198     @type match_attr: tuple
199     @param match_attr: pair (attribute, value) for which we search
200     @rtype: ET.ElementTree or None
201     @return: first element matching match_attr or None if nothing matches
202
203     """
204     potential_elements = self.tree.findall(path)
205     (attr, val) = match_attr
206     for elem in potential_elements:
207       if elem.get(attr) == val:
208         return elem
209     return None
210
211   def _GetElementMatchingText(self, path, match_text):
212     """Searches for element on a path that matches certain text value.
213
214     Function follows the path from root node to the desired tags using path,
215     then searches for the first one matching the text value.
216
217     @type path: string
218     @param path: path of nodes to visit
219     @type match_text: tuple
220     @param match_text: pair (node, text) for which we search
221     @rtype: ET.ElementTree or None
222     @return: first element matching match_text or None if nothing matches
223
224     """
225     potential_elements = self.tree.findall(path)
226     (node, text) = match_text
227     for elem in potential_elements:
228       if elem.findtext(node) == text:
229         return elem
230     return None
231
232   @staticmethod
233   def _GetDictParameters(root, schema):
234     """Reads text in all children and creates the dictionary from the contents.
235
236     @type root: ET.ElementTree or None
237     @param root: father of the nodes we want to collect data about
238     @type schema: string
239     @param schema: schema name to be removed from the tag
240     @rtype: dict
241     @return: dictionary containing tags and their text contents, tags have their
242       schema fragment removed or empty dictionary, when root is None
243
244     """
245     if not root:
246       return {}
247     results = {}
248     for element in list(root):
249       pref_len = len("{%s}" % schema)
250       assert(schema in element.tag)
251       tag = element.tag[pref_len:]
252       results[tag] = element.text
253     return results
254
255   def VerifyManifest(self):
256     """Verifies manifest for the OVF package, if one is given.
257
258     @raise errors.OpPrereqError: if SHA1 checksums do not match
259
260     """
261     if "%s%s" % (self.schema_name, MF_EXT) in self.files_list:
262       logging.warning("Verifying SHA1 checksums, this may take a while")
263       manifest_filename = "%s%s" % (self.schema_name, MF_EXT)
264       manifest_path = utils.PathJoin(self.input_dir, manifest_filename)
265       manifest_content = utils.ReadFile(manifest_path).splitlines()
266       manifest_files = {}
267       regexp = r"SHA1\((\S+)\)= (\S+)"
268       for line in manifest_content:
269         match = re.match(regexp, line)
270         if match:
271           file_name = match.group(1)
272           sha1_sum = match.group(2)
273           manifest_files[file_name] = sha1_sum
274       files_with_paths = [utils.PathJoin(self.input_dir, file_name)
275         for file_name in self.files_list]
276       sha1_sums = utils.FingerprintFiles(files_with_paths)
277       for file_name, value in manifest_files.iteritems():
278         if sha1_sums.get(utils.PathJoin(self.input_dir, file_name)) != value:
279           raise errors.OpPrereqError("SHA1 checksum of %s does not match the"
280                                      " value in manifest file" % file_name)
281       logging.info("SHA1 checksums verified")
282
283   def GetInstanceName(self):
284     """Provides information about instance name.
285
286     @rtype: string
287     @return: instance name string
288
289     """
290     find_name = "{%s}VirtualSystem/{%s}Name" % (OVF_SCHEMA, OVF_SCHEMA)
291     return self.tree.findtext(find_name)
292
293   def GetDiskTemplate(self):
294     """Returns disk template from .ovf file
295
296     @rtype: string or None
297     @return: name of the template
298     """
299     find_template = ("{%s}GanetiSection/{%s}DiskTemplate" %
300                      (GANETI_SCHEMA, GANETI_SCHEMA))
301     return self.tree.findtext(find_template)
302
303   def GetNetworkData(self):
304     """Provides data about the network in the OVF instance.
305
306     The method gathers the data about networks used by OVF instance. It assumes
307     that 'name' tag means something - in essence, if it contains one of the
308     words 'bridged' or 'routed' then that will be the mode of this network in
309     Ganeti. The information about the network can be either in GanetiSection or
310     VirtualHardwareSection.
311
312     @rtype: dict
313     @return: dictionary containing all the network information
314
315     """
316     results = {}
317     networks_search = ("{%s}NetworkSection/{%s}Network" %
318                        (OVF_SCHEMA, OVF_SCHEMA))
319     network_names = self._GetAttributes(networks_search,
320       "{%s}name" % OVF_SCHEMA)
321     required = ["ip", "mac", "link", "mode"]
322     for (counter, network_name) in enumerate(network_names):
323       network_search = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item"
324                         % (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
325       ganeti_search = ("{%s}GanetiSection/{%s}Network/{%s}Nic" %
326                        (GANETI_SCHEMA, GANETI_SCHEMA, GANETI_SCHEMA))
327       network_match = ("{%s}Connection" % RASD_SCHEMA, network_name)
328       ganeti_match = ("{%s}name" % OVF_SCHEMA, network_name)
329       network_data = self._GetElementMatchingText(network_search, network_match)
330       network_ganeti_data = self._GetElementMatchingAttr(ganeti_search,
331         ganeti_match)
332
333       ganeti_data = {}
334       if network_ganeti_data:
335         ganeti_data["mode"] = network_ganeti_data.findtext("{%s}Mode" %
336                                                            GANETI_SCHEMA)
337         ganeti_data["mac"] = network_ganeti_data.findtext("{%s}MACAddress" %
338                                                           GANETI_SCHEMA)
339         ganeti_data["ip"] = network_ganeti_data.findtext("{%s}IPAddress" %
340                                                          GANETI_SCHEMA)
341         ganeti_data["link"] = network_ganeti_data.findtext("{%s}Link" %
342                                                            GANETI_SCHEMA)
343       mac_data = None
344       if network_data:
345         mac_data = network_data.findtext("{%s}Address" % RASD_SCHEMA)
346
347       network_name = network_name.lower()
348
349       # First, some not Ganeti-specific information is collected
350       if constants.NIC_MODE_BRIDGED in network_name:
351         results["nic%s_mode" % counter] = "bridged"
352       elif constants.NIC_MODE_ROUTED in network_name:
353         results["nic%s_mode" % counter] = "routed"
354       results["nic%s_mac" % counter] = mac_data
355
356       # GanetiSection data overrides 'manually' collected data
357       for name, value in ganeti_data.iteritems():
358         results["nic%s_%s" % (counter, name)] = value
359
360       # Bridged network has no IP - unless specifically stated otherwise
361       if (results.get("nic%s_mode" % counter) == "bridged" and
362           not results.get("nic%s_ip" % counter)):
363         results["nic%s_ip" % counter] = constants.VALUE_NONE
364
365       for option in required:
366         if not results.get("nic%s_%s" % (counter, option)):
367           results["nic%s_%s" % (counter, option)] = constants.VALUE_AUTO
368
369     if network_names:
370       results["nic_count"] = str(len(network_names))
371     return results
372
373   def GetDisksNames(self):
374     """Provides list of file names for the disks used by the instance.
375
376     @rtype: list
377     @return: list of file names, as referenced in .ovf file
378
379     """
380     results = []
381     disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA)
382     disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA)
383     for disk in disk_ids:
384       disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA)
385       disk_match = ("{%s}id" % OVF_SCHEMA, disk)
386       disk_elem = self._GetElementMatchingAttr(disk_search, disk_match)
387       if disk_elem is None:
388         raise errors.OpPrereqError("%s file corrupted - disk %s not found in"
389                                    " references" % (OVF_EXT, disk))
390       disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA)
391       disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA)
392       results.append((disk_name, disk_compression))
393     return results
394
395
396 class Converter(object):
397   """Converter class for OVF packages.
398
399   Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is
400   to provide a common interface for the two.
401
402   @type options: optparse.Values
403   @ivar options: options parsed from the command line
404   @type output_dir: string
405   @ivar output_dir: directory to which the results of conversion shall be
406     written
407   @type temp_file_manager: L{utils.TemporaryFileManager}
408   @ivar temp_file_manager: container for temporary files created during
409     conversion
410   @type temp_dir: string
411   @ivar temp_dir: temporary directory created then we deal with OVA
412
413   """
414   def __init__(self, input_path, options):
415     """Initialize the converter.
416
417     @type input_path: string
418     @param input_path: path to the Converter input file
419     @type options: optparse.Values
420     @param options: command line options
421
422     @raise errors.OpPrereqError: if file does not exist
423
424     """
425     input_path = os.path.abspath(input_path)
426     if not os.path.isfile(input_path):
427       raise errors.OpPrereqError("File does not exist: %s" % input_path)
428     self.options = options
429     self.temp_file_manager = utils.TemporaryFileManager()
430     self.temp_dir = None
431     self.output_dir = None
432     self._ReadInputData(input_path)
433
434   def _ReadInputData(self, input_path):
435     """Reads the data on which the conversion will take place.
436
437     @type input_path: string
438     @param input_path: absolute path to the Converter input file
439
440     """
441     raise NotImplementedError()
442
443   def _CompressDisk(self, disk_path, compression, action):
444     """Performs (de)compression on the disk and returns the new path
445
446     @type disk_path: string
447     @param disk_path: path to the disk
448     @type compression: string
449     @param compression: compression type
450     @type action: string
451     @param action: whether the action is compression or decompression
452     @rtype: string
453     @return: new disk path after (de)compression
454
455     @raise errors.OpPrereqError: disk (de)compression failed or "compression"
456       is not supported
457
458     """
459     assert(action in ALLOWED_ACTIONS)
460     # For now we only support gzip, as it is used in ovftool
461     if compression != COMPRESSION_TYPE:
462       raise errors.OpPrereqError("Unsupported compression type: %s"
463                                  % compression)
464     disk_file = os.path.basename(disk_path)
465     if action == DECOMPRESS:
466       (disk_name, _) = os.path.splitext(disk_file)
467       prefix = disk_name
468     elif action == COMPRESS:
469       prefix = disk_file
470     new_path = utils.GetClosedTempfile(suffix=COMPRESSION_EXT, prefix=prefix,
471       dir=self.output_dir)
472     self.temp_file_manager.Add(new_path)
473     args = ["gzip", "-c", disk_path]
474     run_result = utils.RunCmd(args, output=new_path)
475     if run_result.failed:
476       raise errors.OpPrereqError("Disk %s failed with output: %s"
477                                  % (action, run_result.stderr))
478     logging.info("The %s of the disk is completed", action)
479     return (COMPRESSION_EXT, new_path)
480
481   def _ConvertDisk(self, disk_format, disk_path):
482     """Performes conversion to specified format.
483
484     @type disk_format: string
485     @param disk_format: format to which the disk should be converted
486     @type disk_path: string
487     @param disk_path: path to the disk that should be converted
488     @rtype: string
489     @return path to the output disk
490
491     @raise errors.OpPrereqError: convertion of the disk failed
492
493     """
494     disk_file = os.path.basename(disk_path)
495     (disk_name, disk_extension) = os.path.splitext(disk_file)
496     if disk_extension != disk_format:
497       logging.warning("Conversion of disk image to %s format, this may take"
498                       " a while", disk_format)
499
500     new_disk_path = utils.GetClosedTempfile(suffix=".%s" % disk_format,
501       prefix=disk_name, dir=self.output_dir)
502     self.temp_file_manager.Add(new_disk_path)
503     args = [
504       "qemu-img",
505       "convert",
506       "-O",
507       disk_format,
508       disk_path,
509       new_disk_path,
510     ]
511     run_result = utils.RunCmd(args, cwd=os.getcwd())
512     if run_result.failed:
513       raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was"
514                                  ": %s" % (disk_format, run_result.stderr))
515     return (".%s" % disk_format, new_disk_path)
516
517   @staticmethod
518   def _GetDiskQemuInfo(disk_path, regexp):
519     """Figures out some information of the disk using qemu-img.
520
521     @type disk_path: string
522     @param disk_path: path to the disk we want to know the format of
523     @type regexp: string
524     @param regexp: string that has to be matched, it has to contain one group
525     @rtype: string
526     @return: disk format
527
528     @raise errors.OpPrereqError: format information cannot be retrieved
529
530     """
531     args = ["qemu-img", "info", disk_path]
532     run_result = utils.RunCmd(args, cwd=os.getcwd())
533     if run_result.failed:
534       raise errors.OpPrereqError("Gathering info about the disk using qemu-img"
535                                  " failed, output was: %s" % run_result.stderr)
536     result = run_result.output
537     regexp = r"%s" % regexp
538     match = re.search(regexp, result)
539     if match:
540       disk_format = match.group(1)
541     else:
542       raise errors.OpPrereqError("No file information matching %s found in:"
543                                  " %s" % (regexp, result))
544     return disk_format
545
546   def Parse(self):
547     """Parses the data and creates a structure containing all required info.
548
549     """
550     raise NotImplementedError()
551
552   def Save(self):
553     """Saves the gathered configuration in an apropriate format.
554
555     """
556     raise NotImplementedError()
557
558   def Cleanup(self):
559     """Cleans the temporary directory, if one was created.
560
561     """
562     self.temp_file_manager.Cleanup()
563     if self.temp_dir:
564       shutil.rmtree(self.temp_dir)
565       self.temp_dir = None
566
567
568 class OVFImporter(Converter):
569   """Converter from OVF to Ganeti config file.
570
571   @type input_dir: string
572   @ivar input_dir: directory in which the .ovf file resides
573   @type output_dir: string
574   @ivar output_dir: directory to which the results of conversion shall be
575     written
576   @type input_path: string
577   @ivar input_path: complete path to the .ovf file
578   @type ovf_reader: L{OVFReader}
579   @ivar ovf_reader: OVF reader instance collects data from .ovf file
580   @type results_name: string
581   @ivar results_name: name of imported instance
582   @type results_template: string
583   @ivar results_template: disk template read from .ovf file or command line
584     arguments
585   @type results_network: dict
586   @ivar results_network: network information gathered from .ovf file or command
587     line arguments
588   @type results_disk: dict
589   @ivar results_disk: disk information gathered from .ovf file or command line
590     arguments
591
592   """
593   def _ReadInputData(self, input_path):
594     """Reads the data on which the conversion will take place.
595
596     @type input_path: string
597     @param input_path: absolute path to the .ovf or .ova input file
598
599     @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
600
601     """
602     (input_dir, input_file) = os.path.split(input_path)
603     (_, input_extension) = os.path.splitext(input_file)
604
605     if input_extension == OVF_EXT:
606       logging.info("%s file extension found, no unpacking necessary", OVF_EXT)
607       self.input_dir = input_dir
608       self.input_path = input_path
609       self.temp_dir = None
610     elif input_extension == OVA_EXT:
611       logging.info("%s file extension found, proceeding to unpacking", OVA_EXT)
612       self._UnpackOVA(input_path)
613     else:
614       raise errors.OpPrereqError("Unknown file extension; expected %s or %s"
615                                  " file" % (OVA_EXT, OVF_EXT))
616     assert ((input_extension == OVA_EXT and self.temp_dir) or
617             (input_extension == OVF_EXT and not self.temp_dir))
618     assert self.input_dir in self.input_path
619
620     if self.options.output_dir:
621       self.output_dir = os.path.abspath(self.options.output_dir)
622       if (os.path.commonprefix([constants.EXPORT_DIR, self.output_dir]) !=
623           constants.EXPORT_DIR):
624         logging.warning("Export path is not under %s directory, import to"
625                         " Ganeti using gnt-backup may fail",
626                         constants.EXPORT_DIR)
627     else:
628       self.output_dir = constants.EXPORT_DIR
629
630     self.ovf_reader = OVFReader(self.input_path)
631     self.ovf_reader.VerifyManifest()
632
633   def _UnpackOVA(self, input_path):
634     """Unpacks the .ova package into temporary directory.
635
636     @type input_path: string
637     @param input_path: path to the .ova package file
638
639     @raise errors.OpPrereqError: if file is not a proper tarball, one of the
640         files in the archive seem malicious (e.g. path starts with '../') or
641         .ova package does not contain .ovf file
642
643     """
644     input_name = None
645     if not tarfile.is_tarfile(input_path):
646       raise errors.OpPrereqError("The provided %s file is not a proper tar"
647                                  " archive", OVA_EXT)
648     ova_content = tarfile.open(input_path)
649     temp_dir = tempfile.mkdtemp()
650     self.temp_dir = temp_dir
651     for file_name in ova_content.getnames():
652       file_normname = os.path.normpath(file_name)
653       try:
654         utils.PathJoin(temp_dir, file_normname)
655       except ValueError, err:
656         raise errors.OpPrereqError("File %s inside %s package is not safe" %
657                                    (file_name, OVA_EXT))
658       if file_name.endswith(OVF_EXT):
659         input_name = file_name
660     if not input_name:
661       raise errors.OpPrereqError("No %s file in %s package found" %
662                                  (OVF_EXT, OVA_EXT))
663     logging.warning("Unpacking the %s archive, this may take a while",
664       input_path)
665     self.input_dir = temp_dir
666     self.input_path = utils.PathJoin(self.temp_dir, input_name)
667     try:
668       try:
669         extract = ova_content.extractall
670       except AttributeError:
671         # This is a prehistorical case of using python < 2.5
672         for member in ova_content.getmembers():
673           ova_content.extract(member, path=self.temp_dir)
674       else:
675         extract(self.temp_dir)
676     except tarfile.TarError, err:
677       raise errors.OpPrereqError("Error while extracting %s archive: %s" %
678                                  (OVA_EXT, err))
679     logging.info("OVA package extracted to %s directory", self.temp_dir)
680
681   def Parse(self):
682     """Parses the data and creates a structure containing all required info.
683
684     The method reads the information given either as a command line option or as
685     a part of the OVF description.
686
687     @raise errors.OpPrereqError: if some required part of the description of
688       virtual instance is missing or unable to create output directory
689
690     """
691     self.results_name = self._GetInfo("instance name", self.options.name,
692       self._ParseNameOptions, self.ovf_reader.GetInstanceName)
693     if not self.results_name:
694       raise errors.OpPrereqError("Name of instance not provided")
695
696     self.output_dir = utils.PathJoin(self.output_dir, self.results_name)
697     try:
698       utils.Makedirs(self.output_dir)
699     except OSError, err:
700       raise errors.OpPrereqError("Failed to create directory %s: %s" %
701                                  (self.output_dir, err))
702
703     self.results_template = self._GetInfo("disk template",
704       self.options.disk_template, self._ParseTemplateOptions,
705       self.ovf_reader.GetDiskTemplate)
706     if not self.results_template:
707       logging.info("Disk template not given")
708
709     self.results_network = self._GetInfo("network", self.options.nics,
710       self._ParseNicOptions, self.ovf_reader.GetNetworkData,
711       ignore_test=self.options.no_nics)
712
713     self.results_disk = self._GetInfo("disk", self.options.disks,
714       self._ParseDiskOptions, self._GetDiskInfo,
715       ignore_test=self.results_template == constants.DT_DISKLESS)
716
717     if not self.results_disk and not self.results_network:
718       raise errors.OpPrereqError("Either disk specification or network"
719                                  " description must be present")
720
721   @staticmethod
722   def _GetInfo(name, cmd_arg, cmd_function, nocmd_function,
723     ignore_test=False):
724     """Get information about some section - e.g. disk, network, hypervisor.
725
726     @type name: string
727     @param name: name of the section
728     @type cmd_arg: dict
729     @param cmd_arg: command line argument specific for section 'name'
730     @type cmd_function: callable
731     @param cmd_function: function to call if 'cmd_args' exists
732     @type nocmd_function: callable
733     @param nocmd_function: function to call if 'cmd_args' is not there
734
735     """
736     if ignore_test:
737       logging.info("Information for %s will be ignored", name)
738       return {}
739     if cmd_arg:
740       logging.info("Information for %s will be parsed from command line", name)
741       results = cmd_function()
742     else:
743       logging.info("Information for %s will be parsed from %s file",
744         name, OVF_EXT)
745       results = nocmd_function()
746     logging.info("Options for %s were succesfully read", name)
747     return results
748
749   def _ParseNameOptions(self):
750     """Returns name if one was given in command line.
751
752     @rtype: string
753     @return: name of an instance
754
755     """
756     return self.options.name
757
758   def _ParseTemplateOptions(self):
759     """Returns disk template if one was given in command line.
760
761     @rtype: string
762     @return: disk template name
763
764     """
765     return self.options.disk_template
766
767   def _ParseNicOptions(self):
768     """Parses network options given in a command line or as a dictionary.
769
770     @rtype: dict
771     @return: dictionary of network-related options
772
773     """
774     assert self.options.nics
775     results = {}
776     for (nic_id, nic_desc) in self.options.nics:
777       results["nic%s_mode" % nic_id] = \
778         nic_desc.get("mode", constants.VALUE_AUTO)
779       results["nic%s_mac" % nic_id] = nic_desc.get("mac", constants.VALUE_AUTO)
780       results["nic%s_link" % nic_id] = \
781         nic_desc.get("link", constants.VALUE_AUTO)
782       if nic_desc.get("mode") == "bridged":
783         results["nic%s_ip" % nic_id] = constants.VALUE_NONE
784       else:
785         results["nic%s_ip" % nic_id] = constants.VALUE_AUTO
786     results["nic_count"] = str(len(self.options.nics))
787     return results
788
789   def _ParseDiskOptions(self):
790     """Parses disk options given in a command line.
791
792     @rtype: dict
793     @return: dictionary of disk-related options
794
795     @raise errors.OpPrereqError: disk description does not contain size
796       information or size information is invalid or creation failed
797
798     """
799     assert self.options.disks
800     results = {}
801     for (disk_id, disk_desc) in self.options.disks:
802       results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id
803       if disk_desc.get("size"):
804         try:
805           disk_size = utils.ParseUnit(disk_desc["size"])
806         except ValueError:
807           raise errors.OpPrereqError("Invalid disk size for disk %s: %s" %
808                                      (disk_id, disk_desc["size"]))
809         new_path = utils.PathJoin(self.output_dir, str(disk_id))
810         args = [
811           "qemu-img",
812           "create",
813           "-f",
814           "raw",
815           new_path,
816           disk_size,
817         ]
818         run_result = utils.RunCmd(args)
819         if run_result.failed:
820           raise errors.OpPrereqError("Creation of disk %s failed, output was:"
821                                      " %s" % (new_path, run_result.stderr))
822         results["disk%s_size" % disk_id] = str(disk_size)
823         results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id
824       else:
825         raise errors.OpPrereqError("Disks created for import must have their"
826                                    " size specified")
827     results["disk_count"] = str(len(self.options.disks))
828     return results
829
830   def _GetDiskInfo(self):
831     """Gathers information about disks used by instance, perfomes conversion.
832
833     @rtype: dict
834     @return: dictionary of disk-related options
835
836     @raise errors.OpPrereqError: disk is not in the same directory as .ovf file
837
838     """
839     results = {}
840     disks_list = self.ovf_reader.GetDisksNames()
841     for (counter, (disk_name, disk_compression)) in enumerate(disks_list):
842       if os.path.dirname(disk_name):
843         raise errors.OpPrereqError("Disks are not allowed to have absolute"
844                                    " paths or paths outside main OVF directory")
845       disk, _ = os.path.splitext(disk_name)
846       disk_path = utils.PathJoin(self.input_dir, disk_name)
847       if disk_compression:
848         _, disk_path = self._CompressDisk(disk_path, disk_compression,
849           DECOMPRESS)
850         disk, _ = os.path.splitext(disk)
851       if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw":
852         logging.info("Conversion to raw format is required")
853       ext, new_disk_path = self._ConvertDisk("raw", disk_path)
854
855       final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext,
856         directory=self.output_dir)
857       final_name = os.path.basename(final_disk_path)
858       disk_size = os.path.getsize(final_disk_path) / (1024 * 1024)
859       results["disk%s_dump" % counter] = final_name
860       results["disk%s_size" % counter] = str(disk_size)
861       results["disk%s_ivname" % counter] = "disk/%s" % str(counter)
862     if disks_list:
863       results["disk_count"] = str(len(disks_list))
864     return results
865
866   def Save(self):
867     """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
868
869     @raise errors.OpPrereqError: when saving to config file failed
870
871     """
872     logging.info("Conversion was succesfull, saving %s in %s directory",
873                  constants.EXPORT_CONF_FILE, self.output_dir)
874     results = {
875       constants.INISECT_INS: {},
876       constants.INISECT_BEP: {},
877       constants.INISECT_EXP: {},
878       constants.INISECT_OSP: {},
879       constants.INISECT_HYP: {},
880     }
881
882     results[constants.INISECT_INS].update(self.results_disk)
883     results[constants.INISECT_INS].update(self.results_network)
884     results[constants.INISECT_INS]["name"] = self.results_name
885     if self.results_template:
886       results[constants.INISECT_INS]["disk_template"] = self.results_template
887
888     output_file_name = utils.PathJoin(self.output_dir,
889       constants.EXPORT_CONF_FILE)
890
891     output = []
892     for section, options in results.iteritems():
893       output.append("[%s]" % section)
894       for name, value in options.iteritems():
895         if value is None:
896           value = ""
897         output.append("%s = %s" % (name, value))
898       output.append("")
899     output_contents = "\n".join(output)
900
901     try:
902       utils.WriteFile(output_file_name, data=output_contents)
903     except errors.ProgrammerError, err:
904       raise errors.OpPrereqError("Saving the config file failed: %s" % err)
905
906     self.Cleanup()
907
908
909 class OVFExporter(Converter):
910   def _ReadInputData(self, input_path):
911     pass
912
913   def Parse(self):
914     pass
915
916   def Save(self):
917     pass