Statistics
| Branch: | Tag: | Revision:

root / lib / ovf.py @ 2cfbc784

History | View | Annotate | Download (64.7 kB)

1
#!/usr/bin/python
2
#
3

    
4
# Copyright (C) 2011, 2012 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21

    
22
"""Converter tools between ovf and ganeti config file
23

24
"""
25

    
26
# pylint: disable=F0401, E1101
27

    
28
# F0401 because ElementTree is not default for python 2.4
29
# E1101 makes no sense - pylint assumes that ElementTree object is a tuple
30

    
31

    
32
import ConfigParser
33
import errno
34
import logging
35
import os
36
import os.path
37
import re
38
import shutil
39
import tarfile
40
import tempfile
41
import xml.dom.minidom
42
import xml.parsers.expat
43
try:
44
  import xml.etree.ElementTree as ET
45
except ImportError:
46
  import elementtree.ElementTree as ET
47

    
48
try:
49
  ParseError = ET.ParseError # pylint: disable=E1103
50
except AttributeError:
51
  ParseError = None
52

    
53
from ganeti import constants
54
from ganeti import errors
55
from ganeti import utils
56

    
57

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

    
67
# File extensions in OVF package
68
OVA_EXT = ".ova"
69
OVF_EXT = ".ovf"
70
MF_EXT = ".mf"
71
CERT_EXT = ".cert"
72
COMPRESSION_EXT = ".gz"
73
FILE_EXTENSIONS = [
74
  OVF_EXT,
75
  MF_EXT,
76
  CERT_EXT,
77
]
78

    
79
COMPRESSION_TYPE = "gzip"
80
NO_COMPRESSION = [None, "identity"]
81
COMPRESS = "compression"
82
DECOMPRESS = "decompression"
83
ALLOWED_ACTIONS = [COMPRESS, DECOMPRESS]
84

    
85
VMDK = "vmdk"
86
RAW = "raw"
87
COW = "cow"
88
ALLOWED_FORMATS = [RAW, COW, VMDK]
89

    
90
# ResourceType values
91
RASD_TYPE = {
92
  "vcpus": "3",
93
  "memory": "4",
94
  "scsi-controller": "6",
95
  "ethernet-adapter": "10",
96
  "disk": "17",
97
}
98

    
99
SCSI_SUBTYPE = "lsilogic"
100
VS_TYPE = {
101
  "ganeti": "ganeti-ovf",
102
  "external": "vmx-04",
103
}
104

    
105
# AllocationUnits values and conversion
106
ALLOCATION_UNITS = {
107
  "b": ["bytes", "b"],
108
  "kb": ["kilobytes", "kb", "byte * 2^10", "kibibytes", "kib"],
109
  "mb": ["megabytes", "mb", "byte * 2^20", "mebibytes", "mib"],
110
  "gb": ["gigabytes", "gb", "byte * 2^30", "gibibytes", "gib"],
111
}
112
CONVERT_UNITS_TO_MB = {
113
  "b": lambda x: x / (1024 * 1024),
114
  "kb": lambda x: x / 1024,
115
  "mb": lambda x: x,
116
  "gb": lambda x: x * 1024,
117
}
118

    
119
# Names of the config fields
120
NAME = "name"
121
OS = "os"
122
HYPERV = "hypervisor"
123
VCPUS = "vcpus"
124
MEMORY = "memory"
125
AUTO_BALANCE = "auto_balance"
126
DISK_TEMPLATE = "disk_template"
127
TAGS = "tags"
128
VERSION = "version"
129

    
130
# Instance IDs of System and SCSI controller
131
INSTANCE_ID = {
132
  "system": 0,
133
  "vcpus": 1,
134
  "memory": 2,
135
  "scsi": 3,
136
}
137

    
138
# Disk format descriptions
139
DISK_FORMAT = {
140
  RAW: "http://en.wikipedia.org/wiki/Byte",
141
  VMDK: "http://www.vmware.com/interfaces/specifications/vmdk.html"
142
          "#monolithicSparse",
143
  COW: "http://www.gnome.org/~markmc/qcow-image-format.html",
144
}
145

    
146

    
147
def CheckQemuImg():
148
  """ Make sure that qemu-img is present before performing operations.
149

150
  @raise errors.OpPrereqError: when qemu-img was not found in the system
151

152
  """
153
  if not constants.QEMUIMG_PATH:
154
    raise errors.OpPrereqError("qemu-img not found at build time, unable"
155
                               " to continue", errors.ECODE_STATE)
156

    
157

    
158
def LinkFile(old_path, prefix=None, suffix=None, directory=None):
159
  """Create link with a given prefix and suffix.
160

161
  This is a wrapper over os.link. It tries to create a hard link for given file,
162
  but instead of rising error when file exists, the function changes the name
163
  a little bit.
164

165
  @type old_path:string
166
  @param old_path: path to the file that is to be linked
167
  @type prefix: string
168
  @param prefix: prefix of filename for the link
169
  @type suffix: string
170
  @param suffix: suffix of the filename for the link
171
  @type directory: string
172
  @param directory: directory of the link
173

174
  @raise errors.OpPrereqError: when error on linking is different than
175
    "File exists"
176

177
  """
178
  assert(prefix is not None or suffix is not None)
179
  if directory is None:
180
    directory = os.getcwd()
181
  new_path = utils.PathJoin(directory, "%s%s" % (prefix, suffix))
182
  counter = 1
183
  while True:
184
    try:
185
      os.link(old_path, new_path)
186
      break
187
    except OSError, err:
188
      if err.errno == errno.EEXIST:
189
        new_path = utils.PathJoin(directory,
190
          "%s_%s%s" % (prefix, counter, suffix))
191
        counter += 1
192
      else:
193
        raise errors.OpPrereqError("Error moving the file %s to %s location:"
194
                                   " %s" % (old_path, new_path, err),
195
                                   errors.ECODE_ENVIRON)
196
  return new_path
197

    
198

    
199
class OVFReader(object):
200
  """Reader class for OVF files.
201

202
  @type files_list: list
203
  @ivar files_list: list of files in the OVF package
204
  @type tree: ET.ElementTree
205
  @ivar tree: XML tree of the .ovf file
206
  @type schema_name: string
207
  @ivar schema_name: name of the .ovf file
208
  @type input_dir: string
209
  @ivar input_dir: directory in which the .ovf file resides
210

211
  """
212
  def __init__(self, input_path):
213
    """Initialiaze the reader - load the .ovf file to XML parser.
214

215
    It is assumed that names of manifesto (.mf), certificate (.cert) and ovf
216
    files are the same. In order to account any other files as part of the ovf
217
    package, they have to be explicitly mentioned in the Resources section
218
    of the .ovf file.
219

220
    @type input_path: string
221
    @param input_path: absolute path to the .ovf file
222

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

226
    """
227
    self.tree = ET.ElementTree()
228
    try:
229
      self.tree.parse(input_path)
230
    except (ParseError, xml.parsers.expat.ExpatError), err:
231
      raise errors.OpPrereqError("Error while reading %s file: %s" %
232
                                 (OVF_EXT, err), errors.ECODE_ENVIRON)
233

    
234
    # Create a list of all files in the OVF package
235
    (input_dir, input_file) = os.path.split(input_path)
236
    (input_name, _) = os.path.splitext(input_file)
237
    files_directory = utils.ListVisibleFiles(input_dir)
238
    files_list = []
239
    for file_name in files_directory:
240
      (name, extension) = os.path.splitext(file_name)
241
      if extension in FILE_EXTENSIONS and name == input_name:
242
        files_list.append(file_name)
243
    files_list += self._GetAttributes("{%s}References/{%s}File" %
244
                                      (OVF_SCHEMA, OVF_SCHEMA),
245
                                      "{%s}href" % OVF_SCHEMA)
246
    for file_name in files_list:
247
      file_path = utils.PathJoin(input_dir, file_name)
248
      if not os.path.exists(file_path):
249
        raise errors.OpPrereqError("File does not exist: %s" % file_path,
250
                                   errors.ECODE_ENVIRON)
251
    logging.info("Files in the OVF package: %s", " ".join(files_list))
252
    self.files_list = files_list
253
    self.input_dir = input_dir
254
    self.schema_name = input_name
255

    
256
  def _GetAttributes(self, path, attribute):
257
    """Get specified attribute from all nodes accessible using given path.
258

259
    Function follows the path from root node to the desired tags using path,
260
    then reads the apropriate attribute values.
261

262
    @type path: string
263
    @param path: path of nodes to visit
264
    @type attribute: string
265
    @param attribute: attribute for which we gather the information
266
    @rtype: list
267
    @return: for each accessible tag with the attribute value set, value of the
268
      attribute
269

270
    """
271
    current_list = self.tree.findall(path)
272
    results = [x.get(attribute) for x in current_list]
273
    return filter(None, results)
274

    
275
  def _GetElementMatchingAttr(self, path, match_attr):
276
    """Searches for element on a path that matches certain attribute value.
277

278
    Function follows the path from root node to the desired tags using path,
279
    then searches for the first one matching the attribute value.
280

281
    @type path: string
282
    @param path: path of nodes to visit
283
    @type match_attr: tuple
284
    @param match_attr: pair (attribute, value) for which we search
285
    @rtype: ET.ElementTree or None
286
    @return: first element matching match_attr or None if nothing matches
287

288
    """
