Statistics
| Branch: | Tag: | Revision:

root / lib / ovf.py @ 5ae4945a

History | View | Annotate | Download (65.5 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 ALLOCATION_UNITS.items()
455
                        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(
944
      suffix=".%s" % disk_format, 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,
1154
                                      self.ovf_reader.GetInstanceName)
1155
    if not self.results_name:
1156
      raise errors.OpPrereqError("Name of instance not provided",
1157
                                 errors.ECODE_INVAL)
1158

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

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

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

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

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

    
1192
    self.results_tags = self._GetInfo(
1193
      "tags", self.options.tags, self._ParseTags, self.ovf_reader.GetTagsData)
1194

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

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

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

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

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

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

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

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

1245
    @rtype: string
1246
    @return: name of an instance
1247

1248
    """
1249
    return self.options.name
1250

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

1254
    @rtype: string
1255
    @return: disk template name
1256

1257
    """
1258
    return self.options.disk_template
1259

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

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

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

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

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

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

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

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

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

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

1310
    @rtype: string
1311
    @return: string containing comma-separated tags
1312

1313
    """
1314
    return self.options.tags
1315

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

1319
    @rtype: dict
1320
    @return: dictionary of network-related options
1321

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

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

1341
    @rtype: dict
1342
    @return: dictionary of disk-related options
1343

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

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

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

1386
    @rtype: dict
1387
    @return: dictionary of disk-related options
1388

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

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

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

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

1423
    @raise errors.OpPrereqError: when saving to config file failed
1424

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

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

    
1446
    results[constants.INISECT_BEP].update(self.results_backend)
1447

    
1448
    results[constants.INISECT_EXP]["os"] = self.results_os["os_name"]
1449
    results[constants.INISECT_EXP]["version"] = self.results_version
1450

    
1451
    del self.results_os["os_name"]
1452
    results[constants.INISECT_OSP].update(self.results_os)
1453

    
1454
    del self.results_hypervisor["hypervisor_name"]
1455
    results[constants.INISECT_HYP].update(self.results_hypervisor)
1456

    
1457
    output_file_name = utils.PathJoin(self.output_dir,
1458
                                      constants.EXPORT_CONF_FILE)
1459

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

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

    
1476
    self.Cleanup()
1477

    
1478

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

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

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

    
1498

    
1499
class OVFExporter(Converter):
1500
  """Converter from Ganeti config file to OVF
1501

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

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

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

1538
    @raise errors.OpPrereqError: error when reading the config file
1539

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

    
1560
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1561

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

1565
    @rtype: string
1566
    @return: name of Ganeti instance
1567

1568
    @raise errors.OpPrereqError: if name of the instance is not provided
1569

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

    
1580
  def _ParseVCPUs(self):
1581
    """Parses vcpus number from config file.
1582

1583
    @rtype: int
1584
    @return: number of virtual CPUs
1585

1586
    @raise errors.OpPrereqError: if number of VCPUs equals 0
1587

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

    
1595
  def _ParseMemory(self):
1596
    """Parses vcpus number from config file.
1597

1598
    @rtype: int
1599
    @return: amount of memory in MB
1600

1601
    @raise errors.OpPrereqError: if amount of memory equals 0
1602

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

    
1610
  def _ParseGaneti(self):
1611
    """Parses Ganeti data from config file.
1612

1613
    @rtype: dictionary
1614
    @return: dictionary of Ganeti-specific options
1615

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

    
1649
  def _ParseNetworks(self):
1650
    """Parses network data from config file.
1651

1652
    @rtype: list
1653
    @return: list of dictionaries of network options
1654

1655
    @raise errors.OpPrereqError: then network mode is not recognized
1656

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

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

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

1689
    @raise errors.OpPrereqError: when disk image does not exist
1690

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

    
1719
  def _ParseDisks(self):
1720
    """Parses disk data from config file.
1721

1722
    @rtype: list
1723
    @return: list of dictionaries of disk options
1724

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

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

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

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

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

1759
    @type path: string
1760
    @param path: path to manifesto file
1761

1762
    @raise errors.OpPrereqError: if error occurs when writing file
1763

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

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

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

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

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

1803
    @raise errors.OpPrereqError: if unable to create output directory
1804

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

    
1811
    self.output_path = utils.PathJoin(self.output_dir, output_file)
1812
    files_list = [self.output_path]
1813

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

    
1819
    self.ovf_writer.SaveVirtualSystemData(self.results_name, self.results_vcpus,
1820
                                          self.results_memory)
1821

    
1822
    data = self.ovf_writer.PrettyXmlDump()
1823
    utils.WriteFile(self.output_path, data=data)
1824

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

    
1830
    files_list.extend(self.references_files)
1831

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