Statistics
| Branch: | Tag: | Revision:

root / lib / ovf.py @ 0963b26a

History | View | Annotate | Download (47.3 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 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
from ganeti import constants
49
from ganeti import errors
50
from ganeti import utils
51

    
52

    
53
# Schemas used in OVF format
54
GANETI_SCHEMA = "http://ganeti"
55
OVF_SCHEMA = "http://schemas.dmtf.org/ovf/envelope/1"
56
RASD_SCHEMA = ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/"
57
               "CIM_ResourceAllocationSettingData")
58
VSSD_SCHEMA = ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/"
59
               "CIM_VirtualSystemSettingData")
60
XML_SCHEMA = "http://www.w3.org/2001/XMLSchema-instance"
61

    
62
# File extensions in OVF package
63
OVA_EXT = ".ova"
64
OVF_EXT = ".ovf"
65
MF_EXT = ".mf"
66
CERT_EXT = ".cert"
67
COMPRESSION_EXT = ".gz"
68
FILE_EXTENSIONS = [
69
  OVF_EXT,
70
  MF_EXT,
71
  CERT_EXT,
72
]
73

    
74
COMPRESSION_TYPE = "gzip"
75
COMPRESS = "compression"
76
DECOMPRESS = "decompression"
77
ALLOWED_ACTIONS = [COMPRESS, DECOMPRESS]
78

    
79
# ResourceType values
80
RASD_TYPE = {
81
  "vcpus": "3",
82
  "memory": "4",
83
}
84

    
85
# AllocationUnits values and conversion
86
ALLOCATION_UNITS = {
87
  'b': ["bytes", "b"],
88
  'kb': ["kilobytes", "kb", "byte * 2^10", "kibibytes", "kib"],
89
  'mb': ["megabytes", "mb", "byte * 2^20", "mebibytes", "mib"],
90
  'gb': ["gigabytes", "gb", "byte * 2^30", "gibibytes", "gib"],
91
}
92
CONVERT_UNITS_TO_MB = {
93
  'b': lambda x: x / (1024 * 1024),
94
  'kb': lambda x: x / 1024,
95
  'mb': lambda x: x,
96
  'gb': lambda x: x * 1024,
97
}
98

    
99
# Names of the config fields
100
NAME = "name"
101

    
102

    
103
def LinkFile(old_path, prefix=None, suffix=None, directory=None):
104
  """Create link with a given prefix and suffix.
105

106
  This is a wrapper over os.link. It tries to create a hard link for given file,
107
  but instead of rising error when file exists, the function changes the name
108
  a little bit.
109

110
  @type old_path:string
111
  @param old_path: path to the file that is to be linked
112
  @type prefix: string
113
  @param prefix: prefix of filename for the link
114
  @type suffix: string
115
  @param suffix: suffix of the filename for the link
116
  @type directory: string
117
  @param directory: directory of the link
118

119
  @raise errors.OpPrereqError: when error on linking is different than
120
    "File exists"
121

122
  """
123
  assert(prefix is not None or suffix is not None)
124
  if directory is None:
125
    directory = os.getcwd()
126
  new_path = utils.PathJoin(directory, "%s%s" % (prefix, suffix))
127
  counter = 1
128
  while True:
129
    try:
130
      os.link(old_path, new_path)
131
      break
132
    except OSError, err:
133
      if err.errno == errno.EEXIST:
134
        new_path = utils.PathJoin(directory,
135
          "%s_%s%s" % (prefix, counter, suffix))
136
        counter += 1
137
      else:
138
        raise errors.OpPrereqError("Error moving the file %s to %s location:"
139
                                   " %s" % (old_path, new_path, err))
140
  return new_path
141

    
142

    
143
class OVFReader(object):
144
  """Reader class for OVF files.
145

146
  @type files_list: list
147
  @ivar files_list: list of files in the OVF package
148
  @type tree: ET.ElementTree
149
  @ivar tree: XML tree of the .ovf file
150
  @type schema_name: string
151
  @ivar schema_name: name of the .ovf file
152
  @type input_dir: string
153
  @ivar input_dir: directory in which the .ovf file resides
154

155
  """
156
  def __init__(self, input_path):
157
    """Initialiaze the reader - load the .ovf file to XML parser.
158

159
    It is assumed that names of manifesto (.mf), certificate (.cert) and ovf
160
    files are the same. In order to account any other files as part of the ovf
161
    package, they have to be explicitly mentioned in the Resources section
162
    of the .ovf file.
163

164
    @type input_path: string
165
    @param input_path: absolute path to the .ovf file
166

167
    @raise errors.OpPrereqError: when .ovf file is not a proper XML file or some
168
      of the files mentioned in Resources section do not exist
169

170
    """
171
    self.tree = ET.ElementTree()
172
    try:
173
      self.tree.parse(input_path)
174
    except xml.parsers.expat.ExpatError, err:
175
      raise errors.OpPrereqError("Error while reading %s file: %s" %
176
                                 (OVF_EXT, err))
177

    
178
    # Create a list of all files in the OVF package
179
    (input_dir, input_file) = os.path.split(input_path)
180
    (input_name, _) = os.path.splitext(input_file)
181
    files_directory = utils.ListVisibleFiles(input_dir)
182
    files_list = []
183
    for file_name in files_directory:
184
      (name, extension) = os.path.splitext(file_name)
185
      if extension in FILE_EXTENSIONS and name == input_name:
186
        files_list.append(file_name)
187
    files_list += self._GetAttributes("{%s}References/{%s}File" %
188
                                      (OVF_SCHEMA, OVF_SCHEMA),
189
                                      "{%s}href" % OVF_SCHEMA)
190
    for file_name in files_list:
191
      file_path = utils.PathJoin(input_dir, file_name)
192
      if not os.path.exists(file_path):
193
        raise errors.OpPrereqError("File does not exist: %s" % file_path)
194
    logging.info("Files in the OVF package: %s", " ".join(files_list))
195
    self.files_list = files_list
196
    self.input_dir = input_dir
197
    self.schema_name = input_name
198

    
199
  def _GetAttributes(self, path, attribute):
200
    """Get specified attribute from all nodes accessible using given path.
201

202
    Function follows the path from root node to the desired tags using path,
203
    then reads the apropriate attribute values.
204

205
    @type path: string
206
    @param path: path of nodes to visit
207
    @type attribute: string
208
    @param attribute: attribute for which we gather the information
209
    @rtype: list
210
    @return: for each accessible tag with the attribute value set, value of the
211
      attribute
212

213
    """