289
    potential_elements = self.tree.findall(path)
290
    (attr, val) = match_attr
291
    for elem in potential_elements:
292
      if elem.get(attr) == val:
293
        return elem
294
    return None
295

    
296
  def _GetElementMatchingText(self, path, match_text):
297
    """Searches for element on a path that matches certain text value.
298

299
    Function follows the path from root node to the desired tags using path,
300
    then searches for the first one matching the text value.
301

302
    @type path: string
303
    @param path: path of nodes to visit
304
    @type match_text: tuple
305
    @param match_text: pair (node, text) for which we search
306
    @rtype: ET.ElementTree or None
307
    @return: first element matching match_text or None if nothing matches
308

309
    """
310
    potential_elements = self.tree.findall(path)
311
    (node, text) = match_text
312
    for elem in potential_elements:
313
      if elem.findtext(node) == text:
314
        return elem
315
    return None
316

    
317
  @staticmethod
318
  def _GetDictParameters(root, schema):
319
    """Reads text in all children and creates the dictionary from the contents.
320

321
    @type root: ET.ElementTree or None
322
    @param root: father of the nodes we want to collect data about
323
    @type schema: string
324
    @param schema: schema name to be removed from the tag
325
    @rtype: dict
326
    @return: dictionary containing tags and their text contents, tags have their
327
      schema fragment removed or empty dictionary, when root is None
328

329
    """
330
    if not root:
331
      return {}
332
    results = {}
333
    for element in list(root):
334
      pref_len = len("{%s}" % schema)
335
      assert(schema in element.tag)
336
      tag = element.tag[pref_len:]
337
      results[tag] = element.text
338
    return results
339

    
340
  def VerifyManifest(self):
341
    """Verifies manifest for the OVF package, if one is given.
342

343
    @raise errors.OpPrereqError: if SHA1 checksums do not match
344

345
    """
346
    if "%s%s" % (self.schema_name, MF_EXT) in self.files_list:
347
      logging.warning("Verifying SHA1 checksums, this may take a while")
348
      manifest_filename = "%s%s" % (self.schema_name, MF_EXT)
349
      manifest_path = utils.PathJoin(self.input_dir, manifest_filename)
350
      manifest_content = utils.ReadFile(manifest_path).splitlines()
351
      manifest_files = {}
352
      regexp = r"SHA1\((\S+)\)= (\S+)"
353
      for line in manifest_content:
354
        match = re.match(regexp, line)
355
        if match:
356
          file_name = match.group(1)
357
          sha1_sum = match.group(2)
358
          manifest_files[file_name] = sha1_sum
359
      files_with_paths = [utils.PathJoin(self.input_dir, file_name)
360
        for file_name in self.files_list]
361
      sha1_sums = utils.FingerprintFiles(files_with_paths)
362
      for file_name, value in manifest_files.iteritems():
363
        if sha1_sums.get(utils.PathJoin(self.input_dir, file_name)) != value:
364
          raise errors.OpPrereqError("SHA1 checksum of %s does not match the"
365
                                     " value in manifest file" % file_name,
366
                                     errors.ECODE_ENVIRON)
367
      logging.info("SHA1 checksums verified")
368

    
369
  def GetInstanceName(self):
370
    """Provides information about instance name.
371

372
    @rtype: string
373
    @return: instance name string
374

375
    """
376
    find_name = "{%s}VirtualSystem/{%s}Name" % (OVF_SCHEMA, OVF_SCHEMA)
377
    return self.tree.findtext(find_name)
378

    
379
  def GetDiskTemplate(self):
380
    """Returns disk template from .ovf file
381

382
    @rtype: string or None
383
    @return: name of the template
384
    """
385
    find_template = ("{%s}GanetiSection/{%s}DiskTemplate" %
386
                     (GANETI_SCHEMA, GANETI_SCHEMA))
387
    return self.tree.findtext(find_template)
388

    
389
  def GetHypervisorData(self):
390
    """Provides hypervisor information - hypervisor name and options.
391

392
    @rtype: dict
393
    @return: dictionary containing name of the used hypervisor and all the
394
      specified options
395

396
    """
397
    hypervisor_search = ("{%s}GanetiSection/{%s}Hypervisor" %
398
                         (GANETI_SCHEMA, GANETI_SCHEMA))
399
    hypervisor_data = self.tree.find(hypervisor_search)
400
    if not hypervisor_data:
401
      return {"hypervisor_name": constants.VALUE_AUTO}
402
    results = {
403
      "hypervisor_name": hypervisor_data.findtext("{%s}Name" % GANETI_SCHEMA,
404
                           default=constants.VALUE_AUTO),
405
    }
406
    parameters = hypervisor_data.find("{%s}Parameters" % GANETI_SCHEMA)
407
    results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
408
    return results
409

    
410
  def GetOSData(self):
411
    """ Provides operating system information - os name and options.
412

413
    @rtype: dict
414
    @return: dictionary containing name and options for the chosen OS
415

416
    """
417
    results = {}
418
    os_search = ("{%s}GanetiSection/{%s}OperatingSystem" %
419
                 (GANETI_SCHEMA, GANETI_SCHEMA))
420
    os_data = self.tree.find(os_search)
421
    if os_data:
422
      results["os_name"] = os_data.findtext("{%s}Name" % GANETI_SCHEMA)
423
      parameters = os_data.find("{%s}Parameters" % GANETI_SCHEMA)
424
      results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
425
    return results
426

    
427
  def GetBackendData(self):
428
    """ Provides backend information - vcpus, memory, auto balancing options.
429

430
    @rtype: dict
431
    @return: dictionary containing options for vcpus, memory and auto balance
432
      settings
433

434
    """
435
    results = {}
436

    
437
    find_vcpus = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item" %
438
                   (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
439
    match_vcpus = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["vcpus"])
440
    vcpus = self._GetElementMatchingText(find_vcpus, match_vcpus)
441
    if vcpus:
442
      vcpus_count = vcpus.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
443
        default=constants.VALUE_AUTO)
444
    else:
445
      vcpus_count = constants.VALUE_AUTO
446
    results["vcpus"] = str(vcpus_count)
447

    
448
    find_memory = find_vcpus
449
    match_memory = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["memory"])
450
    memory = self._GetElementMatchingText(find_memory, match_memory)
451
    memory_raw = None
452
    if memory:
453
      alloc_units = memory.findtext("{%s}AllocationUnits" % RASD_SCHEMA)
454
      matching_units = [units for units, variants in
455
        ALLOCATION_UNITS.iteritems() if alloc_units.lower() in variants]
456
      if matching_units == []:
457
        raise errors.OpPrereqError("Unit %s for RAM memory unknown" %
458
                                   alloc_units, errors.ECODE_INVAL)
459
      units = matching_units[0]
460
      memory_raw = int(memory.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
461
            default=constants.VALUE_AUTO))
462
      memory_count = CONVERT_UNITS_TO_MB[units](memory_raw)
463
    else:
464
      memory_count = constants.VALUE_AUTO
465
    results["memory"] = str(memory_count)
466

    
467
    find_balance = ("{%s}GanetiSection/{%s}AutoBalance" %
468
                   (GANETI_SCHEMA, GANETI_SCHEMA))
469
    balance = self.tree.findtext(find_balance, default=constants.VALUE_AUTO)
470
    results["auto_balance"] = balance
471

    
472
    return results
473

    
474
  def GetTagsData(self):
475
    """Provides tags information for instance.
476

477
    @rtype: string or None
478
    @return: string of comma-separated tags for the instance
479

480
    """
481
    find_tags = "{%s}GanetiSection/{%s}Tags" % (GANETI_SCHEMA, GANETI_SCHEMA)
482
    results = self.tree.findtext(find_tags)
483
    if results:
484
      return results
485
    else:
486
      return None
487

    
488
  def GetVersionData(self):
489
    """Provides version number read from .ovf file
490

491
    @rtype: string
492
    @return: string containing the version number
493

494
    """
495
    find_version = ("{%s}GanetiSection/{%s}Version" %
496
                    (GANETI_SCHEMA, GANETI_SCHEMA))
497
    return self.tree.findtext(find_version)
498

    
499
  def GetNetworkData(self):
500
    """Provides data about the network in the OVF instance.
501

502
    The method gathers the data about networks used by OVF instance. It assumes
503
    that 'name' tag means something - in essence, if it contains one of the
504
    words 'bridged' or 'routed' then that will be the mode of this network in
505
    Ganeti. The information about the network can be either in GanetiSection or
506
    VirtualHardwareSection.
507

508
    @rtype: dict
509
    @return: dictionary containing all the network information
510

511
    """
512
    results = {}
513
    networks_search = ("{%s}NetworkSection/{%s}Network" %
514
                       (OVF_SCHEMA, OVF_SCHEMA))
515
    network_names = self._GetAttributes(networks_search,
516
      "{%s}name" % OVF_SCHEMA)
517
    required = ["ip", "mac", "link", "mode"]
518
    for (counter, network_name) in enumerate(network_names):
519
      network_search = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item"
