Revision 99381e3b lib/ovf.py

b/lib/ovf.py
29 29
# E1101 makes no sense - pylint assumes that ElementTree object is a tuple
30 30

  
31 31

  
32
import errno
32 33
import logging
34
import os
33 35
import os.path
34 36
import re
35 37
import shutil
......
57 59
OVF_EXT = ".ovf"
58 60
MF_EXT = ".mf"
59 61
CERT_EXT = ".cert"
62
COMPRESSION_EXT = ".gz"
60 63
FILE_EXTENSIONS = [
61 64
  OVF_EXT,
62 65
  MF_EXT,
63 66
  CERT_EXT,
64 67
]
65 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

  
66 114

  
67 115
class OVFReader(object):
68 116
  """Reader class for OVF files.
......
139 187
    results = [x.get(attribute) for x in current_list]
140 188
    return filter(None, results)
141 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
  @staticmethod
212
  def _GetDictParameters(root, schema):
213
    """Reads text in all children and creates the dictionary from the contents.
214

  
215
    @type root: ET.ElementTree or None
216
    @param root: father of the nodes we want to collect data about
217
    @type schema: string
218
    @param schema: schema name to be removed from the tag
219
    @rtype: dict
220
    @return: dictionary containing tags and their text contents, tags have their
221
      schema fragment removed or empty dictionary, when root is None
222

  
223
    """
224
    if not root:
225
      return {}
226
    results = {}
227
    for element in list(root):
228
      pref_len = len("{%s}" % schema)
229
      assert(schema in element.tag)
230
      tag = element.tag[pref_len:]
231
      results[tag] = element.text
232
    return results
233

  
142 234
  def VerifyManifest(self):
143 235
    """Verifies manifest for the OVF package, if one is given.
144 236

  
......
167 259
                                     " value in manifest file" % file_name)
168 260
      logging.info("SHA1 checksums verified")
169 261

  
262
  def GetInstanceName(self):
263
    """Provides information about instance name.
264

  
265
    @rtype: string
266
    @return: instance name string
267

  
268
    """
269
    find_name = "{%s}VirtualSystem/{%s}Name" % (OVF_SCHEMA, OVF_SCHEMA)
270
    return self.tree.findtext(find_name)
271

  
272
  def GetDiskTemplate(self):
273
    """Returns disk template from .ovf file
274

  
275
    @rtype: string or None
276
    @return: name of the template
277
    """
278
    find_template = ("{%s}GanetiSection/{%s}DiskTemplate" %
279
                     (GANETI_SCHEMA, GANETI_SCHEMA))
280
    return self.tree.findtext(find_template)
281

  
282
  def GetDisksNames(self):
283
    """Provides list of file names for the disks used by the instance.
284

  
285
    @rtype: list
286
    @return: list of file names, as referenced in .ovf file
287

  
288
    """
289
    results = []
290
    disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA)
291
    disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA)
292
    for disk in disk_ids:
293
      disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA)
294
      disk_match = ("{%s}id" % OVF_SCHEMA, disk)
295
      disk_elem = self._GetElementMatchingAttr(disk_search, disk_match)
296
      if disk_elem is None:
297
        raise errors.OpPrereqError("%s file corrupted - disk %s not found in"
298
                                   " references" % (OVF_EXT, disk))
299
      disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA)
300
      disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA)
301
      results.append((disk_name, disk_compression))
302
    return results
303

  
170 304

  
171 305
class Converter(object):
172 306
  """Converter class for OVF packages.
......
215 349
    """
216 350
    raise NotImplementedError()
217 351

  
352
  def _CompressDisk(self, disk_path, compression, action):
353
    """Performs (de)compression on the disk and returns the new path
354

  
355
    @type disk_path: string
356
    @param disk_path: path to the disk
357
    @type compression: string
358
    @param compression: compression type
359
    @type action: string
360
    @param action: whether the action is compression or decompression
361
    @rtype: string
362
    @return: new disk path after (de)compression
363

  
364
    @raise errors.OpPrereqError: disk (de)compression failed or "compression"
365
      is not supported
366

  
367
    """
368
    assert(action in ALLOWED_ACTIONS)
369
    # For now we only support gzip, as it is used in ovftool
370
    if compression != COMPRESSION_TYPE:
371
      raise errors.OpPrereqError("Unsupported compression type: %s"
372
                                 % compression)
373
    disk_file = os.path.basename(disk_path)
374
    if action == DECOMPRESS:
375
      (disk_name, _) = os.path.splitext(disk_file)
376
      prefix = disk_name
377
    elif action == COMPRESS:
378
      prefix = disk_file
379
    new_path = utils.GetClosedTempfile(suffix=COMPRESSION_EXT, prefix=prefix,
380
      dir=self.output_dir)
381
    self.temp_file_manager.Add(new_path)
382
    args = ["gzip", "-c", disk_path]
383
    run_result = utils.RunCmd(args, output=new_path)
384
    if run_result.failed:
385
      raise errors.OpPrereqError("Disk %s failed with output: %s"
386
                                 % (action, run_result.stderr))
387
    logging.info("The %s of the disk is completed", action)
388
    return (COMPRESSION_EXT, new_path)
389

  
390
  def _ConvertDisk(self, disk_format, disk_path):
391
    """Performes conversion to specified format.
392

  
393
    @type disk_format: string
394
    @param disk_format: format to which the disk should be converted
395
    @type disk_path: string
396
    @param disk_path: path to the disk that should be converted
397
    @rtype: string
398
    @return path to the output disk
399

  
400
    @raise errors.OpPrereqError: convertion of the disk failed
401

  
402
    """
403
    disk_file = os.path.basename(disk_path)
404
    (disk_name, disk_extension) = os.path.splitext(disk_file)
405
    if disk_extension != disk_format:
406
      logging.warning("Conversion of disk image to %s format, this may take"
407
                      " a while", disk_format)
408

  
409
    new_disk_path = utils.GetClosedTempfile(suffix=".%s" % disk_format,
410
      prefix=disk_name, dir=self.output_dir)
411
    self.temp_file_manager.Add(new_disk_path)
412
    args = [
413
      "qemu-img",
414
      "convert",
415
      "-O",
416
      disk_format,
417
      disk_path,
418
      new_disk_path,
419
    ]
420
    run_result = utils.RunCmd(args, cwd=os.getcwd())
421
    if run_result.failed:
422
      raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was"
423
                                 ": %s" % (disk_format, run_result.stderr))
424
    return (".%s" % disk_format, new_disk_path)
425

  
426
  @staticmethod
427
  def _GetDiskQemuInfo(disk_path, regexp):
428
    """Figures out some information of the disk using qemu-img.
429

  
430
    @type disk_path: string
431
    @param disk_path: path to the disk we want to know the format of
432
    @type regexp: string
433
    @param regexp: string that has to be matched, it has to contain one group
434
    @rtype: string
435
    @return: disk format
436

  
437
    @raise errors.OpPrereqError: format information cannot be retrieved
438

  
439
    """
440
    args = ["qemu-img", "info", disk_path]
441
    run_result = utils.RunCmd(args, cwd=os.getcwd())
442
    if run_result.failed:
443
      raise errors.OpPrereqError("Gathering info about the disk using qemu-img"
444
                                 " failed, output was: %s" % run_result.stderr)
445
    result = run_result.output
446
    regexp = r"%s" % regexp
447
    match = re.search(regexp, result)
448
    if match:
449
      disk_format = match.group(1)
450
    else:
451
      raise errors.OpPrereqError("No file information matching %s found in:"
452
                                 " %s" % (regexp, result))
453
    return disk_format
454

  
218 455
  def Parse(self):
219 456
    """Parses the data and creates a structure containing all required info.
220 457

  
......
249 486
  @ivar input_path: complete path to the .ovf file
250 487
  @type ovf_reader: L{OVFReader}
251 488
  @ivar ovf_reader: OVF reader instance collects data from .ovf file
489
  @type results_name: string
490
  @ivar results_name: name of imported instance
491
  @type results_template: string
492
  @ivar results_template: disk template read from .ovf file or command line
493
    arguments
494
  @type results_disk: dict
495
  @ivar results_disk: disk information gathered from .ovf file or command line
496
    arguments
252 497

  
253 498
  """
254 499
  def _ReadInputData(self, input_path):
......
340 585
    logging.info("OVA package extracted to %s directory", self.temp_dir)
341 586

  
342 587
  def Parse(self):
343
    pass
588
    """Parses the data and creates a structure containing all required info.
589

  
590
    The method reads the information given either as a command line option or as
591
    a part of the OVF description.
592

  
593
    @raise errors.OpPrereqError: if some required part of the description of
594
      virtual instance is missing or unable to create output directory
595

  
596
    """
597
    self.results_name = self._GetInfo("instance name", self.options.name,
598
      self._ParseNameOptions, self.ovf_reader.GetInstanceName)
599
    if not self.results_name:
600
      raise errors.OpPrereqError("Name of instance not provided")
601

  
602
    self.output_dir = utils.PathJoin(self.output_dir, self.results_name)
603
    try:
604
      utils.Makedirs(self.output_dir)
605
    except OSError, err:
606
      raise errors.OpPrereqError("Failed to create directory %s: %s" %
607
                                 (self.output_dir, err))
608

  
609
    self.results_template = self._GetInfo("disk template",
610
      self.options.disk_template, self._ParseTemplateOptions,
611
      self.ovf_reader.GetDiskTemplate)
612
    if not self.results_template:
613
      logging.info("Disk template not given")
614

  
615
    self.results_disk = self._GetInfo("disk", self.options.disks,
616
      self._ParseDiskOptions, self._GetDiskInfo,
617
      ignore_test=self.results_template == constants.DT_DISKLESS)
618

  
619
  @staticmethod