214
    current_list = self.tree.findall(path)
215
    results = [x.get(attribute) for x in current_list]
216
    return filter(None, results)
217

    
218
  def _GetElementMatchingAttr(self, path, match_attr):
219
    """Searches for element on a path that matches certain attribute value.
220

221
    Function follows the path from root node to the desired tags using path,
222
    then searches for the first one matching the attribute value.
223

224
    @type path: string
225
    @param path: path of nodes to visit
226
    @type match_attr: tuple
227
    @param match_attr: pair (attribute, value) for which we search
228
    @rtype: ET.ElementTree or None
229
    @return: first element matching match_attr or None if nothing matches
230

231
    """
232
    potential_elements = self.tree.findall(path)
233
    (attr, val) = match_attr
234
    for elem in potential_elements:
235
      if elem.get(attr) == val:
236
        return elem
237
    return None
238

    
239
  def _GetElementMatchingText(self, path, match_text):
240
    """Searches for element on a path that matches certain text value.
241

242
    Function follows the path from root node to the desired tags using path,
243
    then searches for the first one matching the text value.
244

245
    @type path: string
246
    @param path: path of nodes to visit
247
    @type match_text: tuple
248
    @param match_text: pair (node, text) for which we search
249
    @rtype: ET.ElementTree or None
250
    @return: first element matching match_text or None if nothing matches
251

252
    """
253
    potential_elements = self.tree.findall(path)
254
    (node, text) = match_text
255
    for elem in potential_elements:
256
      if elem.findtext(node) == text:
257
        return elem
258
    return None
259

    
260
  @staticmethod
261
  def _GetDictParameters(root, schema):
262
    """Reads text in all children and creates the dictionary from the contents.
263

264
    @type root: ET.ElementTree or None
265
    @param root: father of the nodes we want to collect data about
266
    @type schema: string
267
    @param schema: schema name to be removed from the tag
268
    @rtype: dict
269
    @return: dictionary containing tags and their text contents, tags have their
270
      schema fragment removed or empty dictionary, when root is None
271

272
    """
273
    if not root:
274
      return {}
275
    results = {}
276
    for element in list(root):
277
      pref_len = len("{%s}" % schema)
278
      assert(schema in element.tag)
279
      tag = element.tag[pref_len:]
280
      results[tag] = element.text
281
    return results
282

    
283
  def VerifyManifest(self):
284
    """Verifies manifest for the OVF package, if one is given.
285

286
    @raise errors.OpPrereqError: if SHA1 checksums do not match
287

288
    """
289
    if "%s%s" % (self.schema_name, MF_EXT) in self.files_list:
290
      logging.warning("Verifying SHA1 checksums, this may take a while")
291
      manifest_filename = "%s%s" % (self.schema_name, MF_EXT)
292
      manifest_path = utils.PathJoin(self.input_dir, manifest_filename)
293
      manifest_content = utils.ReadFile(manifest_path).splitlines()
294
      manifest_files = {}
295
      regexp = r"SHA1\((\S+)\)= (\S+)"
296
      for line in manifest_content:
297
        match = re.match(regexp, line)
298
        if match:
299
          file_name = match.group(1)
300
          sha1_sum = match.group(2)
301
          manifest_files[file_name] = sha1_sum
302
      files_with_paths = [utils.PathJoin(self.input_dir, file_name)
303
        for file_name in self.files_list]
304
      sha1_sums = utils.FingerprintFiles(files_with_paths)
305
      for file_name, value in manifest_files.iteritems():
306
        if sha1_sums.get(utils.PathJoin(self.input_dir, file_name)) != value:
307
          raise errors.OpPrereqError("SHA1 checksum of %s does not match the"
308
                                     " value in manifest file" % file_name)
309
      logging.info("SHA1 checksums verified")
310

    
311
  def GetInstanceName(self):
312
    """Provides information about instance name.
313

314
    @rtype: string
315
    @return: instance name string
316

317
    """
318
    find_name = "{%s}VirtualSystem/{%s}Name" % (OVF_SCHEMA, OVF_SCHEMA)
319
    return self.tree.findtext(find_name)
320

    
321
  def GetDiskTemplate(self):
322
    """Returns disk template from .ovf file
323

324
    @rtype: string or None
325
    @return: name of the template
326
    """
327
    find_template = ("{%s}GanetiSection/{%s}DiskTemplate" %
328
                     (GANETI_SCHEMA, GANETI_SCHEMA))
329
    return self.tree.findtext(find_template)
330

    
331
  def GetHypervisorData(self):
332
    """Provides hypervisor information - hypervisor name and options.
333

334
    @rtype: dict
335
    @return: dictionary containing name of the used hypervisor and all the
336
      specified options
337

338
    """
339
    hypervisor_search = ("{%s}GanetiSection/{%s}Hypervisor" %
340
                         (GANETI_SCHEMA, GANETI_SCHEMA))
341
    hypervisor_data = self.tree.find(hypervisor_search)
342
    if not hypervisor_data:
343
      return {"hypervisor_name": constants.VALUE_AUTO}
344
    results = {
345
      "hypervisor_name": hypervisor_data.findtext("{%s}Name" % GANETI_SCHEMA,
346
                           default=constants.VALUE_AUTO),
347
    }
348
    parameters = hypervisor_data.find("{%s}Parameters" % GANETI_SCHEMA)
349
    results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
350
    return results
351

    
352
  def GetOSData(self):
353
    """ Provides operating system information - os name and options.
354

355
    @rtype: dict
356
    @return: dictionary containing name and options for the chosen OS
357

358
    """
359
    results = {}
360
    os_search = ("{%s}GanetiSection/{%s}OperatingSystem" %
361
                 (GANETI_SCHEMA, GANETI_SCHEMA))
362
    os_data = self.tree.find(os_search)
363
    if os_data:
364
      results["os_name"] = os_data.findtext("{%s}Name" % GANETI_SCHEMA)
365
      parameters = os_data.find("{%s}Parameters" % GANETI_SCHEMA)
366
      results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
367
    return results
368

    
369
  def GetBackendData(self):
370
    """ Provides backend information - vcpus, memory, auto balancing options.
371

372
    @rtype: dict
373
    @return: dictionary containing options for vcpus, memory and auto balance
374
      settings
375

376
    """
377
    results = {}
378

    
379
    find_vcpus = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item" %
380
                   (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
381
    match_vcpus = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["vcpus"])
382
    vcpus = self._GetElementMatchingText(find_vcpus, match_vcpus)
383
    if vcpus:
384
      vcpus_count = vcpus.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