520
                        % (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
521
      ganeti_search = ("{%s}GanetiSection/{%s}Network/{%s}Nic" %
522
                       (GANETI_SCHEMA, GANETI_SCHEMA, GANETI_SCHEMA))
523
      network_match = ("{%s}Connection" % RASD_SCHEMA, network_name)
524
      ganeti_match = ("{%s}name" % OVF_SCHEMA, network_name)
525
      network_data = self._GetElementMatchingText(network_search, network_match)
526
      network_ganeti_data = self._GetElementMatchingAttr(ganeti_search,
527
        ganeti_match)
528

    
529
      ganeti_data = {}
530
      if network_ganeti_data:
531
        ganeti_data["mode"] = network_ganeti_data.findtext("{%s}Mode" %
532
                                                           GANETI_SCHEMA)
533
        ganeti_data["mac"] = network_ganeti_data.findtext("{%s}MACAddress" %
534
                                                          GANETI_SCHEMA)
535
        ganeti_data["ip"] = network_ganeti_data.findtext("{%s}IPAddress" %
536
                                                         GANETI_SCHEMA)
537
        ganeti_data["link"] = network_ganeti_data.findtext("{%s}Link" %
538
                                                           GANETI_SCHEMA)
539
      mac_data = None
540
      if network_data:
541
        mac_data = network_data.findtext("{%s}Address" % RASD_SCHEMA)
542

    
543
      network_name = network_name.lower()
544

    
545
      # First, some not Ganeti-specific information is collected
546
      if constants.NIC_MODE_BRIDGED in network_name:
547
        results["nic%s_mode" % counter] = "bridged"
548
      elif constants.NIC_MODE_ROUTED in network_name:
549
        results["nic%s_mode" % counter] = "routed"
550
      results["nic%s_mac" % counter] = mac_data
551

    
552
      # GanetiSection data overrides 'manually' collected data
553
      for name, value in ganeti_data.iteritems():
554
        results["nic%s_%s" % (counter, name)] = value
555

    
556
      # Bridged network has no IP - unless specifically stated otherwise
557
      if (results.get("nic%s_mode" % counter) == "bridged" and
558
          not results.get("nic%s_ip" % counter)):
559
        results["nic%s_ip" % counter] = constants.VALUE_NONE
560

    
561
      for option in required:
562
        if not results.get("nic%s_%s" % (counter, option)):
563
          results["nic%s_%s" % (counter, option)] = constants.VALUE_AUTO
564

    
565
    if network_names:
566
      results["nic_count"] = str(len(network_names))
567
    return results
568

    
569
  def GetDisksNames(self):
570
    """Provides list of file names for the disks used by the instance.
571

572
    @rtype: list
573
    @return: list of file names, as referenced in .ovf file
574

575
    """
576
    results = []
577
    disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA)
578
    disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA)
579
    for disk in disk_ids:
580
      disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA)
581
      disk_match = ("{%s}id" % OVF_SCHEMA, disk)
582
      disk_elem = self._GetElementMatchingAttr(disk_search, disk_match)
583
      if disk_elem is None:
584
        raise errors.OpPrereqError("%s file corrupted - disk %s not found in"
585
                                   " references" % (OVF_EXT, disk),
586
                                   errors.ECODE_ENVIRON)
587
      disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA)
588
      disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA)
589
      results.append((disk_name, disk_compression))
590
    return results
591

    
592

    
593
def SubElementText(parent, tag, text, attrib={}, **extra):
594
# pylint: disable=W0102
595
  """This is just a wrapper on ET.SubElement that always has text content.
596

597
  """
598
  if text is None:
599
    return None
600
  elem = ET.SubElement(parent, tag, attrib=attrib, **extra)
601
  elem.text = str(text)
602
  return elem
603

    
604

    
605
class OVFWriter(object):
606
  """Writer class for OVF files.
607

608
  @type tree: ET.ElementTree
609
  @ivar tree: XML tree that we are constructing
610
  @type virtual_system_type: string
611
  @ivar virtual_system_type: value of vssd:VirtualSystemType, for external usage
612
    in VMWare this requires to be vmx
613
  @type hardware_list: list
614
  @ivar hardware_list: list of items prepared for VirtualHardwareSection
615
  @type next_instance_id: int
616
  @ivar next_instance_id: next instance id to be used when creating elements on
617
    hardware_list
618

619
  """
620
  def __init__(self, has_gnt_section):
621
    """Initialize the writer - set the top element.
622

623
    @type has_gnt_section: bool
624
    @param has_gnt_section: if the Ganeti schema should be added - i.e. this
625
      means that Ganeti section will be present
626

627
    """
628
    env_attribs = {
629
      "xmlns:xsi": XML_SCHEMA,
630
      "xmlns:vssd": VSSD_SCHEMA,
631
      "xmlns:rasd": RASD_SCHEMA,
632
      "xmlns:ovf": OVF_SCHEMA,
633
      "xmlns": OVF_SCHEMA,
634
      "xml:lang": "en-US",
635
    }
636
    if has_gnt_section:
637
      env_attribs["xmlns:gnt"] = GANETI_SCHEMA
638
      self.virtual_system_type = VS_TYPE["ganeti"]
639
    else:
640
      self.virtual_system_type = VS_TYPE["external"]
641
    self.tree = ET.Element("Envelope", attrib=env_attribs)
642
    self.hardware_list = []
643
    # INSTANCE_ID contains statically assigned IDs, starting from 0
644
    self.next_instance_id = len(INSTANCE_ID) # FIXME: hackish
645

    
646
  def SaveDisksData(self, disks):
647
    """Convert disk information to certain OVF sections.
648

649
    @type disks: list
650
    @param disks: list of dictionaries of disk options from config.ini
651

652
    """
653
    references = ET.SubElement(self.tree, "References")
654
    disk_section = ET.SubElement(self.tree, "DiskSection")
655
    SubElementText(disk_section, "Info", "Virtual disk information")
656
    for counter, disk in enumerate(disks):
657
      file_id = "file%s" % counter
658
      disk_id = "disk%s" % counter
659
      file_attribs = {
660
        "ovf:href": disk["path"],
661
        "ovf:size": str(disk["real-size"]),
662
        "ovf:id": file_id,
663
      }
664
      disk_attribs = {
665
        "ovf:capacity": str(disk["virt-size"]),
666
        "ovf:diskId": disk_id,
667
        "ovf:fileRef": file_id,
668
        "ovf:format": DISK_FORMAT.get(disk["format"], disk["format"]),
669
      }
670
      if "compression" in disk:
671
        file_attribs["ovf:compression"] = disk["compression"]
672
      ET.SubElement(references, "File", attrib=file_attribs)
673
      ET.SubElement(disk_section, "Disk", attrib=disk_attribs)
674

    
675
      # Item in VirtualHardwareSection creation
676
      disk_item = ET.Element("Item")
677
      SubElementText(disk_item, "rasd:ElementName", disk_id)
678
      SubElementText(disk_item, "rasd:HostResource", "ovf:/disk/%s" % disk_id)
679
      SubElementText(disk_item, "rasd:InstanceID", self.next_instance_id)
680
      SubElementText(disk_item, "rasd:Parent", INSTANCE_ID["scsi"])
681
      SubElementText(disk_item, "rasd:ResourceType", RASD_TYPE["disk"])
682
      self.hardware_list.append(disk_item)
683
      self.next_instance_id += 1
684

    
685
  def SaveNetworksData(self, networks):
686
    """Convert network information to NetworkSection.
687

688
    @type networks: list
689
    @param networks: list of dictionaries of network options form config.ini
690

691
    """
692
    network_section = ET.SubElement(self.tree, "NetworkSection")
693
    SubElementText(network_section, "Info", "List of logical networks")
694
    for counter, network in enumerate(networks):
695
      network_name = "%s%s" % (network["mode"], counter)
696
      network_attrib = {"ovf:name": network_name}
697
      ET.SubElement(network_section, "Network", attrib=network_attrib)
698

    
699
      # Item in VirtualHardwareSection creation
700
      network_item = ET.Element("Item")
701
      SubElementText(network_item, "rasd:Address", network["mac"])
702
      SubElementText(network_item, "rasd:Connection", network_name)
703
      SubElementText(network_item, "rasd:ElementName", network_name)
704
      SubElementText(network_item, "rasd:InstanceID", self.next_instance_id)
705
      SubElementText(network_item, "rasd:ResourceType",
706
        RASD_TYPE["ethernet-adapter"])
707
      self.hardware_list.append(network_item)
708
      self.next_instance_id += 1
709

    
710
  @staticmethod
711
  def _SaveNameAndParams(root, data):
712
    """Save name and parameters information under root using data.
713

714
    @type root: ET.Element
715
    @param root: root element for the Name and Parameters
716
    @type data: dict
717
    @param data: data from which we gather the values
718

719
    """
720
    assert(data.get("name"))
721
    name = SubElementText(root, "gnt:Name", data["name"])
722
    params = ET.SubElement(root, "gnt:Parameters")
723
    for name, value in data.iteritems():
724
      if name != "name":
725
        SubElementText(params, "gnt:%s" % name, value)
726

    
727
  def SaveGanetiData(self, ganeti, networks):
728
    """Convert Ganeti-specific information to GanetiSection.
729

730
    @type ganeti: dict
731
    @param ganeti: dictionary of Ganeti-specific options from config.ini
732
    @type networks: list
733
    @param networks: list of dictionaries of network options form config.ini
734

735
    """
