Statistics
| Branch: | Tag: | Revision:

root / lib / ovf.py @ f92ed8ab

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
from ganeti import pathutils
57

    
58

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

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

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

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

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

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

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

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

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

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

    
147

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

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

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

    
158

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

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

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

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

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

    
199

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

435
    """
436
    results = {}
437

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

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

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

    
473
    return results
474

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

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

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

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

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

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

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

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

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

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

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

    
544
      network_name = network_name.lower()
545

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

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

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

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

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

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

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

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

    
593

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

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

    
605

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
836

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

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

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

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

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

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

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

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

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

882
    """
883
    raise NotImplementedError()
884

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

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

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

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

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

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

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

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

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

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

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

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

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

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

997
    """
998
    raise NotImplementedError()
999

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

1003
    """
1004
    raise NotImplementedError()
1005

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

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

    
1015

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1477
    self.Cleanup()
1478

    
1479

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

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

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

    
1499

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1831
    files_list.extend(self.references_files)
1832

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