385
        default=constants.VALUE_AUTO)
386
    else:
387
      vcpus_count = constants.VALUE_AUTO
388
    results["vcpus"] = str(vcpus_count)
389

    
390
    find_memory = find_vcpus
391
    match_memory = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["memory"])
392
    memory = self._GetElementMatchingText(find_memory, match_memory)
393
    memory_raw = None
394
    if memory:
395
      alloc_units = memory.findtext("{%s}AllocationUnits" % RASD_SCHEMA)
396
      matching_units = [units for units, variants in
397
        ALLOCATION_UNITS.iteritems() if alloc_units.lower() in variants]
398
      if matching_units == []:
399
        raise errors.OpPrereqError("Unit %s for RAM memory unknown",
400
          alloc_units)
401
      units = matching_units[0]
402
      memory_raw = int(memory.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
403
            default=constants.VALUE_AUTO))
404
      memory_count = CONVERT_UNITS_TO_MB[units](memory_raw)
405
    else:
406
      memory_count = constants.VALUE_AUTO
407
    results["memory"] = str(memory_count)
408

    
409
    find_balance = ("{%s}GanetiSection/{%s}AutoBalance" %
410
                   (GANETI_SCHEMA, GANETI_SCHEMA))
411
    balance = self.tree.findtext(find_balance, default=constants.VALUE_AUTO)
412
    results["auto_balance"] = balance
413

    
414
    return results
415

    
416
  def GetTagsData(self):
417
    """Provides tags information for instance.
418

419
    @rtype: string or None
420
    @return: string of comma-separated tags for the instance
421

422
    """
423
    find_tags = "{%s}GanetiSection/{%s}Tags" % (GANETI_SCHEMA, GANETI_SCHEMA)
424
    results = self.tree.findtext(find_tags)
425
    if results:
426
      return results
427
    else:
428
      return None
429

    
430
  def GetVersionData(self):
431
    """Provides version number read from .ovf file
432

433
    @rtype: string
434
    @return: string containing the version number
435

436
    """
437
    find_version = ("{%s}GanetiSection/{%s}Version" %
438
                    (GANETI_SCHEMA, GANETI_SCHEMA))
439
    return self.tree.findtext(find_version)
440

    
441
  def GetNetworkData(self):
442
    """Provides data about the network in the OVF instance.
443

444
    The method gathers the data about networks used by OVF instance. It assumes
445
    that 'name' tag means something - in essence, if it contains one of the
446
    words 'bridged' or 'routed' then that will be the mode of this network in
447
    Ganeti. The information about the network can be either in GanetiSection or
448
    VirtualHardwareSection.
449

450
    @rtype: dict
451
    @return: dictionary containing all the network information
452

453
    """
454
    results = {}
455
    networks_search = ("{%s}NetworkSection/{%s}Network" %
456
                       (OVF_SCHEMA, OVF_SCHEMA))
457
    network_names = self._GetAttributes(networks_search,
458
      "{%s}name" % OVF_SCHEMA)
459
    required = ["ip", "mac", "link", "mode"]
460
    for (counter, network_name) in enumerate(network_names):
461
      network_search = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item"
462
                        % (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
463
      ganeti_search = ("{%s}GanetiSection/{%s}Network/{%s}Nic" %
464
                       (GANETI_SCHEMA, GANETI_SCHEMA, GANETI_SCHEMA))
465
      network_match = ("{%s}Connection" % RASD_SCHEMA, network_name)
466
      ganeti_match = ("{%s}name" % OVF_SCHEMA, network_name)
467
      network_data = self._GetElementMatchingText(network_search, network_match)
468
      network_ganeti_data = self._GetElementMatchingAttr(ganeti_search,
469
        ganeti_match)
470

    
471
      ganeti_data = {}
472
      if network_ganeti_data:
473
        ganeti_data["mode"] = network_ganeti_data.findtext("{%s}Mode" %
474
                                                           GANETI_SCHEMA)
475
        ganeti_data["mac"] = network_ganeti_data.findtext("{%s}MACAddress" %
476
                                                          GANETI_SCHEMA)
477
        ganeti_data["ip"] = network_ganeti_data.findtext("{%s}IPAddress" %
478
                                                         GANETI_SCHEMA)
479
        ganeti_data["link"] = network_ganeti_data.findtext("{%s}Link" %
480
                                                           GANETI_SCHEMA)
481
      mac_data = None
482
      if network_data:
483
        mac_data = network_data.findtext("{%s}Address" % RASD_SCHEMA)
484

    
485
      network_name = network_name.lower()
486

    
487
      # First, some not Ganeti-specific information is collected
488
      if constants.NIC_MODE_BRIDGED in network_name:
489
        results["nic%s_mode" % counter] = "bridged"
490
      elif constants.NIC_MODE_ROUTED in network_name:
491
        results["nic%s_mode" % counter] = "routed"
492
      results["nic%s_mac" % counter] = mac_data
493

    
494
      # GanetiSection data overrides 'manually' collected data
495
      for name, value in ganeti_data.iteritems():
496
        results["nic%s_%s" % (counter, name)] = value
497

    
498
      # Bridged network has no IP - unless specifically stated otherwise
499
      if (results.get("nic%s_mode" % counter) == "bridged" and
500
          not results.get("nic%s_ip" % counter)):
501
        results["nic%s_ip" % counter] = constants.VALUE_NONE
502

    
503
      for option in required:
504
        if not results.get("nic%s_%s" % (counter, option)):
505
          results["nic%s_%s" % (counter, option)] = constants.VALUE_AUTO
506

    
507
    if network_names:
508
      results["nic_count"] = str(len(network_names))
509
    return results
510

    
511
  def GetDisksNames(self):
512
    """Provides list of file names for the disks used by the instance.
513

514
    @rtype: list
515
    @return: list of file names, as referenced in .ovf file
516

517
    """
518
    results = []
519
    disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA)
520
    disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA)
521
    for disk in disk_ids:
522
      disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA)
523
      disk_match = ("{%s}id" % OVF_SCHEMA, disk)
524
      disk_elem = self._GetElementMatchingAttr(disk_search, disk_match)
525
      if disk_elem is None:
526
        raise errors.OpPrereqError("%s file corrupted - disk %s not found in"
527
                                   " references" % (OVF_EXT, disk))
528
      disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA)
529
      disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA)
530
      results.append((disk_name, disk_compression))
531
    return results
532

    
533

    
534
class OVFWriter(object):
535
  """Writer class for OVF files.
536

537
  @type tree: ET.ElementTree
538
  @ivar tree: XML tree that we are constructing
539

540
  """