736
    ganeti_section = ET.SubElement(self.tree, "gnt:GanetiSection")
737

    
738
    SubElementText(ganeti_section, "gnt:Version", ganeti.get("version"))
739
    SubElementText(ganeti_section, "gnt:DiskTemplate",
740
      ganeti.get("disk_template"))
741
    SubElementText(ganeti_section, "gnt:AutoBalance",
742
      ganeti.get("auto_balance"))
743
    SubElementText(ganeti_section, "gnt:Tags", ganeti.get("tags"))
744

    
745
    osys = ET.SubElement(ganeti_section, "gnt:OperatingSystem")
746
    self._SaveNameAndParams(osys, ganeti["os"])
747

    
748
    hypervisor = ET.SubElement(ganeti_section, "gnt:Hypervisor")
749
    self._SaveNameAndParams(hypervisor, ganeti["hypervisor"])
750

    
751
    network_section = ET.SubElement(ganeti_section, "gnt:Network")
752
    for counter, network in enumerate(networks):
753
      network_name = "%s%s" % (network["mode"], counter)
754
      nic_attrib = {"ovf:name": network_name}
755
      nic = ET.SubElement(network_section, "gnt:Nic", attrib=nic_attrib)
756
      SubElementText(nic, "gnt:Mode", network["mode"])
757
      SubElementText(nic, "gnt:MACAddress", network["mac"])
758
      SubElementText(nic, "gnt:IPAddress", network["ip"])
759
      SubElementText(nic, "gnt:Link", network["link"])
760

    
761
  def SaveVirtualSystemData(self, name, vcpus, memory):
762
    """Convert virtual system information to OVF sections.
763

764
    @type name: string
765
    @param name: name of the instance
766
    @type vcpus: int
767
    @param vcpus: number of VCPUs
768
    @type memory: int
769
    @param memory: RAM memory in MB
770

771
    """
772
    assert(vcpus > 0)
773
    assert(memory > 0)
774
    vs_attrib = {"ovf:id": name}
775
    virtual_system = ET.SubElement(self.tree, "VirtualSystem", attrib=vs_attrib)
776
    SubElementText(virtual_system, "Info", "A virtual machine")
777

    
778
    name_section = ET.SubElement(virtual_system, "Name")
779
    name_section.text = name
780
    os_attrib = {"ovf:id": "0"}
781
    os_section = ET.SubElement(virtual_system, "OperatingSystemSection",
782
      attrib=os_attrib)
783
    SubElementText(os_section, "Info", "Installed guest operating system")
784
    hardware_section = ET.SubElement(virtual_system, "VirtualHardwareSection")
785
    SubElementText(hardware_section, "Info", "Virtual hardware requirements")
786

    
787
    # System description
788
    system = ET.SubElement(hardware_section, "System")
789
    SubElementText(system, "vssd:ElementName", "Virtual Hardware Family")
790
    SubElementText(system, "vssd:InstanceID", INSTANCE_ID["system"])
791
    SubElementText(system, "vssd:VirtualSystemIdentifier", name)
792
    SubElementText(system, "vssd:VirtualSystemType", self.virtual_system_type)
793

    
794
    # Item for vcpus
795
    vcpus_item = ET.SubElement(hardware_section, "Item")
796
    SubElementText(vcpus_item, "rasd:ElementName",
797
      "%s virtual CPU(s)" % vcpus)
798
    SubElementText(vcpus_item, "rasd:InstanceID", INSTANCE_ID["vcpus"])
799
    SubElementText(vcpus_item, "rasd:ResourceType", RASD_TYPE["vcpus"])
800
    SubElementText(vcpus_item, "rasd:VirtualQuantity", vcpus)
801

    
802
    # Item for memory
803
    memory_item = ET.SubElement(hardware_section, "Item")
804
    SubElementText(memory_item, "rasd:AllocationUnits", "byte * 2^20")
805
    SubElementText(memory_item, "rasd:ElementName", "%sMB of memory" % memory)
806
    SubElementText(memory_item, "rasd:InstanceID", INSTANCE_ID["memory"])
807
    SubElementText(memory_item, "rasd:ResourceType", RASD_TYPE["memory"])
808
    SubElementText(memory_item, "rasd:VirtualQuantity", memory)
809

    
810
    # Item for scsi controller
811
    scsi_item = ET.SubElement(hardware_section, "Item")
812
    SubElementText(scsi_item, "rasd:Address", INSTANCE_ID["system"])
813
    SubElementText(scsi_item, "rasd:ElementName", "scsi_controller0")
814
    SubElementText(scsi_item, "rasd:InstanceID", INSTANCE_ID["scsi"])
815
    SubElementText(scsi_item, "rasd:ResourceSubType", SCSI_SUBTYPE)
816
    SubElementText(scsi_item, "rasd:ResourceType", RASD_TYPE["scsi-controller"])
817

    
818
    # Other items - from self.hardware_list
819
    for item in self.hardware_list:
820
      hardware_section.append(item)
821

    
822
  def PrettyXmlDump(self):
823
    """Formatter of the XML file.
824

825
    @rtype: string
826
    @return: XML tree in the form of nicely-formatted string
827

828
    """
829
    raw_string = ET.tostring(self.tree)
830
    parsed_xml = xml.dom.minidom.parseString(raw_string)
831
    xml_string = parsed_xml.toprettyxml(indent="  ")
832
    text_re = re.compile(">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL)
833
    return text_re.sub(">\g<1></", xml_string)
834

    
835

    
836
class Converter(object):
837
  """Converter class for OVF packages.
838

839
  Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is
840
  to provide a common interface for the two.
841

842
  @type options: optparse.Values
843
  @ivar options: options parsed from the command line
844
  @type output_dir: string
845
  @ivar output_dir: directory to which the results of conversion shall be
846
    written
847
  @type temp_file_manager: L{utils.TemporaryFileManager}
848
  @ivar temp_file_manager: container for temporary files created during
849
    conversion
850
  @type temp_dir: string
851
  @ivar temp_dir: temporary directory created then we deal with OVA
852

853
  """
854
  def __init__(self, input_path, options):
855
    """Initialize the converter.
856

857
    @type input_path: string
858
    @param input_path: path to the Converter input file
859
    @type options: optparse.Values
860
    @param options: command line options
861

862
    @raise errors.OpPrereqError: if file does not exist
863

864
    """
865
    input_path = os.path.abspath(input_path)
866
    if not os.path.isfile(input_path):
867
      raise errors.OpPrereqError("File does not exist: %s" % input_path,
868
                                 errors.ECODE_ENVIRON)
869
    self.options = options
870
    self.temp_file_manager = utils.TemporaryFileManager()
871
    self.temp_dir = None
872
    self.output_dir = None
873
    self._ReadInputData(input_path)
874

    
875
  def _ReadInputData(self, input_path):
876
    """Reads the data on which the conversion will take place.
877

878
    @type input_path: string
879
    @param input_path: absolute path to the Converter input file
880

881
    """
882
    raise NotImplementedError()
883

    
884
  def _CompressDisk(self, disk_path, compression, action):
885
    """Performs (de)compression on the disk and returns the new path
886

887
    @type disk_path: string
888
    @param disk_path: path to the disk
889
    @type compression: string
890
    @param compression: compression type
891
    @type action: string
892
    @param action: whether the action is compression or decompression
893
    @rtype: string
894
    @return: new disk path after (de)compression
895

896
    @raise errors.OpPrereqError: disk (de)compression failed or "compression"
897
      is not supported
898

899
    """
900
    assert(action in ALLOWED_ACTIONS)
901
    # For now we only support gzip, as it is used in ovftool
902
    if compression != COMPRESSION_TYPE:
903
      raise errors.OpPrereqError("Unsupported compression type: %s"
904
                                 % compression, errors.ECODE_INVAL)
905
    disk_file = os.path.basename(disk_path)
906
    if action == DECOMPRESS:
907
      (disk_name, _) = os.path.splitext(disk_file)
908
      prefix = disk_name
909
    elif action == COMPRESS:
910
      prefix = disk_file
911
    new_path = utils.GetClosedTempfile(suffix=COMPRESSION_EXT, prefix=prefix,
912
      dir=self.output_dir)
913
    self.temp_file_manager.Add(new_path)
914
    args = ["gzip", "-c", disk_path]
915
    run_result = utils.RunCmd(args, output=new_path)
916
    if run_result.failed:
917
      raise errors.OpPrereqError("Disk %s failed with output: %s"
918
                                 % (action, run_result.stderr),
919
                                 errors.ECODE_ENVIRON)
920
    logging.info("The %s of the disk is completed", action)
921
    return (COMPRESSION_EXT, new_path)
922

    
923
  def _ConvertDisk(self, disk_format, disk_path):
924
    """Performes conversion to specified format.
925

926
    @type disk_format: string
927
    @param disk_format: format to which the disk should be converted
928
    @type disk_path: string
929
    @param disk_path: path to the disk that should be converted
930
    @rtype: string
931
    @return path to the output disk
932

933
    @raise errors.OpPrereqError: convertion of the disk failed
934

935
    """
936
    CheckQemuImg()
937
    disk_file = os.path.basename(disk_path)
938
    (disk_name, disk_extension) = os.path.splitext(disk_file)
939
    if disk_extension != disk_format:
940
      logging.warning("Conversion of disk image to %s format, this may take"
941
                      " a while", disk_format)
942

    
943
    new_disk_path = utils.GetClosedTempfile(suffix=".%s" % disk_format,
944
      prefix=disk_name, dir=self.output_dir)
945
    self.temp_file_manager.Add(new_disk_path)
946
    args = [
947
      constants.QEMUIMG_PATH,
948
      "convert",
949
      "-O",
950
      disk_format,
951
      disk_path,
952
      new_disk_path,
953
    ]
954
    run_result = utils.RunCmd(args, cwd=os.getcwd())
955
    if run_result.failed:
956
      raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was"
957
                                 ": %s" % (disk_format, run_result.stderr),
958
                                 errors.ECODE_ENVIRON)