620
  def _GetInfo(name, cmd_arg, cmd_function, nocmd_function,
621
    ignore_test=False):
622
    """Get information about some section - e.g. disk, network, hypervisor.
623

  
624
    @type name: string
625
    @param name: name of the section
626
    @type cmd_arg: dict
627
    @param cmd_arg: command line argument specific for section 'name'
628
    @type cmd_function: callable
629
    @param cmd_function: function to call if 'cmd_args' exists
630
    @type nocmd_function: callable
631
    @param nocmd_function: function to call if 'cmd_args' is not there
632

  
633
    """
634
    if ignore_test:
635
      logging.info("Information for %s will be ignored", name)
636
      return {}
637
    if cmd_arg:
638
      logging.info("Information for %s will be parsed from command line", name)
639
      results = cmd_function()
640
    else:
641
      logging.info("Information for %s will be parsed from %s file",
642
        name, OVF_EXT)
643
      results = nocmd_function()
644
    logging.info("Options for %s were succesfully read", name)
645
    return results
646

  
647
  def _ParseNameOptions(self):
648
    """Returns name if one was given in command line.
649

  
650
    @rtype: string
651
    @return: name of an instance
652

  
653
    """
654
    return self.options.name
655

  
656
  def _ParseTemplateOptions(self):
657
    """Returns disk template if one was given in command line.
658

  
659
    @rtype: string
660
    @return: disk template name
661

  
662
    """
663
    return self.options.disk_template
664

  
665
  def _ParseDiskOptions(self):
666
    """Parses disk options given in a command line.
667

  
668
    @rtype: dict
669
    @return: dictionary of disk-related options
670

  
671
    @raise errors.OpPrereqError: disk description does not contain size
672
      information or size information is invalid or creation failed
673

  
674
    """
675
    assert self.options.disks
676
    results = {}
677
    for (disk_id, disk_desc) in self.options.disks:
678
      results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id
679
      if disk_desc.get("size"):
680
        try:
681
          disk_size = utils.ParseUnit(disk_desc["size"])
682
        except ValueError:
683
          raise errors.OpPrereqError("Invalid disk size for disk %s: %s" %
684
                                     (disk_id, disk_desc["size"]))
685
        new_path = utils.PathJoin(self.output_dir, str(disk_id))
686
        args = [
687
          "qemu-img",
688
          "create",
689
          "-f",
690
          "raw",
691
          new_path,
692
          disk_size,
693
        ]
694
        run_result = utils.RunCmd(args)
695
        if run_result.failed:
696
          raise errors.OpPrereqError("Creation of disk %s failed, output was:"
697
                                     " %s" % (new_path, run_result.stderr))
698
        results["disk%s_size" % disk_id] = str(disk_size)
699
        results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id
700
      else:
701
        raise errors.OpPrereqError("Disks created for import must have their"
702
                                   " size specified")
703
    results["disk_count"] = str(len(self.options.disks))
704
    return results
705

  
706
  def _GetDiskInfo(self):
707
    """Gathers information about disks used by instance, perfomes conversion.
708

  
709
    @rtype: dict
710
    @return: dictionary of disk-related options
711

  
712
    @raise errors.OpPrereqError: disk is not in the same directory as .ovf file
713

  
714
    """
715
    results = {}
716
    disks_list = self.ovf_reader.GetDisksNames()
717
    for (counter, (disk_name, disk_compression)) in enumerate(disks_list):
718
      if os.path.dirname(disk_name):
719
        raise errors.OpPrereqError("Disks are not allowed to have absolute"
720
                                   " paths or paths outside main OVF directory")
721
      disk, _ = os.path.splitext(disk_name)
722
      disk_path = utils.PathJoin(self.input_dir, disk_name)
723
      if disk_compression:
724
        _, disk_path = self._CompressDisk(disk_path, disk_compression,
725
          DECOMPRESS)
726
        disk, _ = os.path.splitext(disk)
727
      if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw":
728
        logging.info("Conversion to raw format is required")
729
      ext, new_disk_path = self._ConvertDisk("raw", disk_path)
730

  
731
      final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext,
732
        directory=self.output_dir)
733
      final_name = os.path.basename(final_disk_path)
734
      disk_size = os.path.getsize(final_disk_path) / (1024 * 1024)
735
      results["disk%s_dump" % counter] = final_name
736
      results["disk%s_size" % counter] = str(disk_size)
737
      results["disk%s_ivname" % counter] = "disk/%s" % str(counter)
738
    if disks_list:
739
      results["disk_count"] = str(len(disks_list))
740
    return results
344 741

  
345 742
  def Save(self):
346 743
    """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
......
370 767
    for section, options in results.iteritems():
371 768
      output.append("[%s]" % section)
372 769
      for name, value in options.iteritems():
770
        if value is None:
771
          value = ""
373 772
        output.append("%s = %s" % (name, value))
374 773
      output.append("")
375 774
    output_contents = "\n".join(output)

Also available in: Unified diff