541
  def __init__(self, has_gnt_section):
542
    """Initialize the writer - set the top element.
543

544
    @type has_gnt_section: bool
545
    @param has_gnt_section: if the Ganeti schema should be added - i.e. this
546
      means that Ganeti section will be present
547

548
    """
549
    env_attribs = {
550
      "xmlns:xsi": XML_SCHEMA,
551
      "xmlns:vssd": VSSD_SCHEMA,
552
      "xmlns:rasd": RASD_SCHEMA,
553
      "xmlns:ovf": OVF_SCHEMA,
554
      "xmlns": OVF_SCHEMA,
555
      "xml:lang": "en-US",
556
    }
557
    if has_gnt_section:
558
      env_attribs["xmlns:gnt"] = GANETI_SCHEMA
559
    self.tree = ET.Element("Envelope", attrib=env_attribs)
560

    
561
  def PrettyXmlDump(self):
562
    """Formatter of the XML file.
563

564
    @rtype: string
565
    @return: XML tree in the form of nicely-formatted string
566

567
    """
568
    raw_string = ET.tostring(self.tree)
569
    parsed_xml = xml.dom.minidom.parseString(raw_string)
570
    xml_string = parsed_xml.toprettyxml(indent="  ")
571
    text_re = re.compile(">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL)
572
    return text_re.sub(">\g<1></", xml_string)
573

    
574

    
575
class Converter(object):
576
  """Converter class for OVF packages.
577

578
  Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is
579
  to provide a common interface for the two.
580

581
  @type options: optparse.Values
582
  @ivar options: options parsed from the command line
583
  @type output_dir: string
584
  @ivar output_dir: directory to which the results of conversion shall be
585
    written
586
  @type temp_file_manager: L{utils.TemporaryFileManager}
587
  @ivar temp_file_manager: container for temporary files created during
588
    conversion
589
  @type temp_dir: string
590
  @ivar temp_dir: temporary directory created then we deal with OVA
591

592
  """
593
  def __init__(self, input_path, options):
594
    """Initialize the converter.
595

596
    @type input_path: string
597
    @param input_path: path to the Converter input file
598
    @type options: optparse.Values
599
    @param options: command line options
600

601
    @raise errors.OpPrereqError: if file does not exist
602

603
    """
604
    input_path = os.path.abspath(input_path)
605
    if not os.path.isfile(input_path):
606
      raise errors.OpPrereqError("File does not exist: %s" % input_path)
607
    self.options = options
608
    self.temp_file_manager = utils.TemporaryFileManager()
609
    self.temp_dir = None
610
    self.output_dir = None
611
    self._ReadInputData(input_path)
612

    
613
  def _ReadInputData(self, input_path):
614
    """Reads the data on which the conversion will take place.
615

616
    @type input_path: string
617
    @param input_path: absolute path to the Converter input file
618

619
    """
620
    raise NotImplementedError()
621

    
622
  def _CompressDisk(self, disk_path, compression, action):
623
    """Performs (de)compression on the disk and returns the new path
624

625
    @type disk_path: string
626
    @param disk_path: path to the disk
627
    @type compression: string
628
    @param compression: compression type
629
    @type action: string
630
    @param action: whether the action is compression or decompression
631
    @rtype: string
632
    @return: new disk path after (de)compression
633

634
    @raise errors.OpPrereqError: disk (de)compression failed or "compression"
635
      is not supported
636

637
    """
638
    assert(action in ALLOWED_ACTIONS)
639
    # For now we only support gzip, as it is used in ovftool
640
    if compression != COMPRESSION_TYPE:
641
      raise errors.OpPrereqError("Unsupported compression type: %s"
642
                                 % compression)
643
    disk_file = os.path.basename(disk_path)
644
    if action == DECOMPRESS:
645
      (disk_name, _) = os.path.splitext(disk_file)
646
      prefix = disk_name
647
    elif action == COMPRESS:
648
      prefix = disk_file
649
    new_path = utils.GetClosedTempfile(suffix=COMPRESSION_EXT, prefix=prefix,
650
      dir=self.output_dir)
651
    self.temp_file_manager.Add(new_path)
652
    args = ["gzip", "-c", disk_path]
653
    run_result = utils.RunCmd(args, output=new_path)
654
    if run_result.failed:
655
      raise errors.OpPrereqError("Disk %s failed with output: %s"
656
                                 % (action, run_result.stderr))
657
    logging.info("The %s of the disk is completed", action)
658
    return (COMPRESSION_EXT, new_path)
659

    
660
  def _ConvertDisk(self, disk_format, disk_path):
661
    """Performes conversion to specified format.
662

663
    @type disk_format: string
664
    @param disk_format: format to which the disk should be converted
665
    @type disk_path: string
666
    @param disk_path: path to the disk that should be converted
667
    @rtype: string
668
    @return path to the output disk
669

670
    @raise errors.OpPrereqError: convertion of the disk failed
671

672
    """
673
    disk_file = os.path.basename(disk_path)
674
    (disk_name, disk_extension) = os.path.splitext(disk_file)
675
    if disk_extension != disk_format:
676
      logging.warning("Conversion of disk image to %s format, this may take"
677
                      " a while", disk_format)
678

    
679
    new_disk_path = utils.GetClosedTempfile(suffix=".%s" % disk_format,
680
      prefix=disk_name, dir=self.output_dir)
681
    self.temp_file_manager.Add(new_disk_path)
682
    args = [
683
      "qemu-img",
684
      "convert",
685
      "-O",
686
      disk_format,
687
      disk_path,
688
      new_disk_path,
689
    ]
690
    run_result = utils.RunCmd(args, cwd=os.getcwd())
691
    if run_result.failed:
692
      raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was"
693
                                 ": %s" % (disk_format, run_result.stderr))
694
    return (".%s" % disk_format, new_disk_path)
695

    
696
  @staticmethod
697
  def _GetDiskQemuInfo(disk_path, regexp):
698
    """Figures out some information of the disk using qemu-img.
699

700
    @type disk_path: string
701
    @param disk_path: path to the disk we want to know the format of
702
    @type regexp: string
703
    @param regexp: string that has to be matched, it has to contain one group
704
    @rtype: string
705
    @return: disk format
706

707
    @raise errors.OpPrereqError: format information cannot be retrieved
708

709
    """
710
    args = ["qemu-img", "info", disk_path]
711
    run_result = utils.RunCmd(args, cwd=os.getcwd())
712
    if run_result.failed:
713
      raise errors.OpPrereqError("Gathering info about the disk using qemu-img"
714
                                 " failed, output was: %s" % run_result.stderr)
715
    result = run_result.output
716
    regexp = r"%s" % regexp
717
    match = re.search(regexp, result)
718
    if match:
719
      disk_format = match.group(1)
720
    else:
721
      raise errors.OpPrereqError("No file information matching %s found in:"
722
                                 " %s" % (regexp, result))
723
    return disk_format
724

    
725
  def Parse(self):
726
    """Parses the data and creates a structure containing all required info.
727

728
    """
729
    raise NotImplementedError()
730

    
731
  def Save(self):
732
    """Saves the gathered configuration in an apropriate format.
733

734
    """
735
    raise NotImplementedError()
736

    
737
  def Cleanup(self):
738
    """Cleans the temporary directory, if one was created.
739

740
    """
741
    self.temp_file_manager.Cleanup()
742
    if self.temp_dir:
743
      shutil.rmtree(self.temp_dir)
744
      self.temp_dir = None
745

    
746

    
747
class OVFImporter(Converter):
748
  """Converter from OVF to Ganeti config file.
749

750
  @type input_dir: string
751
  @ivar input_dir: directory in which the .ovf file resides
752
  @type output_dir: string
753
  @ivar output_dir: directory to which the results of conversion shall be
754
    written
755
  @type input_path: string
756
  @ivar input_path: complete path to the .ovf file
757
  @type ovf_reader: L{OVFReader}
758
  @ivar ovf_reader: OVF reader instance collects data from .ovf file
759
  @type results_name: string
760
  @ivar results_name: name of imported instance
761
  @type results_template: string
762
  @ivar results_template: disk template read from .ovf file or command line
763
    arguments
764
  @type results_hypervisor: dict
765
  @ivar results_hypervisor: hypervisor information gathered from .ovf file or
766
    command line arguments
767
  @type results_os: dict
768
  @ivar results_os: operating system information gathered from .ovf file or
769
    command line arguments
770
  @type results_backend: dict
771
  @ivar results_backend: backend information gathered from .ovf file or
772
    command line arguments
773
  @type results_tags: string
774
  @ivar results_tags: string containing instance-specific tags
775
  @type results_version: string
776
  @ivar results_version: version as required by Ganeti import
777
  @type results_network: dict
778
  @ivar results_network: network information gathered from .ovf file or command
779
    line arguments
780
  @type results_disk: dict
781
  @ivar results_disk: disk information gathered from .ovf file or command line
782
    arguments
783

784
  """
785
  def _ReadInputData(self, input_path):
786
    """Reads the data on which the conversion will take place.
787

788
    @type input_path: string
789
    @param input_path: absolute path to the .ovf or .ova input file
790

791
    @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
792

793
    """
794
    (input_dir, input_file) = os.path.split(input_path)
795
    (_, input_extension) = os.path.splitext(input_file)
796

    
797
    if input_extension == OVF_EXT:
798
      logging.info("%s file extension found, no unpacking necessary", OVF_EXT)
799
      self.input_dir = input_dir
800
      self.input_path = input_path
801
      self.temp_dir = None
802
    elif input_extension == OVA_EXT:
803
      logging.info("%s file extension found, proceeding to unpacking", OVA_EXT)
804
      self._UnpackOVA(input_path)
805
    else:
806
      raise errors.OpPrereqError("Unknown file extension; expected %s or %s"
807
                                 " file" % (OVA_EXT, OVF_EXT))
808
    assert ((input_extension == OVA_EXT and self.temp_dir) or
809
            (input_extension == OVF_EXT and not self.temp_dir))
810
    assert self.input_dir in self.input_path
811

    
812
    if self.options.output_dir:
813
      self.output_dir = os.path.abspath(self.options.output_dir)
814
      if (os.path.commonprefix([constants.EXPORT_DIR, self.output_dir]) !=
815
          constants.EXPORT_DIR):
816
        logging.warning("Export path is not under %s directory, import to"
817
                        " Ganeti using gnt-backup may fail",
818
                        constants.EXPORT_DIR)
819
    else:
820
      self.output_dir = constants.EXPORT_DIR
821

    
822
    self.ovf_reader = OVFReader(self.input_path)
823
    self.ovf_reader.VerifyManifest()
824

    
825
  def _UnpackOVA(self, input_path):
826
    """Unpacks the .ova package into temporary directory.
827

828
    @type input_path: string
829
    @param input_path: path to the .ova package file
830

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

835
    """
836
    input_name = None
837
    if not tarfile.is_tarfile(input_path):
838
      raise errors.OpPrereqError("The provided %s file is not a proper tar"
839
                                 " archive", OVA_EXT)
840
    ova_content = tarfile.open(input_path)
841
    temp_dir = tempfile.mkdtemp()
842
    self.temp_dir = temp_dir
843
    for file_name in ova_content.getnames():
844
      file_normname = os.path.normpath(file_name)
845
      try:
846
        utils.PathJoin(temp_dir, file_normname)
847
      except ValueError, err:
848
        raise errors.OpPrereqError("File %s inside %s package is not safe" %
849
                                   (file_name, OVA_EXT))
850
      if file_name.endswith(OVF_EXT):
851
        input_name = file_name
852
    if not input_name:
853
      raise errors.OpPrereqError("No %s file in %s package found" %
854
                                 (OVF_EXT, OVA_EXT))
855
    logging.warning("Unpacking the %s archive, this may take a while",
856
      input_path)
857
    self.input_dir = temp_dir
858
    self.input_path = utils.PathJoin(self.temp_dir, input_name)
859
    try:
860
      try:
861
        extract = ova_content.extractall
862
      except AttributeError:
863
        # This is a prehistorical case of using python < 2.5
864
        for member in ova_content.getmembers():
865
          ova_content.extract(member, path=self.temp_dir)
866
      else:
867
        extract(self.temp_dir)
868
    except tarfile.TarError, err:
869
      raise errors.OpPrereqError("Error while extracting %s archive: %s" %
870
                                 (OVA_EXT, err))
871
    logging.info("OVA package extracted to %s directory", self.temp_dir)
872

    
873
  def Parse(self):
874
    """Parses the data and creates a structure containing all required info.
875

876
    The method reads the information given either as a command line option or as
877
    a part of the OVF description.
878

879
    @raise errors.OpPrereqError: if some required part of the description of
880
      virtual instance is missing or unable to create output directory
881

882
    """