959
    return (".%s" % disk_format, new_disk_path)
960

    
961
  @staticmethod
962
  def _GetDiskQemuInfo(disk_path, regexp):
963
    """Figures out some information of the disk using qemu-img.
964

965
    @type disk_path: string
966
    @param disk_path: path to the disk we want to know the format of
967
    @type regexp: string
968
    @param regexp: string that has to be matched, it has to contain one group
969
    @rtype: string
970
    @return: disk format
971

972
    @raise errors.OpPrereqError: format information cannot be retrieved
973

974
    """
975
    CheckQemuImg()
976
    args = [constants.QEMUIMG_PATH, "info", disk_path]
977
    run_result = utils.RunCmd(args, cwd=os.getcwd())
978
    if run_result.failed:
979
      raise errors.OpPrereqError("Gathering info about the disk using qemu-img"
980
                                 " failed, output was: %s" % run_result.stderr,
981
                                 errors.ECODE_ENVIRON)
982
    result = run_result.output
983
    regexp = r"%s" % regexp
984
    match = re.search(regexp, result)
985
    if match:
986
      disk_format = match.group(1)
987
    else:
988
      raise errors.OpPrereqError("No file information matching %s found in:"
989
                                 " %s" % (regexp, result),
990
                                 errors.ECODE_ENVIRON)
991
    return disk_format
992

    
993
  def Parse(self):
994
    """Parses the data and creates a structure containing all required info.
995

996
    """
997
    raise NotImplementedError()
998

    
999
  def Save(self):
1000
    """Saves the gathered configuration in an apropriate format.
1001

1002
    """
1003
    raise NotImplementedError()
1004

    
1005
  def Cleanup(self):
1006
    """Cleans the temporary directory, if one was created.
1007

1008
    """
1009
    self.temp_file_manager.Cleanup()
1010
    if self.temp_dir:
1011
      shutil.rmtree(self.temp_dir)
1012
      self.temp_dir = None
1013

    
1014

    
1015
class OVFImporter(Converter):
1016
  """Converter from OVF to Ganeti config file.
1017

1018
  @type input_dir: string
1019
  @ivar input_dir: directory in which the .ovf file resides
1020
  @type output_dir: string
1021
  @ivar output_dir: directory to which the results of conversion shall be
1022
    written
1023
  @type input_path: string
1024
  @ivar input_path: complete path to the .ovf file
1025
  @type ovf_reader: L{OVFReader}
1026
  @ivar ovf_reader: OVF reader instance collects data from .ovf file
1027
  @type results_name: string
1028
  @ivar results_name: name of imported instance
1029
  @type results_template: string
1030
  @ivar results_template: disk template read from .ovf file or command line
1031
    arguments
1032
  @type results_hypervisor: dict
1033
  @ivar results_hypervisor: hypervisor information gathered from .ovf file or
1034
    command line arguments
1035
  @type results_os: dict
1036
  @ivar results_os: operating system information gathered from .ovf file or
1037
    command line arguments
1038
  @type results_backend: dict
1039
  @ivar results_backend: backend information gathered from .ovf file or
1040
    command line arguments
1041
  @type results_tags: string
1042
  @ivar results_tags: string containing instance-specific tags
1043
  @type results_version: string
1044
  @ivar results_version: version as required by Ganeti import
1045
  @type results_network: dict
1046
  @ivar results_network: network information gathered from .ovf file or command
1047
    line arguments
1048
  @type results_disk: dict
1049
  @ivar results_disk: disk information gathered from .ovf file or command line
1050
    arguments
1051

1052
  """
1053
  def _ReadInputData(self, input_path):
1054
    """Reads the data on which the conversion will take place.
1055

1056
    @type input_path: string
1057
    @param input_path: absolute path to the .ovf or .ova input file
1058

1059
    @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
1060

1061
    """
1062
    (input_dir, input_file) = os.path.split(input_path)
1063
    (_, input_extension) = os.path.splitext(input_file)
1064

    
1065
    if input_extension == OVF_EXT:
1066
      logging.info("%s file extension found, no unpacking necessary", OVF_EXT)
1067
      self.input_dir = input_dir
1068
      self.input_path = input_path
1069
      self.temp_dir = None
1070
    elif input_extension == OVA_EXT:
1071
      logging.info("%s file extension found, proceeding to unpacking", OVA_EXT)
1072
      self._UnpackOVA(input_path)
1073
    else:
1074
      raise errors.OpPrereqError("Unknown file extension; expected %s or %s"
1075
                                 " file" % (OVA_EXT, OVF_EXT),
1076
                                 errors.ECODE_INVAL)
1077
    assert ((input_extension == OVA_EXT and self.temp_dir) or
1078
            (input_extension == OVF_EXT and not self.temp_dir))
1079
    assert self.input_dir in self.input_path
1080

    
1081
    if self.options.output_dir:
1082
      self.output_dir = os.path.abspath(self.options.output_dir)
1083
      if (os.path.commonprefix([constants.EXPORT_DIR, self.output_dir]) !=
1084
          constants.EXPORT_DIR):
1085
        logging.warning("Export path is not under %s directory, import to"
1086
                        " Ganeti using gnt-backup may fail",
1087
                        constants.EXPORT_DIR)
1088
    else:
1089
      self.output_dir = constants.EXPORT_DIR
1090

    
1091
    self.ovf_reader = OVFReader(self.input_path)
1092
    self.ovf_reader.VerifyManifest()
1093

    
1094
  def _UnpackOVA(self, input_path):
1095
    """Unpacks the .ova package into temporary directory.
1096

1097
    @type input_path: string
1098
    @param input_path: path to the .ova package file
1099

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

1104
    """
1105
    input_name = None
1106
    if not tarfile.is_tarfile(input_path):
1107
      raise errors.OpPrereqError("The provided %s file is not a proper tar"
1108
                                 " archive" % OVA_EXT, errors.ECODE_ENVIRON)
1109
    ova_content = tarfile.open(input_path)
1110
    temp_dir = tempfile.mkdtemp()
1111
    self.temp_dir = temp_dir
1112
    for file_name in ova_content.getnames():
1113
      file_normname = os.path.normpath(file_name)
1114
      try:
1115
        utils.PathJoin(temp_dir, file_normname)
1116
      except ValueError, err:
1117
        raise errors.OpPrereqError("File %s inside %s package is not safe" %
1118
                                   (file_name, OVA_EXT), errors.ECODE_ENVIRON)
1119
      if file_name.endswith(OVF_EXT):
1120
        input_name = file_name
1121
    if not input_name:
1122
      raise errors.OpPrereqError("No %s file in %s package found" %
1123
                                 (OVF_EXT, OVA_EXT), errors.ECODE_ENVIRON)
1124
    logging.warning("Unpacking the %s archive, this may take a while",
1125
      input_path)
1126
    self.input_dir = temp_dir
1127
    self.input_path = utils.PathJoin(self.temp_dir, input_name)
1128
    try:
1129
      try:
1130
        extract = ova_content.extractall
1131
      except AttributeError:
1132
        # This is a prehistorical case of using python < 2.5
1133
        for member in ova_content.getmembers():
1134
          ova_content.extract(member, path=self.temp_dir)
1135
      else:
1136
        extract(self.temp_dir)
1137
    except tarfile.TarError, err:
1138
      raise errors.OpPrereqError("Error while extracting %s archive: %s" %
1139
                                 (OVA_EXT, err), errors.ECODE_ENVIRON)
1140
    logging.info("OVA package extracted to %s directory", self.temp_dir)
1141

    
1142
  def Parse(self):
1143
    """Parses the data and creates a structure containing all required info.
1144

1145
    The method reads the information given either as a command line option or as
1146
    a part of the OVF description.
1147

1148
    @raise errors.OpPrereqError: if some required part of the description of
1149
      virtual instance is missing or unable to create output directory
1150

1151
    """
1152
    self.results_name = self._GetInfo("instance name", self.options.name,
1153
      self._ParseNameOptions, self.ovf_reader.GetInstanceName)
1154
    if not self.results_name:
1155
      raise errors.OpPrereqError("Name of instance not provided",
1156
                                 errors.ECODE_INVAL)
1157

    
1158
    self.output_dir = utils.PathJoin(self.output_dir, self.results_name)
1159
    try:
1160
      utils.Makedirs(self.output_dir)
1161
    except OSError, err:
1162
      raise errors.OpPrereqError("Failed to create directory %s: %s" %
1163
                                 (self.output_dir, err), errors.ECODE_ENVIRON)
