Statistics
| Branch: | Tag: | Revision:

root / lib / ovf.py @ 99381e3b

History | View | Annotate | Download (26.9 kB)

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
  @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

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

237
    @raise errors.OpPrereqError: if SHA1 checksums do not match
238

239
    """
240
    if "%s%s" % (self.schema_name, MF_EXT) in self.files_list:
241
      logging.warning("Verifying SHA1 checksums, this may take a while")
242
      manifest_filename = "%s%s" % (self.schema_name, MF_EXT)
243
      manifest_path = utils.PathJoin(self.input_dir, manifest_filename)
244
      manifest_content = utils.ReadFile(manifest_path).splitlines()
245
      manifest_files = {}
246
      regexp = r"SHA1\((\S+)\)= (\S+)"
247
      for line in manifest_content:
248
        match = re.match(regexp, line)
249
        if match:
250
          file_name = match.group(1)
251
          sha1_sum = match.group(2)
252
          manifest_files[file_name] = sha1_sum
253
      files_with_paths = [utils.PathJoin(self.input_dir, file_name)
254
        for file_name in self.files_list]
255
      sha1_sums = utils.FingerprintFiles(files_with_paths)
256
      for file_name, value in manifest_files.iteritems():
257
        if sha1_sums.get(utils.PathJoin(self.input_dir, file_name)) != value:
258
          raise errors.OpPrereqError("SHA1 checksum of %s does not match the"
259
                                     " value in manifest file" % file_name)
260
      logging.info("SHA1 checksums verified")
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

    
304

    
305
class Converter(object):
306
  """Converter class for OVF packages.
307

308
  Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is
309
  to provide a common interface for the two.
310

311
  @type options: optparse.Values
312
  @ivar options: options parsed from the command line
313
  @type output_dir: string
314
  @ivar output_dir: directory to which the results of conversion shall be
315
    written
316
  @type temp_file_manager: L{utils.TemporaryFileManager}
317
  @ivar temp_file_manager: container for temporary files created during
318
    conversion
319
  @type temp_dir: string
320
  @ivar temp_dir: temporary directory created then we deal with OVA
321

322
  """
323
  def __init__(self, input_path, options):
324
    """Initialize the converter.
325

326
    @type input_path: string
327
    @param input_path: path to the Converter input file
328
    @type options: optparse.Values
329
    @param options: command line options
330

331
    @raise errors.OpPrereqError: if file does not exist
332

333
    """
334
    input_path = os.path.abspath(input_path)
335
    if not os.path.isfile(input_path):
336
      raise errors.OpPrereqError("File does not exist: %s" % input_path)
337
    self.options = options
338
    self.temp_file_manager = utils.TemporaryFileManager()
339
    self.temp_dir = None
340
    self.output_dir = None
341
    self._ReadInputData(input_path)
342

    
343
  def _ReadInputData(self, input_path):
344
    """Reads the data on which the conversion will take place.
345

346
    @type input_path: string
347
    @param input_path: absolute path to the Converter input file
348

349
    """
350
    raise NotImplementedError()
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

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

458
    """
459
    raise NotImplementedError()
460

    
461
  def Save(self):
462
    """Saves the gathered configuration in an apropriate format.
463

464
    """
465
    raise NotImplementedError()
466

    
467
  def Cleanup(self):
468
    """Cleans the temporary directory, if one was created.
469

470
    """
471
    self.temp_file_manager.Cleanup()
472
    if self.temp_dir:
473
      shutil.rmtree(self.temp_dir)
474
      self.temp_dir = None
475

    
476

    
477
class OVFImporter(Converter):
478
  """Converter from OVF to Ganeti config file.
479

480
  @type input_dir: string
481
  @ivar input_dir: directory in which the .ovf file resides
482
  @type output_dir: string
483
  @ivar output_dir: directory to which the results of conversion shall be
484
    written
485
  @type input_path: string
486
  @ivar input_path: complete path to the .ovf file
487
  @type ovf_reader: L{OVFReader}
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
497

498
  """
499
  def _ReadInputData(self, input_path):
500
    """Reads the data on which the conversion will take place.
501

502
    @type input_path: string
503
    @param input_path: absolute path to the .ovf or .ova input file
504

505
    @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
506

507
    """
508
    (input_dir, input_file) = os.path.split(input_path)
509
    (_, input_extension) = os.path.splitext(input_file)
510

    
511
    if input_extension == OVF_EXT:
512
      logging.info("%s file extension found, no unpacking necessary", OVF_EXT)
513
      self.input_dir = input_dir
514
      self.input_path = input_path
515
      self.temp_dir = None
516
    elif input_extension == OVA_EXT:
517
      logging.info("%s file extension found, proceeding to unpacking", OVA_EXT)
518
      self._UnpackOVA(input_path)