883
    self.results_name = self._GetInfo("instance name", self.options.name,
884
      self._ParseNameOptions, self.ovf_reader.GetInstanceName)
885
    if not self.results_name:
886
      raise errors.OpPrereqError("Name of instance not provided")
887

    
888
    self.output_dir = utils.PathJoin(self.output_dir, self.results_name)
889
    try:
890
      utils.Makedirs(self.output_dir)
891
    except OSError, err:
892
      raise errors.OpPrereqError("Failed to create directory %s: %s" %
893
                                 (self.output_dir, err))
894

    
895
    self.results_template = self._GetInfo("disk template",
896
      self.options.disk_template, self._ParseTemplateOptions,
897
      self.ovf_reader.GetDiskTemplate)
898
    if not self.results_template:
899
      logging.info("Disk template not given")
900

    
901
    self.results_hypervisor = self._GetInfo("hypervisor",
902
      self.options.hypervisor, self._ParseHypervisorOptions,
903
      self.ovf_reader.GetHypervisorData)
904
    assert self.results_hypervisor["hypervisor_name"]
905
    if self.results_hypervisor["hypervisor_name"] == constants.VALUE_AUTO:
906
      logging.debug("Default hypervisor settings from the cluster will be used")
907

    
908
    self.results_os = self._GetInfo("OS", self.options.os,
909
      self._ParseOSOptions, self.ovf_reader.GetOSData)
910
    if not self.results_os.get("os_name"):
911
      raise errors.OpPrereqError("OS name must be provided")
912

    
913
    self.results_backend = self._GetInfo("backend", self.options.beparams,
914
      self._ParseBackendOptions, self.ovf_reader.GetBackendData)
915
    assert self.results_backend.get("vcpus")
916
    assert self.results_backend.get("memory")
917
    assert self.results_backend.get("auto_balance") is not None
918

    
919
    self.results_tags = self._GetInfo("tags", self.options.tags,
920
      self._ParseTags, self.ovf_reader.GetTagsData)
921

    
922
    ovf_version = self.ovf_reader.GetVersionData()
923
    if ovf_version:
924
      self.results_version = ovf_version
925
    else:
926
      self.results_version = constants.EXPORT_VERSION
927

    
928
    self.results_network = self._GetInfo("network", self.options.nics,
929
      self._ParseNicOptions, self.ovf_reader.GetNetworkData,
930
      ignore_test=self.options.no_nics)
931

    
932
    self.results_disk = self._GetInfo("disk", self.options.disks,
933
      self._ParseDiskOptions, self._GetDiskInfo,
934
      ignore_test=self.results_template == constants.DT_DISKLESS)
935

    
936
    if not self.results_disk and not self.results_network:
937
      raise errors.OpPrereqError("Either disk specification or network"
938
                                 " description must be present")
939

    
940
  @staticmethod
941
  def _GetInfo(name, cmd_arg, cmd_function, nocmd_function,
942
    ignore_test=False):
943
    """Get information about some section - e.g. disk, network, hypervisor.
944

945
    @type name: string
946
    @param name: name of the section
947
    @type cmd_arg: dict
948
    @param cmd_arg: command line argument specific for section 'name'
949
    @type cmd_function: callable
950
    @param cmd_function: function to call if 'cmd_args' exists
951
    @type nocmd_function: callable
952
    @param nocmd_function: function to call if 'cmd_args' is not there
953

954
    """
955
    if ignore_test:
956
      logging.info("Information for %s will be ignored", name)
957
      return {}
958
    if cmd_arg:
959
      logging.info("Information for %s will be parsed from command line", name)
960
      results = cmd_function()
961
    else:
962
      logging.info("Information for %s will be parsed from %s file",
963
        name, OVF_EXT)
964
      results = nocmd_function()
965
    logging.info("Options for %s were succesfully read", name)
966
    return results
967

    
968
  def _ParseNameOptions(self):
969
    """Returns name if one was given in command line.
970

971
    @rtype: string
972
    @return: name of an instance
973

974
    """
975
    return self.options.name
976

    
977
  def _ParseTemplateOptions(self):
978
    """Returns disk template if one was given in command line.
979

980
    @rtype: string
981
    @return: disk template name
982

983
    """
984
    return self.options.disk_template
985

    
986
  def _ParseHypervisorOptions(self):
987
    """Parses hypervisor options given in a command line.
988

989
    @rtype: dict
990
    @return: dictionary containing name of the chosen hypervisor and all the
991
      options
992

993
    """
994
    assert type(self.options.hypervisor) is tuple
995
    assert len(self.options.hypervisor) == 2
996
    results = {}
997
    if self.options.hypervisor[0]:
998
      results["hypervisor_name"] = self.options.hypervisor[0]
999
    else:
1000
      results["hypervisor_name"] = constants.VALUE_AUTO
1001
    results.update(self.options.hypervisor[1])
1002
    return results
1003

    
1004
  def _ParseOSOptions(self):
1005
    """Parses OS options given in command line.
1006

1007
    @rtype: dict
1008
    @return: dictionary containing name of chosen OS and all its options
1009

1010
    """
1011
    assert self.options.os
1012
    results = {}
1013
    results["os_name"] = self.options.os
1014
    results.update(self.options.osparams)
1015
    return results
1016

    
1017
  def _ParseBackendOptions(self):
1018
    """Parses backend options given in command line.
1019

1020
    @rtype: dict
1021
    @return: dictionary containing vcpus, memory and auto-balance options
1022

1023
    """
1024
    assert self.options.beparams
1025
    backend = {}
1026
    backend.update(self.options.beparams)
1027
    must_contain = ["vcpus", "memory", "auto_balance"]
1028
    for element in must_contain:
1029
      if backend.get(element) is None:
1030
        backend[element] = constants.VALUE_AUTO
1031
    return backend
1032

    
1033
  def _ParseTags(self):
1034
    """Returns tags list given in command line.
1035

1036
    @rtype: string
1037
    @return: string containing comma-separated tags
1038

1039
    """
1040
    return self.options.tags
1041

    
1042
  def _ParseNicOptions(self):
1043
    """Parses network options given in a command line or as a dictionary.
1044

1045
    @rtype: dict
1046
    @return: dictionary of network-related options
1047

1048
    """
1049
    assert self.options.nics
1050
    results = {}
1051
    for (nic_id, nic_desc) in self.options.nics:
1052
      results["nic%s_mode" % nic_id] = \
1053
        nic_desc.get("mode", constants.VALUE_AUTO)
1054
      results["nic%s_mac" % nic_id] = nic_desc.get("mac", constants.VALUE_AUTO)