1164

    
1165
    self.results_template = self._GetInfo("disk template",
1166
      self.options.disk_template, self._ParseTemplateOptions,
1167
      self.ovf_reader.GetDiskTemplate)
1168
    if not self.results_template:
1169
      logging.info("Disk template not given")
1170

    
1171
    self.results_hypervisor = self._GetInfo("hypervisor",
1172
      self.options.hypervisor, self._ParseHypervisorOptions,
1173
      self.ovf_reader.GetHypervisorData)
1174
    assert self.results_hypervisor["hypervisor_name"]
1175
    if self.results_hypervisor["hypervisor_name"] == constants.VALUE_AUTO:
1176
      logging.debug("Default hypervisor settings from the cluster will be used")
1177

    
1178
    self.results_os = self._GetInfo("OS", self.options.os,
1179
      self._ParseOSOptions, self.ovf_reader.GetOSData)
1180
    if not self.results_os.get("os_name"):
1181
      raise errors.OpPrereqError("OS name must be provided",
1182
                                 errors.ECODE_INVAL)
1183

    
1184
    self.results_backend = self._GetInfo("backend", self.options.beparams,
1185
      self._ParseBackendOptions, self.ovf_reader.GetBackendData)
1186
    assert self.results_backend.get("vcpus")
1187
    assert self.results_backend.get("memory")
1188
    assert self.results_backend.get("auto_balance") is not None
1189

    
1190
    self.results_tags = self._GetInfo("tags", self.options.tags,
1191
      self._ParseTags, self.ovf_reader.GetTagsData)
1192

    
1193
    ovf_version = self.ovf_reader.GetVersionData()
1194
    if ovf_version:
1195
      self.results_version = ovf_version
1196
    else:
1197
      self.results_version = constants.EXPORT_VERSION
1198

    
1199
    self.results_network = self._GetInfo("network", self.options.nics,
1200
      self._ParseNicOptions, self.ovf_reader.GetNetworkData,
1201
      ignore_test=self.options.no_nics)
1202

    
1203
    self.results_disk = self._GetInfo("disk", self.options.disks,
1204
      self._ParseDiskOptions, self._GetDiskInfo,
1205
      ignore_test=self.results_template == constants.DT_DISKLESS)
1206

    
1207
    if not self.results_disk and not self.results_network:
1208
      raise errors.OpPrereqError("Either disk specification or network"
1209
                                 " description must be present",
1210
                                 errors.ECODE_STATE)
1211

    
1212
  @staticmethod
1213
  def _GetInfo(name, cmd_arg, cmd_function, nocmd_function,
1214
    ignore_test=False):
1215
    """Get information about some section - e.g. disk, network, hypervisor.
1216

1217
    @type name: string
1218
    @param name: name of the section
1219
    @type cmd_arg: dict
1220
    @param cmd_arg: command line argument specific for section 'name'
1221
    @type cmd_function: callable
1222
    @param cmd_function: function to call if 'cmd_args' exists
1223
    @type nocmd_function: callable
1224
    @param nocmd_function: function to call if 'cmd_args' is not there
1225

1226
    """
1227
    if ignore_test:
1228
      logging.info("Information for %s will be ignored", name)
1229
      return {}
1230
    if cmd_arg:
1231
      logging.info("Information for %s will be parsed from command line", name)
1232
      results = cmd_function()
1233
    else:
1234
      logging.info("Information for %s will be parsed from %s file",
1235
        name, OVF_EXT)
1236
      results = nocmd_function()
1237
    logging.info("Options for %s were succesfully read", name)
1238
    return results
1239

    
1240
  def _ParseNameOptions(self):
1241
    """Returns name if one was given in command line.
1242

1243
    @rtype: string
1244
    @return: name of an instance
1245

1246
    """
1247
    return self.options.name
1248

    
1249
  def _ParseTemplateOptions(self):
1250
    """Returns disk template if one was given in command line.
1251

1252
    @rtype: string
1253
    @return: disk template name
1254

1255
    """
1256
    return self.options.disk_template
1257

    
1258
  def _ParseHypervisorOptions(self):
1259
    """Parses hypervisor options given in a command line.
1260

1261
    @rtype: dict
1262
    @return: dictionary containing name of the chosen hypervisor and all the
1263
      options
1264

1265
    """
1266
    assert type(self.options.hypervisor) is tuple
1267
    assert len(self.options.hypervisor) == 2
1268
    results = {}
1269
    if self.options.hypervisor[0]:
1270
      results["hypervisor_name"] = self.options.hypervisor[0]
1271
    else:
1272
      results["hypervisor_name"] = constants.VALUE_AUTO
1273
    results.update(self.options.hypervisor[1])
1274
    return results
1275

    
1276
  def _ParseOSOptions(self):
1277
    """Parses OS options given in command line.
1278

1279
    @rtype: dict
1280
    @return: dictionary containing name of chosen OS and all its options
1281

1282
    """
1283
    assert self.options.os
1284
    results = {}
1285
    results["os_name"] = self.options.os
1286
    results.update(self.options.osparams)
1287
    return results
1288

    
1289
  def _ParseBackendOptions(self):
1290
    """Parses backend options given in command line.
1291

1292
    @rtype: dict
1293
    @return: dictionary containing vcpus, memory and auto-balance options
1294

1295
    """
1296
    assert self.options.beparams
1297
    backend = {}
1298
    backend.update(self.options.beparams)
1299
    must_contain = ["vcpus", "memory", "auto_balance"]
1300
    for element in must_contain:
1301
      if backend.get(element) is None:
1302
        backend[element] = constants.VALUE_AUTO
1303
    return backend
1304

    
1305
  def _ParseTags(self):
1306
    """Returns tags list given in command line.
1307

1308
    @rtype: string
1309
    @return: string containing comma-separated tags
1310

1311
    """
1312
    return self.options.tags
1313

    
1314
  def _ParseNicOptions(self):
1315
    """Parses network options given in a command line or as a dictionary.
1316

1317
    @rtype: dict
1318
    @return: dictionary of network-related options
1319

1320
    """
1321
    assert self.options.nics
1322
    results = {}
1323
    for (nic_id, nic_desc) in self.options.nics:
1324
      results["nic%s_mode" % nic_id] = \
1325
        nic_desc.get("mode", constants.VALUE_AUTO)
1326
      results["nic%s_mac" % nic_id] = nic_desc.get("mac", constants.VALUE_AUTO)
1327
      results["nic%s_link" % nic_id] = \
1328
        nic_desc.get("link", constants.VALUE_AUTO)
1329
      if nic_desc.get("mode") == "bridged":
1330
        results["nic%s_ip" % nic_id] = constants.VALUE_NONE
1331
      else:
1332
        results["nic%s_ip" % nic_id] = constants.VALUE_AUTO
1333
    results["nic_count"] = str(len(self.options.nics))
1334
    return results
1335

    
1336
  def _ParseDiskOptions(self):
1337
    """Parses disk options given in a command line.
1338

1339
    @rtype: dict
1340
    @return: dictionary of disk-related options
1341

1342
    @raise errors.OpPrereqError: disk description does not contain size
1343
      information or size information is invalid or creation failed
1344

1345
    """
1346
    CheckQemuImg()
1347
    assert self.options.disks
1348
    results = {}
1349
    for (disk_id, disk_desc) in self.options.disks:
1350
      results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id
1351
      if disk_desc.get("size"):
1352
        try:
1353
          disk_size = utils.ParseUnit(disk_desc["size"])
1354
        except ValueError:
1355
          raise errors.OpPrereqError("Invalid disk size for disk %s: %s" %
1356
                                     (disk_id, disk_desc["size"]),
1357
                                     errors.ECODE_INVAL)
1358
        new_path = utils.PathJoin(self.output_dir, str(disk_id))
1359
        args = [
1360
          constants.QEMUIMG_PATH,
1361
          "create",
1362
          "-f",
1363
          "raw",
1364
          new_path,
1365
          disk_size,
1366
        ]
1367
        run_result = utils.RunCmd(args)
1368
        if run_result.failed:
1369
          raise errors.OpPrereqError("Creation of disk %s failed, output was:"
1370
                                     " %s" % (new_path, run_result.stderr),
1371
                                     errors.ECODE_ENVIRON)
1372
        results["disk%s_size" % disk_id] = str(disk_size)
1373
        results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id
1374
      else:
1375
        raise errors.OpPrereqError("Disks created for import must have their"
1376
                                   " size specified",
1377
                                   errors.ECODE_INVAL)
1378
    results["disk_count"] = str(len(self.options.disks))
1379
    return results
1380

    
1381
  def _GetDiskInfo(self):
1382
    """Gathers information about disks used by instance, perfomes conversion.
1383

1384
    @rtype: dict
1385
    @return: dictionary of disk-related options
1386

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

1389
    """
1390
    results = {}
1391
    disks_list = self.ovf_reader.GetDisksNames()
1392
    for (counter, (disk_name, disk_compression)) in enumerate(disks_list):
1393
      if os.path.dirname(disk_name):
1394
        raise errors.OpPrereqError("Disks are not allowed to have absolute"
1395
                                   " paths or paths outside main OVF"
1396
                                   " directory", errors.ECODE_ENVIRON)
1397
      disk, _ = os.path.splitext(disk_name)
1398
      disk_path = utils.PathJoin(self.input_dir, disk_name)