519
    else:
520
      raise errors.OpPrereqError("Unknown file extension; expected %s or %s"
521
                                 " file" % (OVA_EXT, OVF_EXT))
522
    assert ((input_extension == OVA_EXT and self.temp_dir) or
523
            (input_extension == OVF_EXT and not self.temp_dir))
524
    assert self.input_dir in self.input_path
525

    
526
    if self.options.output_dir:
527
      self.output_dir = os.path.abspath(self.options.output_dir)
528
      if (os.path.commonprefix([constants.EXPORT_DIR, self.output_dir]) !=
529
          constants.EXPORT_DIR):
530
        logging.warning("Export path is not under %s directory, import to"
531
                        " Ganeti using gnt-backup may fail",
532
                        constants.EXPORT_DIR)
533
    else:
534
      self.output_dir = constants.EXPORT_DIR
535

    
536
    self.ovf_reader = OVFReader(self.input_path)
537
    self.ovf_reader.VerifyManifest()
538

    
539
  def _UnpackOVA(self, input_path):
540
    """Unpacks the .ova package into temporary directory.
541

542
    @type input_path: string
543
    @param input_path: path to the .ova package file
544

545
    @raise errors.OpPrereqError: if file is not a proper tarball, one of the
546
        files in the archive seem malicious (e.g. path starts with '../') or
547
        .ova package does not contain .ovf file
548

549
    """
550
    input_name = None
551
    if not tarfile.is_tarfile(input_path):
552
      raise errors.OpPrereqError("The provided %s file is not a proper tar"
553
                                 " archive", OVA_EXT)
554
    ova_content = tarfile.open(input_path)
555
    temp_dir = tempfile.mkdtemp()
556
    self.temp_dir = temp_dir
557
    for file_name in ova_content.getnames():
558
      file_normname = os.path.normpath(file_name)
559
      try:
560
        utils.PathJoin(temp_dir, file_normname)
561
      except ValueError, err:
562
        raise errors.OpPrereqError("File %s inside %s package is not safe" %
563
                                   (file_name, OVA_EXT))
564
      if file_name.endswith(OVF_EXT):
565
        input_name = file_name
566
    if not input_name:
567
      raise errors.OpPrereqError("No %s file in %s package found" %
568
                                 (OVF_EXT, OVA_EXT))
569
    logging.warning("Unpacking the %s archive, this may take a while",
570
      input_path)
571
    self.input_dir = temp_dir
572
    self.input_path = utils.PathJoin(self.temp_dir, input_name)
573
    try:
574
      try:
575
        extract = ova_content.extractall
576
      except AttributeError:
577
        # This is a prehistorical case of using python < 2.5
578
        for member in ova_content.getmembers():
579
          ova_content.extract(member, path=self.temp_dir)
580
      else:
581
        extract(self.temp_dir)
582
    except tarfile.TarError, err:
583
      raise errors.OpPrereqError("Error while extracting %s archive: %s" %
584
                                 (OVA_EXT, err))
585
    logging.info("OVA package extracted to %s directory", self.temp_dir)
586

    
587
  def Parse(self):
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
741

    
742
  def Save(self):
743
    """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
744

745
    @raise errors.OpPrereqError: when saving to config file failed
746

747
    """
748
    logging.info("Conversion was succesfull, saving %s in %s directory",
749
                 constants.EXPORT_CONF_FILE, self.output_dir)
750
    results = {
751
      constants.INISECT_INS: {},
752
      constants.INISECT_BEP: {},
753
      constants.INISECT_EXP: {},
754
      constants.INISECT_OSP: {},
755
      constants.INISECT_HYP: {},
756
    }
757

    
758
    results[constants.INISECT_INS].update(self.results_disk)
759
    results[constants.INISECT_INS]["name"] = self.results_name
760
    if self.results_template:
761
      results[constants.INISECT_INS]["disk_template"] = self.results_template
762

    
763
    output_file_name = utils.PathJoin(self.output_dir,
764
      constants.EXPORT_CONF_FILE)
765

    
766
    output = []
767
    for section, options in results.iteritems():
768
      output.append("[%s]" % section)
769
      for name, value in options.iteritems():
770
        if value is None:
771
          value = ""
772
        output.append("%s = %s" % (name, value))
773
      output.append("")
774
    output_contents = "\n".join(output)
775

    
776
    try:
777
      utils.WriteFile(output_file_name, data=output_contents)
778
    except errors.ProgrammerError, err:
779
      raise errors.OpPrereqError("Saving the config file failed: %s" % err)
780

    
781
    self.Cleanup()
782

    
783

    
784
class OVFExporter(Converter):
785
  def _ReadInputData(self, input_path):
786
    pass
787

    
788
  def Parse(self):
789
    pass
790

    
791
  def Save(self):
792
    pass