1055
      results["nic%s_link" % nic_id] = \
1056
        nic_desc.get("link", constants.VALUE_AUTO)
1057
      if nic_desc.get("mode") == "bridged":
1058
        results["nic%s_ip" % nic_id] = constants.VALUE_NONE
1059
      else:
1060
        results["nic%s_ip" % nic_id] = constants.VALUE_AUTO
1061
    results["nic_count"] = str(len(self.options.nics))
1062
    return results
1063

    
1064
  def _ParseDiskOptions(self):
1065
    """Parses disk options given in a command line.
1066

1067
    @rtype: dict
1068
    @return: dictionary of disk-related options
1069

1070
    @raise errors.OpPrereqError: disk description does not contain size
1071
      information or size information is invalid or creation failed
1072

1073
    """
1074
    assert self.options.disks
1075
    results = {}
1076
    for (disk_id, disk_desc) in self.options.disks:
1077
      results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id
1078
      if disk_desc.get("size"):
1079
        try:
1080
          disk_size = utils.ParseUnit(disk_desc["size"])
1081
        except ValueError:
1082
          raise errors.OpPrereqError("Invalid disk size for disk %s: %s" %
1083
                                     (disk_id, disk_desc["size"]))
1084
        new_path = utils.PathJoin(self.output_dir, str(disk_id))
1085
        args = [
1086
          "qemu-img",
1087
          "create",
1088
          "-f",
1089
          "raw",
1090
          new_path,
1091
          disk_size,
1092
        ]
1093
        run_result = utils.RunCmd(args)
1094
        if run_result.failed:
1095
          raise errors.OpPrereqError("Creation of disk %s failed, output was:"
1096
                                     " %s" % (new_path, run_result.stderr))
1097
        results["disk%s_size" % disk_id] = str(disk_size)
1098
        results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id
1099
      else:
1100
        raise errors.OpPrereqError("Disks created for import must have their"
1101
                                   " size specified")
1102
    results["disk_count"] = str(len(self.options.disks))
1103
    return results
1104

    
1105
  def _GetDiskInfo(self):
1106
    """Gathers information about disks used by instance, perfomes conversion.
1107

1108
    @rtype: dict
1109
    @return: dictionary of disk-related options
1110

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

1113
    """
1114
    results = {}
1115
    disks_list = self.ovf_reader.GetDisksNames()
1116
    for (counter, (disk_name, disk_compression)) in enumerate(disks_list):
1117
      if os.path.dirname(disk_name):
1118
        raise errors.OpPrereqError("Disks are not allowed to have absolute"
1119
                                   " paths or paths outside main OVF directory")
1120
      disk, _ = os.path.splitext(disk_name)
1121
      disk_path = utils.PathJoin(self.input_dir, disk_name)
1122
      if disk_compression:
1123
        _, disk_path = self._CompressDisk(disk_path, disk_compression,
1124
          DECOMPRESS)
1125
        disk, _ = os.path.splitext(disk)
1126
      if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw":
1127
        logging.info("Conversion to raw format is required")
1128
      ext, new_disk_path = self._ConvertDisk("raw", disk_path)
1129

    
1130
      final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext,
1131
        directory=self.output_dir)
1132
      final_name = os.path.basename(final_disk_path)
1133
      disk_size = os.path.getsize(final_disk_path) / (1024 * 1024)
1134
      results["disk%s_dump" % counter] = final_name
1135
      results["disk%s_size" % counter] = str(disk_size)
1136
      results["disk%s_ivname" % counter] = "disk/%s" % str(counter)
1137
    if disks_list:
1138
      results["disk_count"] = str(len(disks_list))
1139
    return results
1140

    
1141
  def Save(self):
1142
    """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
1143

1144
    @raise errors.OpPrereqError: when saving to config file failed
1145

1146
    """
1147
    logging.info("Conversion was succesfull, saving %s in %s directory",
1148
                 constants.EXPORT_CONF_FILE, self.output_dir)
1149
    results = {
1150
      constants.INISECT_INS: {},
1151
      constants.INISECT_BEP: {},
1152
      constants.INISECT_EXP: {},
1153
      constants.INISECT_OSP: {},
1154
      constants.INISECT_HYP: {},
1155
    }
1156

    
1157
    results[constants.INISECT_INS].update(self.results_disk)
1158
    results[constants.INISECT_INS].update(self.results_network)
1159
    results[constants.INISECT_INS]["hypervisor"] = \
1160
      self.results_hypervisor["hypervisor_name"]
1161
    results[constants.INISECT_INS]["name"] = self.results_name
1162
    if self.results_template:
1163
      results[constants.INISECT_INS]["disk_template"] = self.results_template
1164
    if self.results_tags:
1165
      results[constants.INISECT_INS]["tags"] = self.results_tags
1166

    
1167
    results[constants.INISECT_BEP].update(self.results_backend)
1168

    
1169
    results[constants.INISECT_EXP]["os"] = self.results_os["os_name"]
1170
    results[constants.INISECT_EXP]["version"] = self.results_version
1171

    
1172
    del self.results_os["os_name"]
1173
    results[constants.INISECT_OSP].update(self.results_os)
1174

    
1175
    del self.results_hypervisor["hypervisor_name"]
1176
    results[constants.INISECT_HYP].update(self.results_hypervisor)
1177

    
1178
    output_file_name = utils.PathJoin(self.output_dir,
1179
      constants.EXPORT_CONF_FILE)
1180

    
1181
    output = []
1182
    for section, options in results.iteritems():
1183
      output.append("[%s]" % section)
1184
      for name, value in options.iteritems():
1185
        if value is None:
1186
          value = ""
1187
        output.append("%s = %s" % (name, value))
1188
      output.append("")
1189
    output_contents = "\n".join(output)
1190

    
1191
    try:
1192
      utils.WriteFile(output_file_name, data=output_contents)
1193
    except errors.ProgrammerError, err:
1194
      raise errors.OpPrereqError("Saving the config file failed: %s" % err)
1195

    
1196
    self.Cleanup()
1197

    
1198

    
1199
class ConfigParserWithDefaults(ConfigParser.SafeConfigParser):
1200
  """This is just a wrapper on SafeConfigParser, that uses default values
1201

1202
  """
1203
  def get(self, section, options, raw=None, vars=None): # pylint: disable=W0622
1204
    try:
1205
      result = ConfigParser.SafeConfigParser.get(self, section, options, \
1206
        raw=raw, vars=vars)
1207
    except ConfigParser.NoOptionError:
1208
      result = None