1399
      if disk_compression not in NO_COMPRESSION:
1400
        _, disk_path = self._CompressDisk(disk_path, disk_compression,
1401
          DECOMPRESS)
1402
        disk, _ = os.path.splitext(disk)
1403
      if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw":
1404
        logging.info("Conversion to raw format is required")
1405
      ext, new_disk_path = self._ConvertDisk("raw", disk_path)
1406

    
1407
      final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext,
1408
        directory=self.output_dir)
1409
      final_name = os.path.basename(final_disk_path)
1410
      disk_size = os.path.getsize(final_disk_path) / (1024 * 1024)
1411
      results["disk%s_dump" % counter] = final_name
1412
      results["disk%s_size" % counter] = str(disk_size)
1413
      results["disk%s_ivname" % counter] = "disk/%s" % str(counter)
1414
    if disks_list:
1415
      results["disk_count"] = str(len(disks_list))
1416
    return results
1417

    
1418
  def Save(self):
1419
    """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
1420

1421
    @raise errors.OpPrereqError: when saving to config file failed
1422

1423
    """
1424
    logging.info("Conversion was succesfull, saving %s in %s directory",
1425
                 constants.EXPORT_CONF_FILE, self.output_dir)
1426
    results = {
1427
      constants.INISECT_INS: {},
1428
      constants.INISECT_BEP: {},
1429
      constants.INISECT_EXP: {},
1430
      constants.INISECT_OSP: {},
1431
      constants.INISECT_HYP: {},
1432
    }
1433

    
1434
    results[constants.INISECT_INS].update(self.results_disk)
1435
    results[constants.INISECT_INS].update(self.results_network)
1436
    results[constants.INISECT_INS]["hypervisor"] = \
1437
      self.results_hypervisor["hypervisor_name"]
1438
    results[constants.INISECT_INS]["name"] = self.results_name
1439
    if self.results_template:
1440
      results[constants.INISECT_INS]["disk_template"] = self.results_template
1441
    if self.results_tags:
1442
      results[constants.INISECT_INS]["tags"] = self.results_tags
1443

    
1444
    results[constants.INISECT_BEP].update(self.results_backend)
1445

    
1446
    results[constants.INISECT_EXP]["os"] = self.results_os["os_name"]
1447
    results[constants.INISECT_EXP]["version"] = self.results_version
1448

    
1449
    del self.results_os["os_name"]
1450
    results[constants.INISECT_OSP].update(self.results_os)
1451

    
1452
    del self.results_hypervisor["hypervisor_name"]
1453
    results[constants.INISECT_HYP].update(self.results_hypervisor)
1454

    
1455
    output_file_name = utils.PathJoin(self.output_dir,
1456
      constants.EXPORT_CONF_FILE)
1457

    
1458
    output = []
1459
    for section, options in results.iteritems():
1460
      output.append("[%s]" % section)
1461
      for name, value in options.iteritems():
1462
        if value is None:
1463
          value = ""
1464
        output.append("%s = %s" % (name, value))
1465
      output.append("")
1466
    output_contents = "\n".join(output)
1467

    
1468
    try:
1469
      utils.WriteFile(output_file_name, data=output_contents)
1470
    except errors.ProgrammerError, err:
1471
      raise errors.OpPrereqError("Saving the config file failed: %s" % err,
1472
                                 errors.ECODE_ENVIRON)
1473

    
1474
    self.Cleanup()
1475

    
1476

    
1477
class ConfigParserWithDefaults(ConfigParser.SafeConfigParser):
1478
  """This is just a wrapper on SafeConfigParser, that uses default values
1479

1480
  """
1481
  def get(self, section, options, raw=None, vars=None): # pylint: disable=W0622
1482
    try:
1483
      result = ConfigParser.SafeConfigParser.get(self, section, options, \
1484
        raw=raw, vars=vars)
1485
    except ConfigParser.NoOptionError:
1486
      result = None
1487
    return result
1488

    
1489
  def getint(self, section, options):
1490
    try:
1491
      result = ConfigParser.SafeConfigParser.get(self, section, options)
1492
    except ConfigParser.NoOptionError:
1493
      result = 0
1494
    return int(result)
1495

    
1496

    
1497
class OVFExporter(Converter):
1498
  """Converter from Ganeti config file to OVF
1499

1500
  @type input_dir: string
1501
  @ivar input_dir: directory in which the config.ini file resides
1502
  @type output_dir: string
1503
  @ivar output_dir: directory to which the results of conversion shall be
1504
    written
1505
  @type packed_dir: string
1506
  @ivar packed_dir: if we want OVA package, this points to the real (i.e. not
1507
    temp) output directory
1508
  @type input_path: string
1509
  @ivar input_path: complete path to the config.ini file
1510
  @type output_path: string
1511
  @ivar output_path: complete path to .ovf file
1512
  @type config_parser: L{ConfigParserWithDefaults}
1513
  @ivar config_parser: parser for the config.ini file
1514
  @type reference_files: list
1515
  @ivar reference_files: files referenced in the ovf file
1516
  @type results_disk: list
1517
  @ivar results_disk: list of dictionaries of disk options from config.ini
1518
  @type results_network: list
1519
  @ivar results_network: list of dictionaries of network options form config.ini
1520
  @type results_name: string
1521
  @ivar results_name: name of the instance
1522
  @type results_vcpus: string
1523
  @ivar results_vcpus: number of VCPUs
1524
  @type results_memory: string
1525
  @ivar results_memory: RAM memory in MB
1526
  @type results_ganeti: dict
1527
  @ivar results_ganeti: dictionary of Ganeti-specific options from config.ini
1528

1529
  """
1530
  def _ReadInputData(self, input_path):
1531
    """Reads the data on which the conversion will take place.
1532

1533
    @type input_path: string
1534
    @param input_path: absolute path to the config.ini input file
1535

1536
    @raise errors.OpPrereqError: error when reading the config file
1537

1538
    """
1539
    input_dir = os.path.dirname(input_path)
1540
    self.input_path = input_path
1541
    self.input_dir = input_dir
1542
    if self.options.output_dir:
1543
      self.output_dir = os.path.abspath(self.options.output_dir)
1544
    else:
1545
      self.output_dir = input_dir
1546
    self.config_parser = ConfigParserWithDefaults()
1547
    logging.info("Reading configuration from %s file", input_path)
1548
    try:
1549
      self.config_parser.read(input_path)
1550
    except ConfigParser.MissingSectionHeaderError, err:
1551
      raise errors.OpPrereqError("Error when trying to read %s: %s" %
1552
                                 (input_path, err), errors.ECODE_ENVIRON)
1553
    if self.options.ova_package:
1554
      self.temp_dir = tempfile.mkdtemp()
1555
      self.packed_dir = self.output_dir
1556
      self.output_dir = self.temp_dir
1557

    
1558
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1559

    
1560
  def _ParseName(self):
1561
    """Parses name from command line options or config file.
1562

1563
    @rtype: string
1564
    @return: name of Ganeti instance
1565

1566
    @raise errors.OpPrereqError: if name of the instance is not provided
1567

1568
    """
1569
    if self.options.name:
1570
      name = self.options.name
1571
    else:
1572
      name = self.config_parser.get(constants.INISECT_INS, NAME)
1573
    if name is None:
1574
      raise errors.OpPrereqError("No instance name found",
1575
                                 errors.ECODE_ENVIRON)
1576
    return name
1577

    
1578
  def _ParseVCPUs(self):
1579
    """Parses vcpus number from config file.
1580

1581
    @rtype: int
1582
    @return: number of virtual CPUs
1583

1584
    @raise errors.OpPrereqError: if number of VCPUs equals 0
1585

1586
    """
1587
    vcpus = self.config_parser.getint(constants.INISECT_BEP, VCPUS)
1588
    if vcpus == 0:
1589
      raise errors.OpPrereqError("No CPU information found",
1590
                                 errors.ECODE_ENVIRON)
1591
    return vcpus
1592

    
1593
  def _ParseMemory(self):
1594
    """Parses vcpus number from config file.
1595

1596
    @rtype: int
1597
    @return: amount of memory in MB
1598

1599
    @raise errors.OpPrereqError: if amount of memory equals 0
1600

1601
    """
1602
    memory = self.config_parser.getint(constants.INISECT_BEP, MEMORY)
1603
    if memory == 0:
1604
      raise errors.OpPrereqError("No memory information found",
1605
                                 errors.ECODE_ENVIRON)
1606
    return memory
1607

    
1608
  def _ParseGaneti(self):
1609
    """Parses Ganeti data from config file.
1610

1611
    @rtype: dictionary
1612
    @return: dictionary of Ganeti-specific options
1613

1614
    """
1615
    results = {}
1616
    # hypervisor
1617
    results["hypervisor"] = {}
1618
    hyp_name = self.config_parser.get(constants.INISECT_INS, HYPERV)
1619
    if hyp_name is None:
1620
      raise errors.OpPrereqError("No hypervisor information found",
1621
                                 errors.ECODE_ENVIRON)
1622
    results["hypervisor"]["name"] = hyp_name
1623
    pairs = self.config_parser.items(constants.INISECT_HYP)
1624
    for (name, value) in pairs:
1625
      results["hypervisor"][name] = value
1626
    # os