1209
    return result
1210

    
1211
  def getint(self, section, options):
1212
    try:
1213
      result = ConfigParser.SafeConfigParser.get(self, section, options)
1214
    except ConfigParser.NoOptionError:
1215
      result = 0
1216
    return int(result)
1217

    
1218

    
1219
class OVFExporter(Converter):
1220
  """Converter from Ganeti config file to OVF
1221

1222
  @type input_dir: string
1223
  @ivar input_dir: directory in which the config.ini file resides
1224
  @type output_dir: string
1225
  @ivar output_dir: directory to which the results of conversion shall be
1226
    written
1227
  @type packed_dir: string
1228
  @ivar packed_dir: if we want OVA package, this points to the real (i.e. not
1229
    temp) output directory
1230
  @type input_path: string
1231
  @ivar input_path: complete path to the config.ini file
1232
  @type output_path: string
1233
  @ivar output_path: complete path to .ovf file
1234
  @type config_parser: L{ConfigParserWithDefaults}
1235
  @ivar config_parser: parser for the config.ini file
1236
  @type results_name: string
1237
  @ivar results_name: name of the instance
1238

1239
  """
1240
  def _ReadInputData(self, input_path):
1241
    """Reads the data on which the conversion will take place.
1242

1243
    @type input_path: string
1244
    @param input_path: absolute path to the config.ini input file
1245

1246
    @raise errors.OpPrereqError: error when reading the config file
1247

1248
    """
1249
    input_dir = os.path.dirname(input_path)
1250
    self.input_path = input_path
1251
    self.input_dir = input_dir
1252
    if self.options.output_dir:
1253
      self.output_dir = os.path.abspath(self.options.output_dir)
1254
    else:
1255
      self.output_dir = input_dir
1256
    self.config_parser = ConfigParserWithDefaults()
1257
    logging.info("Reading configuration from %s file", input_path)
1258
    try:
1259
      self.config_parser.read(input_path)
1260
    except ConfigParser.MissingSectionHeaderError, err:
1261
      raise errors.OpPrereqError("Error when trying to read %s: %s" %
1262
                                 (input_path, err))
1263
    if self.options.ova_package:
1264
      self.temp_dir = tempfile.mkdtemp()
1265
      self.packed_dir = self.output_dir
1266
      self.output_dir = self.temp_dir
1267

    
1268
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1269

    
1270
  def _ParseName(self):
1271
    """Parses name from command line options or config file.
1272

1273
    @rtype: string
1274
    @return: name of Ganeti instance
1275

1276
    @raise errors.OpPrereqError: if name of the instance is not provided
1277

1278
    """
1279
    if self.options.name:
1280
      name = self.options.name
1281
    else:
1282
      name = self.config_parser.get(constants.INISECT_INS, NAME)
1283
    if name is None:
1284
      raise errors.OpPrereqError("No instance name found")
1285
    return name
1286

    
1287
  def Parse(self):
1288
    """Parses the data and creates a structure containing all required info.
1289

1290
    """
1291
    try:
1292
      utils.Makedirs(self.output_dir)
1293
    except OSError, err:
1294
      raise errors.OpPrereqError("Failed to create directory %s: %s" %
1295
                                 (self.output_dir, err))
1296

    
1297
    self.results_name = self._ParseName()
1298

    
1299
  def _PrepareManifest(self, path):
1300
    """Creates manifest for all the files in OVF package.
1301

1302
    @type path: string
1303
    @param path: path to manifesto file
1304

1305
    @raise errors.OpPrereqError: if error occurs when writing file
1306

1307
    """
1308
    logging.info("Preparing manifest for the OVF package")
1309
    lines = []
1310
    files_list = [self.output_path]
1311
    files_list.extend(self.references_files)
1312
    logging.warning("Calculating SHA1 checksums, this may take a while")
1313
    sha1_sums = utils.FingerprintFiles(files_list)
1314
    for file_path, value in sha1_sums.iteritems():
1315
      file_name = os.path.basename(file_path)
1316
      lines.append("SHA1(%s)= %s" % (file_name, value))
1317
    lines.append("")
1318
    data = "\n".join(lines)
1319
    try:
1320
      utils.WriteFile(path, data=data)
1321
    except errors.ProgrammerError, err:
1322
      raise errors.OpPrereqError("Saving the manifest file failed: %s" % err)
1323

    
1324
  @staticmethod
1325
  def _PrepareTarFile(tar_path, files_list):
1326
    """Creates tarfile from the files in OVF package.
1327

1328
    @type tar_path: string
1329
    @param tar_path: path to the resulting file
1330
    @type files_list: list
1331
    @param files_list: list of files in the OVF package
1332

1333
    """
1334
    logging.info("Preparing tarball for the OVF package")
1335
    open(tar_path, mode="w").close()
1336
    ova_package = tarfile.open(name=tar_path, mode="w")
1337
    for file_name in files_list:
1338
      ova_package.add(file_name)
1339
    ova_package.close()
1340

    
1341
  def Save(self):
1342
    """Saves the gathered configuration in an apropriate format.
1343

1344
    @raise errors.OpPrereqError: if unable to create output directory
1345

1346
    """
1347
    output_file = "%s%s" % (self.results_name, OVF_EXT)
1348
    output_path = utils.PathJoin(self.output_dir, output_file)
1349
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1350
    logging.info("Saving read data to %s", output_path)
1351

    
1352
    self.output_path = utils.PathJoin(self.output_dir, output_file)
1353
    files_list = [self.output_path]
1354

    
1355
    data = self.ovf_writer.PrettyXmlDump()
1356
    utils.WriteFile(self.output_path, data=data)
1357

    
1358
    manifest_file = "%s%s" % (self.results_name, MF_EXT)
1359
    manifest_path = utils.PathJoin(self.output_dir, manifest_file)
1360
    self._PrepareManifest(manifest_path)
1361
    files_list.append(manifest_path)
1362

    
1363
    files_list.extend(self.references_files)
1364

    
1365
    if self.options.ova_package:
1366
      ova_file = "%s%s" % (self.results_name, OVA_EXT)
1367
      packed_path = utils.PathJoin(self.packed_dir, ova_file)
1368
      try:
1369
        utils.Makedirs(self.packed_dir)
1370
      except OSError, err:
1371
        raise errors.OpPrereqError("Failed to create directory %s: %s" %
1372
                                   (self.packed_dir, err))
1373
      self._PrepareTarFile(packed_path, files_list)
1374
    logging.info("Creation of the OVF package was successfull")
1375
    self.Cleanup()