1627
    results["os"] = {}
1628
    os_name = self.config_parser.get(constants.INISECT_EXP, OS)
1629
    if os_name is None:
1630
      raise errors.OpPrereqError("No operating system information found",
1631
                                 errors.ECODE_ENVIRON)
1632
    results["os"]["name"] = os_name
1633
    pairs = self.config_parser.items(constants.INISECT_OSP)
1634
    for (name, value) in pairs:
1635
      results["os"][name] = value
1636
    # other
1637
    others = [
1638
      (constants.INISECT_INS, DISK_TEMPLATE, "disk_template"),
1639
      (constants.INISECT_BEP, AUTO_BALANCE, "auto_balance"),
1640
      (constants.INISECT_INS, TAGS, "tags"),
1641
      (constants.INISECT_EXP, VERSION, "version"),
1642
    ]
1643
    for (section, element, name) in others:
1644
      results[name] = self.config_parser.get(section, element)
1645
    return results
1646

    
1647
  def _ParseNetworks(self):
1648
    """Parses network data from config file.
1649

1650
    @rtype: list
1651
    @return: list of dictionaries of network options
1652

1653
    @raise errors.OpPrereqError: then network mode is not recognized
1654

1655
    """
1656
    results = []
1657
    counter = 0
1658
    while True:
1659
      data_link = \
1660
        self.config_parser.get(constants.INISECT_INS, "nic%s_link" % counter)
1661
      if data_link is None:
1662
        break
1663
      results.append({
1664
        "mode": self.config_parser.get(constants.INISECT_INS,
1665
           "nic%s_mode" % counter),
1666
        "mac": self.config_parser.get(constants.INISECT_INS,
1667
           "nic%s_mac" % counter),
1668
        "ip": self.config_parser.get(constants.INISECT_INS,
1669
           "nic%s_ip" % counter),
1670
        "link": data_link,
1671
      })
1672
      if results[counter]["mode"] not in constants.NIC_VALID_MODES:
1673
        raise errors.OpPrereqError("Network mode %s not recognized"
1674
                                   % results[counter]["mode"],
1675
                                   errors.ECODE_INVAL)
1676
      counter += 1
1677
    return results
1678

    
1679
  def _GetDiskOptions(self, disk_file, compression):
1680
    """Convert the disk and gather disk info for .ovf file.
1681

1682
    @type disk_file: string
1683
    @param disk_file: name of the disk (without the full path)
1684
    @type compression: bool
1685
    @param compression: whether the disk should be compressed or not
1686

1687
    @raise errors.OpPrereqError: when disk image does not exist
1688

1689
    """
1690
    disk_path = utils.PathJoin(self.input_dir, disk_file)
1691
    results = {}
1692
    if not os.path.isfile(disk_path):
1693
      raise errors.OpPrereqError("Disk image does not exist: %s" % disk_path,
1694
                                 errors.ECODE_ENVIRON)
1695
    if os.path.dirname(disk_file):
1696
      raise errors.OpPrereqError("Path for the disk: %s contains a directory"
1697
                                 " name" % disk_path, errors.ECODE_ENVIRON)
1698
    disk_name, _ = os.path.splitext(disk_file)
1699
    ext, new_disk_path = self._ConvertDisk(self.options.disk_format, disk_path)
1700
    results["format"] = self.options.disk_format
1701
    results["virt-size"] = self._GetDiskQemuInfo(new_disk_path,
1702
      "virtual size: \S+ \((\d+) bytes\)")
1703
    if compression:
1704
      ext2, new_disk_path = self._CompressDisk(new_disk_path, "gzip",
1705
        COMPRESS)
1706
      disk_name, _ = os.path.splitext(disk_name)
1707
      results["compression"] = "gzip"
1708
      ext += ext2
1709
    final_disk_path = LinkFile(new_disk_path, prefix=disk_name, suffix=ext,
1710
      directory=self.output_dir)
1711
    final_disk_name = os.path.basename(final_disk_path)
1712
    results["real-size"] = os.path.getsize(final_disk_path)
1713
    results["path"] = final_disk_name
1714
    self.references_files.append(final_disk_path)
1715
    return results
1716

    
1717
  def _ParseDisks(self):
1718
    """Parses disk data from config file.
1719

1720
    @rtype: list
1721
    @return: list of dictionaries of disk options
1722

1723
    """
1724
    results = []
1725
    counter = 0
1726
    while True:
1727
      disk_file = \
1728
        self.config_parser.get(constants.INISECT_INS, "disk%s_dump" % counter)
1729
      if disk_file is None:
1730
        break
1731
      results.append(self._GetDiskOptions(disk_file, self.options.compression))
1732
      counter += 1
1733
    return results
1734

    
1735
  def Parse(self):
1736
    """Parses the data and creates a structure containing all required info.
1737

1738
    """
1739
    try:
1740
      utils.Makedirs(self.output_dir)
1741
    except OSError, err:
1742
      raise errors.OpPrereqError("Failed to create directory %s: %s" %
1743
                                 (self.output_dir, err), errors.ECODE_ENVIRON)
1744

    
1745
    self.references_files = []
1746
    self.results_name = self._ParseName()
1747
    self.results_vcpus = self._ParseVCPUs()
1748
    self.results_memory = self._ParseMemory()
1749
    if not self.options.ext_usage:
1750
      self.results_ganeti = self._ParseGaneti()
1751
    self.results_network = self._ParseNetworks()
1752
    self.results_disk = self._ParseDisks()
1753

    
1754
  def _PrepareManifest(self, path):
1755
    """Creates manifest for all the files in OVF package.
1756

1757
    @type path: string
1758
    @param path: path to manifesto file
1759

1760
    @raise errors.OpPrereqError: if error occurs when writing file
1761

1762
    """
1763
    logging.info("Preparing manifest for the OVF package")
1764
    lines = []
1765
    files_list = [self.output_path]
1766
    files_list.extend(self.references_files)
1767
    logging.warning("Calculating SHA1 checksums, this may take a while")
1768
    sha1_sums = utils.FingerprintFiles(files_list)
1769
    for file_path, value in sha1_sums.iteritems():
1770
      file_name = os.path.basename(file_path)
1771
      lines.append("SHA1(%s)= %s" % (file_name, value))
1772
    lines.append("")
1773
    data = "\n".join(lines)
1774
    try:
1775
      utils.WriteFile(path, data=data)
1776
    except errors.ProgrammerError, err:
1777
      raise errors.OpPrereqError("Saving the manifest file failed: %s" % err,
1778
                                 errors.ECODE_ENVIRON)
1779

    
1780
  @staticmethod
1781
  def _PrepareTarFile(tar_path, files_list):
1782
    """Creates tarfile from the files in OVF package.
1783

1784
    @type tar_path: string
1785
    @param tar_path: path to the resulting file
1786
    @type files_list: list
1787
    @param files_list: list of files in the OVF package
1788

1789
    """
1790
    logging.info("Preparing tarball for the OVF package")
1791
    open(tar_path, mode="w").close()
1792
    ova_package = tarfile.open(name=tar_path, mode="w")
1793
    for file_path in files_list:
1794
      file_name = os.path.basename(file_path)
1795
      ova_package.add(file_path, arcname=file_name)
1796
    ova_package.close()
1797

    
1798
  def Save(self):
1799
    """Saves the gathered configuration in an apropriate format.
1800

1801
    @raise errors.OpPrereqError: if unable to create output directory
1802

1803
    """
1804
    output_file = "%s%s" % (self.results_name, OVF_EXT)
1805
    output_path = utils.PathJoin(self.output_dir, output_file)
1806
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1807
    logging.info("Saving read data to %s", output_path)
1808

    
1809
    self.output_path = utils.PathJoin(self.output_dir, output_file)
1810
    files_list = [self.output_path]
1811

    
1812
    self.ovf_writer.SaveDisksData(self.results_disk)
1813
    self.ovf_writer.SaveNetworksData(self.results_network)
1814
    if not self.options.ext_usage:
1815
      self.ovf_writer.SaveGanetiData(self.results_ganeti, self.results_network)
1816

    
1817
    self.ovf_writer.SaveVirtualSystemData(self.results_name, self.results_vcpus,
1818
      self.results_memory)
1819

    
1820
    data = self.ovf_writer.PrettyXmlDump()
1821
    utils.WriteFile(self.output_path, data=data)
1822

    
1823
    manifest_file = "%s%s" % (self.results_name, MF_EXT)
1824
    manifest_path = utils.PathJoin(self.output_dir, manifest_file)
1825
    self._PrepareManifest(manifest_path)
1826
    files_list.append(manifest_path)
1827

    
1828
    files_list.extend(self.references_files)
1829

    
1830
    if self.options.ova_package:
1831
      ova_file = "%s%s" % (self.results_name, OVA_EXT)
1832
      packed_path = utils.PathJoin(self.packed_dir, ova_file)
1833
      try:
1834
        utils.Makedirs(self.packed_dir)
1835
      except OSError, err:
1836
        raise errors.OpPrereqError("Failed to create directory %s: %s" %
1837
                                   (self.packed_dir, err),
1838
                                   errors.ECODE_ENVIRON)
1839
      self._PrepareTarFile(packed_path, files_list)
1840
    logging.info("Creation of the OVF package was successfull")
1841
    self.Cleanup()