Statistics
| Branch: | Tag: | Revision:

root / lib / ovf.py @ 6ece11be

History | View | Annotate | Download (63.5 kB)

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

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

    
21

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

24
"""
25

    
26
# pylint: disable=F0401, E1101
27

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

    
31

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

    
48
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")
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
  return new_path
196

    
197

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

341
    @raise errors.OpPrereqError: if SHA1 checksums do not match
342

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

    
366
  def GetInstanceName(self):
367
    """Provides information about instance name.
368

369
    @rtype: string
370
    @return: instance name string
371

372
    """
373
    find_name = "{%s}VirtualSystem/{%s}Name" % (OVF_SCHEMA, OVF_SCHEMA)
374
    return self.tree.findtext(find_name)
375

    
376
  def GetDiskTemplate(self):
377
    """Returns disk template from .ovf file
378

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

    
386
  def GetHypervisorData(self):
387
    """Provides hypervisor information - hypervisor name and options.
388

389
    @rtype: dict
390
    @return: dictionary containing name of the used hypervisor and all the
391
      specified options
392

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

    
407
  def GetOSData(self):
408
    """ Provides operating system information - os name and options.
409

410
    @rtype: dict
411
    @return: dictionary containing name and options for the chosen OS
412

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

    
424
  def GetBackendData(self):
425
    """ Provides backend information - vcpus, memory, auto balancing options.
426

427
    @rtype: dict
428
    @return: dictionary containing options for vcpus, memory and auto balance
429
      settings
430

431
    """
432
    results = {}
433

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

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

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

    
469
    return results
470

    
471
  def GetTagsData(self):
472
    """Provides tags information for instance.
473

474
    @rtype: string or None
475
    @return: string of comma-separated tags for the instance
476

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

    
485
  def GetVersionData(self):
486
    """Provides version number read from .ovf file
487

488
    @rtype: string
489
    @return: string containing the version number
490

491
    """
492
    find_version = ("{%s}GanetiSection/{%s}Version" %
493
                    (GANETI_SCHEMA, GANETI_SCHEMA))
494
    return self.tree.findtext(find_version)
495

    
496
  def GetNetworkData(self):
497
    """Provides data about the network in the OVF instance.
498

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

505
    @rtype: dict
506
    @return: dictionary containing all the network information
507

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

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

    
542
      network_name = network_name.lower()
543

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

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

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

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

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

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

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

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

    
590

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

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

    
602

    
603
class OVFWriter(object):
604
  """Writer class for OVF files.
605

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

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

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

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

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

647
    @type disks: list
648
    @param disks: list of dictionaries of disk options from config.ini
649

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

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

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

686
    @type networks: list
687
    @param networks: list of dictionaries of network options form config.ini
688

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

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

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

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

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

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

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

733
    """
734
    ganeti_section = ET.SubElement(self.tree, "gnt:GanetiSection")
735

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
834

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

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

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

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

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

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

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

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

876
    @type input_path: string
877
    @param input_path: absolute path to the Converter input file
878

879
    """
880
    raise NotImplementedError()
881

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

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

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

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

    
920
  def _ConvertDisk(self, disk_format, disk_path):
921
    """Performes conversion to specified format.
922

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

930
    @raise errors.OpPrereqError: convertion of the disk failed
931

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

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

    
957
  @staticmethod
958
  def _GetDiskQemuInfo(disk_path, regexp):
959
    """Figures out some information of the disk using qemu-img.
960

961
    @type disk_path: string
962
    @param disk_path: path to the disk we want to know the format of
963
    @type regexp: string
964
    @param regexp: string that has to be matched, it has to contain one group
965
    @rtype: string
966
    @return: disk format
967

968
    @raise errors.OpPrereqError: format information cannot be retrieved
969

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

    
987
  def Parse(self):
988
    """Parses the data and creates a structure containing all required info.
989

990
    """
991
    raise NotImplementedError()
992

    
993
  def Save(self):
994
    """Saves the gathered configuration in an apropriate format.
995

996
    """
997
    raise NotImplementedError()
998

    
999
  def Cleanup(self):
1000
    """Cleans the temporary directory, if one was created.
1001

1002
    """
1003
    self.temp_file_manager.Cleanup()
1004
    if self.temp_dir:
1005
      shutil.rmtree(self.temp_dir)
1006
      self.temp_dir = None
1007

    
1008

    
1009
class OVFImporter(Converter):
1010
  """Converter from OVF to Ganeti config file.
1011

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

1046
  """
1047
  def _ReadInputData(self, input_path):
1048
    """Reads the data on which the conversion will take place.
1049

1050
    @type input_path: string
1051
    @param input_path: absolute path to the .ovf or .ova input file
1052

1053
    @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
1054

1055
    """
1056
    (input_dir, input_file) = os.path.split(input_path)
1057
    (_, input_extension) = os.path.splitext(input_file)
1058

    
1059
    if input_extension == OVF_EXT:
1060
      logging.info("%s file extension found, no unpacking necessary", OVF_EXT)
1061
      self.input_dir = input_dir
1062
      self.input_path = input_path
1063
      self.temp_dir = None
1064
    elif input_extension == OVA_EXT:
1065
      logging.info("%s file extension found, proceeding to unpacking", OVA_EXT)
1066
      self._UnpackOVA(input_path)
1067
    else:
1068
      raise errors.OpPrereqError("Unknown file extension; expected %s or %s"
1069
                                 " file" % (OVA_EXT, OVF_EXT))
1070
    assert ((input_extension == OVA_EXT and self.temp_dir) or
1071
            (input_extension == OVF_EXT and not self.temp_dir))
1072
    assert self.input_dir in self.input_path
1073

    
1074
    if self.options.output_dir:
1075
      self.output_dir = os.path.abspath(self.options.output_dir)
1076
      if (os.path.commonprefix([constants.EXPORT_DIR, self.output_dir]) !=
1077
          constants.EXPORT_DIR):
1078
        logging.warning("Export path is not under %s directory, import to"
1079
                        " Ganeti using gnt-backup may fail",
1080
                        constants.EXPORT_DIR)
1081
    else:
1082
      self.output_dir = constants.EXPORT_DIR
1083

    
1084
    self.ovf_reader = OVFReader(self.input_path)
1085
    self.ovf_reader.VerifyManifest()
1086

    
1087
  def _UnpackOVA(self, input_path):
1088
    """Unpacks the .ova package into temporary directory.
1089

1090
    @type input_path: string
1091
    @param input_path: path to the .ova package file
1092

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

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

    
1135
  def Parse(self):
1136
    """Parses the data and creates a structure containing all required info.
1137

1138
    The method reads the information given either as a command line option or as
1139
    a part of the OVF description.
1140

1141
    @raise errors.OpPrereqError: if some required part of the description of
1142
      virtual instance is missing or unable to create output directory
1143

1144
    """
1145
    self.results_name = self._GetInfo("instance name", self.options.name,
1146
      self._ParseNameOptions, self.ovf_reader.GetInstanceName)
1147
    if not self.results_name:
1148
      raise errors.OpPrereqError("Name of instance not provided")
1149

    
1150
    self.output_dir = utils.PathJoin(self.output_dir, self.results_name)
1151
    try:
1152
      utils.Makedirs(self.output_dir)
1153
    except OSError, err:
1154
      raise errors.OpPrereqError("Failed to create directory %s: %s" %
1155
                                 (self.output_dir, err))
1156

    
1157
    self.results_template = self._GetInfo("disk template",
1158
      self.options.disk_template, self._ParseTemplateOptions,
1159
      self.ovf_reader.GetDiskTemplate)
1160
    if not self.results_template:
1161
      logging.info("Disk template not given")
1162

    
1163
    self.results_hypervisor = self._GetInfo("hypervisor",
1164
      self.options.hypervisor, self._ParseHypervisorOptions,
1165
      self.ovf_reader.GetHypervisorData)
1166
    assert self.results_hypervisor["hypervisor_name"]
1167
    if self.results_hypervisor["hypervisor_name"] == constants.VALUE_AUTO:
1168
      logging.debug("Default hypervisor settings from the cluster will be used")
1169

    
1170
    self.results_os = self._GetInfo("OS", self.options.os,
1171
      self._ParseOSOptions, self.ovf_reader.GetOSData)
1172
    if not self.results_os.get("os_name"):
1173
      raise errors.OpPrereqError("OS name must be provided")
1174

    
1175
    self.results_backend = self._GetInfo("backend", self.options.beparams,
1176
      self._ParseBackendOptions, self.ovf_reader.GetBackendData)
1177
    assert self.results_backend.get("vcpus")
1178
    assert self.results_backend.get("memory")
1179
    assert self.results_backend.get("auto_balance") is not None
1180

    
1181
    self.results_tags = self._GetInfo("tags", self.options.tags,
1182
      self._ParseTags, self.ovf_reader.GetTagsData)
1183

    
1184
    ovf_version = self.ovf_reader.GetVersionData()
1185
    if ovf_version:
1186
      self.results_version = ovf_version
1187
    else:
1188
      self.results_version = constants.EXPORT_VERSION
1189

    
1190
    self.results_network = self._GetInfo("network", self.options.nics,
1191
      self._ParseNicOptions, self.ovf_reader.GetNetworkData,
1192
      ignore_test=self.options.no_nics)
1193

    
1194
    self.results_disk = self._GetInfo("disk", self.options.disks,
1195
      self._ParseDiskOptions, self._GetDiskInfo,
1196
      ignore_test=self.results_template == constants.DT_DISKLESS)
1197

    
1198
    if not self.results_disk and not self.results_network:
1199
      raise errors.OpPrereqError("Either disk specification or network"
1200
                                 " description must be present")
1201

    
1202
  @staticmethod
1203
  def _GetInfo(name, cmd_arg, cmd_function, nocmd_function,
1204
    ignore_test=False):
1205
    """Get information about some section - e.g. disk, network, hypervisor.
1206

1207
    @type name: string
1208
    @param name: name of the section
1209
    @type cmd_arg: dict
1210
    @param cmd_arg: command line argument specific for section 'name'
1211
    @type cmd_function: callable
1212
    @param cmd_function: function to call if 'cmd_args' exists
1213
    @type nocmd_function: callable
1214
    @param nocmd_function: function to call if 'cmd_args' is not there
1215

1216
    """
1217
    if ignore_test:
1218
      logging.info("Information for %s will be ignored", name)
1219
      return {}
1220
    if cmd_arg:
1221
      logging.info("Information for %s will be parsed from command line", name)
1222
      results = cmd_function()
1223
    else:
1224
      logging.info("Information for %s will be parsed from %s file",
1225
        name, OVF_EXT)
1226
      results = nocmd_function()
1227
    logging.info("Options for %s were succesfully read", name)
1228
    return results
1229

    
1230
  def _ParseNameOptions(self):
1231
    """Returns name if one was given in command line.
1232

1233
    @rtype: string
1234
    @return: name of an instance
1235

1236
    """
1237
    return self.options.name
1238

    
1239
  def _ParseTemplateOptions(self):
1240
    """Returns disk template if one was given in command line.
1241

1242
    @rtype: string
1243
    @return: disk template name
1244

1245
    """
1246
    return self.options.disk_template
1247

    
1248
  def _ParseHypervisorOptions(self):
1249
    """Parses hypervisor options given in a command line.
1250

1251
    @rtype: dict
1252
    @return: dictionary containing name of the chosen hypervisor and all the
1253
      options
1254

1255
    """
1256
    assert type(self.options.hypervisor) is tuple
1257
    assert len(self.options.hypervisor) == 2
1258
    results = {}
1259
    if self.options.hypervisor[0]:
1260
      results["hypervisor_name"] = self.options.hypervisor[0]
1261
    else:
1262
      results["hypervisor_name"] = constants.VALUE_AUTO
1263
    results.update(self.options.hypervisor[1])
1264
    return results
1265

    
1266
  def _ParseOSOptions(self):
1267
    """Parses OS options given in command line.
1268

1269
    @rtype: dict
1270
    @return: dictionary containing name of chosen OS and all its options
1271

1272
    """
1273
    assert self.options.os
1274
    results = {}
1275
    results["os_name"] = self.options.os
1276
    results.update(self.options.osparams)
1277
    return results
1278

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

1282
    @rtype: dict
1283
    @return: dictionary containing vcpus, memory and auto-balance options
1284

1285
    """
1286
    assert self.options.beparams
1287
    backend = {}
1288
    backend.update(self.options.beparams)
1289
    must_contain = ["vcpus", "memory", "auto_balance"]
1290
    for element in must_contain:
1291
      if backend.get(element) is None:
1292
        backend[element] = constants.VALUE_AUTO
1293
    return backend
1294

    
1295
  def _ParseTags(self):
1296
    """Returns tags list given in command line.
1297

1298
    @rtype: string
1299
    @return: string containing comma-separated tags
1300

1301
    """
1302
    return self.options.tags
1303

    
1304
  def _ParseNicOptions(self):
1305
    """Parses network options given in a command line or as a dictionary.
1306

1307
    @rtype: dict
1308
    @return: dictionary of network-related options
1309

1310
    """
1311
    assert self.options.nics
1312
    results = {}
1313
    for (nic_id, nic_desc) in self.options.nics:
1314
      results["nic%s_mode" % nic_id] = \
1315
        nic_desc.get("mode", constants.VALUE_AUTO)
1316
      results["nic%s_mac" % nic_id] = nic_desc.get("mac", constants.VALUE_AUTO)
1317
      results["nic%s_link" % nic_id] = \
1318
        nic_desc.get("link", constants.VALUE_AUTO)
1319
      results["nic%s_network" % nic_id] = \
1320
        nic_desc.get("network", constants.VALUE_AUTO)
1321
      if nic_desc.get("mode") == "bridged":
1322
        results["nic%s_ip" % nic_id] = constants.VALUE_NONE
1323
      else:
1324
        results["nic%s_ip" % nic_id] = constants.VALUE_AUTO
1325
    results["nic_count"] = str(len(self.options.nics))
1326
    return results
1327

    
1328
  def _ParseDiskOptions(self):
1329
    """Parses disk options given in a command line.
1330

1331
    @rtype: dict
1332
    @return: dictionary of disk-related options
1333

1334
    @raise errors.OpPrereqError: disk description does not contain size
1335
      information or size information is invalid or creation failed
1336

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

    
1370
  def _GetDiskInfo(self):
1371
    """Gathers information about disks used by instance, perfomes conversion.
1372

1373
    @rtype: dict
1374
    @return: dictionary of disk-related options
1375

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

1378
    """
1379
    results = {}
1380
    disks_list = self.ovf_reader.GetDisksNames()
1381
    for (counter, (disk_name, disk_compression)) in enumerate(disks_list):
1382
      if os.path.dirname(disk_name):
1383
        raise errors.OpPrereqError("Disks are not allowed to have absolute"
1384
                                   " paths or paths outside main OVF directory")
1385
      disk, _ = os.path.splitext(disk_name)
1386
      disk_path = utils.PathJoin(self.input_dir, disk_name)
1387
      if disk_compression not in NO_COMPRESSION:
1388
        _, disk_path = self._CompressDisk(disk_path, disk_compression,
1389
          DECOMPRESS)
1390
        disk, _ = os.path.splitext(disk)
1391
      if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw":
1392
        logging.info("Conversion to raw format is required")
1393
      ext, new_disk_path = self._ConvertDisk("raw", disk_path)
1394

    
1395
      final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext,
1396
        directory=self.output_dir)
1397
      final_name = os.path.basename(final_disk_path)
1398
      disk_size = os.path.getsize(final_disk_path) / (1024 * 1024)
1399
      results["disk%s_dump" % counter] = final_name
1400
      results["disk%s_size" % counter] = str(disk_size)
1401
      results["disk%s_ivname" % counter] = "disk/%s" % str(counter)
1402
    if disks_list:
1403
      results["disk_count"] = str(len(disks_list))
1404
    return results
1405

    
1406
  def Save(self):
1407
    """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
1408

1409
    @raise errors.OpPrereqError: when saving to config file failed
1410

1411
    """
1412
    logging.info("Conversion was succesfull, saving %s in %s directory",
1413
                 constants.EXPORT_CONF_FILE, self.output_dir)
1414
    results = {
1415
      constants.INISECT_INS: {},
1416
      constants.INISECT_BEP: {},
1417
      constants.INISECT_EXP: {},
1418
      constants.INISECT_OSP: {},
1419
      constants.INISECT_HYP: {},
1420
    }
1421

    
1422
    results[constants.INISECT_INS].update(self.results_disk)
1423
    results[constants.INISECT_INS].update(self.results_network)
1424
    results[constants.INISECT_INS]["hypervisor"] = \
1425
      self.results_hypervisor["hypervisor_name"]
1426
    results[constants.INISECT_INS]["name"] = self.results_name
1427
    if self.results_template:
1428
      results[constants.INISECT_INS]["disk_template"] = self.results_template
1429
    if self.results_tags:
1430
      results[constants.INISECT_INS]["tags"] = self.results_tags
1431

    
1432
    results[constants.INISECT_BEP].update(self.results_backend)
1433

    
1434
    results[constants.INISECT_EXP]["os"] = self.results_os["os_name"]
1435
    results[constants.INISECT_EXP]["version"] = self.results_version
1436

    
1437
    del self.results_os["os_name"]
1438
    results[constants.INISECT_OSP].update(self.results_os)
1439

    
1440
    del self.results_hypervisor["hypervisor_name"]
1441
    results[constants.INISECT_HYP].update(self.results_hypervisor)
1442

    
1443
    output_file_name = utils.PathJoin(self.output_dir,
1444
      constants.EXPORT_CONF_FILE)
1445

    
1446
    output = []
1447
    for section, options in results.iteritems():
1448
      output.append("[%s]" % section)
1449
      for name, value in options.iteritems():
1450
        if value is None:
1451
          value = ""
1452
        output.append("%s = %s" % (name, value))
1453
      output.append("")
1454
    output_contents = "\n".join(output)
1455

    
1456
    try:
1457
      utils.WriteFile(output_file_name, data=output_contents)
1458
    except errors.ProgrammerError, err:
1459
      raise errors.OpPrereqError("Saving the config file failed: %s" % err)
1460

    
1461
    self.Cleanup()
1462

    
1463

    
1464
class ConfigParserWithDefaults(ConfigParser.SafeConfigParser):
1465
  """This is just a wrapper on SafeConfigParser, that uses default values
1466

1467
  """
1468
  def get(self, section, options, raw=None, vars=None): # pylint: disable=W0622
1469
    try:
1470
      result = ConfigParser.SafeConfigParser.get(self, section, options, \
1471
        raw=raw, vars=vars)
1472
    except ConfigParser.NoOptionError:
1473
      result = None
1474
    return result
1475

    
1476
  def getint(self, section, options):
1477
    try:
1478
      result = ConfigParser.SafeConfigParser.get(self, section, options)
1479
    except ConfigParser.NoOptionError:
1480
      result = 0
1481
    return int(result)
1482

    
1483

    
1484
class OVFExporter(Converter):
1485
  """Converter from Ganeti config file to OVF
1486

1487
  @type input_dir: string
1488
  @ivar input_dir: directory in which the config.ini file resides
1489
  @type output_dir: string
1490
  @ivar output_dir: directory to which the results of conversion shall be
1491
    written
1492
  @type packed_dir: string
1493
  @ivar packed_dir: if we want OVA package, this points to the real (i.e. not
1494
    temp) output directory
1495
  @type input_path: string
1496
  @ivar input_path: complete path to the config.ini file
1497
  @type output_path: string
1498
  @ivar output_path: complete path to .ovf file
1499
  @type config_parser: L{ConfigParserWithDefaults}
1500
  @ivar config_parser: parser for the config.ini file
1501
  @type reference_files: list
1502
  @ivar reference_files: files referenced in the ovf file
1503
  @type results_disk: list
1504
  @ivar results_disk: list of dictionaries of disk options from config.ini
1505
  @type results_network: list
1506
  @ivar results_network: list of dictionaries of network options form config.ini
1507
  @type results_name: string
1508
  @ivar results_name: name of the instance
1509
  @type results_vcpus: string
1510
  @ivar results_vcpus: number of VCPUs
1511
  @type results_memory: string
1512
  @ivar results_memory: RAM memory in MB
1513
  @type results_ganeti: dict
1514
  @ivar results_ganeti: dictionary of Ganeti-specific options from config.ini
1515

1516
  """
1517
  def _ReadInputData(self, input_path):
1518
    """Reads the data on which the conversion will take place.
1519

1520
    @type input_path: string
1521
    @param input_path: absolute path to the config.ini input file
1522

1523
    @raise errors.OpPrereqError: error when reading the config file
1524

1525
    """
1526
    input_dir = os.path.dirname(input_path)
1527
    self.input_path = input_path
1528
    self.input_dir = input_dir
1529
    if self.options.output_dir:
1530
      self.output_dir = os.path.abspath(self.options.output_dir)
1531
    else:
1532
      self.output_dir = input_dir
1533
    self.config_parser = ConfigParserWithDefaults()
1534
    logging.info("Reading configuration from %s file", input_path)
1535
    try:
1536
      self.config_parser.read(input_path)
1537
    except ConfigParser.MissingSectionHeaderError, err:
1538
      raise errors.OpPrereqError("Error when trying to read %s: %s" %
1539
                                 (input_path, err))
1540
    if self.options.ova_package:
1541
      self.temp_dir = tempfile.mkdtemp()
1542
      self.packed_dir = self.output_dir
1543
      self.output_dir = self.temp_dir
1544

    
1545
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1546

    
1547
  def _ParseName(self):
1548
    """Parses name from command line options or config file.
1549

1550
    @rtype: string
1551
    @return: name of Ganeti instance
1552

1553
    @raise errors.OpPrereqError: if name of the instance is not provided
1554

1555
    """
1556
    if self.options.name:
1557
      name = self.options.name
1558
    else:
1559
      name = self.config_parser.get(constants.INISECT_INS, NAME)
1560
    if name is None:
1561
      raise errors.OpPrereqError("No instance name found")
1562
    return name
1563

    
1564
  def _ParseVCPUs(self):
1565
    """Parses vcpus number from config file.
1566

1567
    @rtype: int
1568
    @return: number of virtual CPUs
1569

1570
    @raise errors.OpPrereqError: if number of VCPUs equals 0
1571

1572
    """
1573
    vcpus = self.config_parser.getint(constants.INISECT_BEP, VCPUS)
1574
    if vcpus == 0:
1575
      raise errors.OpPrereqError("No CPU information found")
1576
    return vcpus
1577

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

1581
    @rtype: int
1582
    @return: amount of memory in MB
1583

1584
    @raise errors.OpPrereqError: if amount of memory equals 0
1585

1586
    """
1587
    memory = self.config_parser.getint(constants.INISECT_BEP, MEMORY)
1588
    if memory == 0:
1589
      raise errors.OpPrereqError("No memory information found")
1590
    return memory
1591

    
1592
  def _ParseGaneti(self):
1593
    """Parses Ganeti data from config file.
1594

1595
    @rtype: dictionary
1596
    @return: dictionary of Ganeti-specific options
1597

1598
    """
1599
    results = {}
1600
    # hypervisor
1601
    results["hypervisor"] = {}
1602
    hyp_name = self.config_parser.get(constants.INISECT_INS, HYPERV)
1603
    if hyp_name is None:
1604
      raise errors.OpPrereqError("No hypervisor information found")
1605
    results["hypervisor"]["name"] = hyp_name
1606
    pairs = self.config_parser.items(constants.INISECT_HYP)
1607
    for (name, value) in pairs:
1608
      results["hypervisor"][name] = value
1609
    # os
1610
    results["os"] = {}
1611
    os_name = self.config_parser.get(constants.INISECT_EXP, OS)
1612
    if os_name is None:
1613
      raise errors.OpPrereqError("No operating system information found")
1614
    results["os"]["name"] = os_name
1615
    pairs = self.config_parser.items(constants.INISECT_OSP)
1616
    for (name, value) in pairs:
1617
      results["os"][name] = value
1618
    # other
1619
    others = [
1620
      (constants.INISECT_INS, DISK_TEMPLATE, "disk_template"),
1621
      (constants.INISECT_BEP, AUTO_BALANCE, "auto_balance"),
1622
      (constants.INISECT_INS, TAGS, "tags"),
1623
      (constants.INISECT_EXP, VERSION, "version"),
1624
    ]
1625
    for (section, element, name) in others:
1626
      results[name] = self.config_parser.get(section, element)
1627
    return results
1628

    
1629
  def _ParseNetworks(self):
1630
    """Parses network data from config file.
1631

1632
    @rtype: list
1633
    @return: list of dictionaries of network options
1634

1635
    @raise errors.OpPrereqError: then network mode is not recognized
1636

1637
    """
1638
    results = []
1639
    counter = 0
1640
    while True:
1641
      data_link = \
1642
        self.config_parser.get(constants.INISECT_INS,
1643
                               "nic%s_link" % counter)
1644
      if data_link is None:
1645
        break
1646
      results.append({
1647
        "mode": self.config_parser.get(constants.INISECT_INS,
1648
           "nic%s_mode" % counter),
1649
        "mac": self.config_parser.get(constants.INISECT_INS,
1650
           "nic%s_mac" % counter),
1651
        "ip": self.config_parser.get(constants.INISECT_INS,
1652
                                     "nic%s_ip" % counter),
1653
        "network": self.config_parser.get(constants.INISECT_INS,
1654
                                          "nic%s_network" % counter),
1655
        "link": data_link,
1656
      })
1657
      if results[counter]["mode"] not in constants.NIC_VALID_MODES:
1658
        raise errors.OpPrereqError("Network mode %s not recognized"
1659
                                   % results[counter]["mode"])
1660
      counter += 1
1661
    return results
1662

    
1663
  def _GetDiskOptions(self, disk_file, compression):
1664
    """Convert the disk and gather disk info for .ovf file.
1665

1666
    @type disk_file: string
1667
    @param disk_file: name of the disk (without the full path)
1668
    @type compression: bool
1669
    @param compression: whether the disk should be compressed or not
1670

1671
    @raise errors.OpPrereqError: when disk image does not exist
1672

1673
    """
1674
    disk_path = utils.PathJoin(self.input_dir, disk_file)
1675
    results = {}
1676
    if not os.path.isfile(disk_path):
1677
      raise errors.OpPrereqError("Disk image does not exist: %s" % disk_path)
1678
    if os.path.dirname(disk_file):
1679
      raise errors.OpPrereqError("Path for the disk: %s contains a directory"
1680
                                 " name" % disk_path)
1681
    disk_name, _ = os.path.splitext(disk_file)
1682
    ext, new_disk_path = self._ConvertDisk(self.options.disk_format, disk_path)
1683
    results["format"] = self.options.disk_format
1684
    results["virt-size"] = self._GetDiskQemuInfo(new_disk_path,
1685
      "virtual size: \S+ \((\d+) bytes\)")
1686
    if compression:
1687
      ext2, new_disk_path = self._CompressDisk(new_disk_path, "gzip",
1688
        COMPRESS)
1689
      disk_name, _ = os.path.splitext(disk_name)
1690
      results["compression"] = "gzip"
1691
      ext += ext2
1692
    final_disk_path = LinkFile(new_disk_path, prefix=disk_name, suffix=ext,
1693
      directory=self.output_dir)
1694
    final_disk_name = os.path.basename(final_disk_path)
1695
    results["real-size"] = os.path.getsize(final_disk_path)
1696
    results["path"] = final_disk_name
1697
    self.references_files.append(final_disk_path)
1698
    return results
1699

    
1700
  def _ParseDisks(self):
1701
    """Parses disk data from config file.
1702

1703
    @rtype: list
1704
    @return: list of dictionaries of disk options
1705

1706
    """
1707
    results = []
1708
    counter = 0
1709
    while True:
1710
      disk_file = \
1711
        self.config_parser.get(constants.INISECT_INS, "disk%s_dump" % counter)
1712
      if disk_file is None:
1713
        break
1714
      results.append(self._GetDiskOptions(disk_file, self.options.compression))
1715
      counter += 1
1716
    return results
1717

    
1718
  def Parse(self):
1719
    """Parses the data and creates a structure containing all required info.
1720

1721
    """
1722
    try:
1723
      utils.Makedirs(self.output_dir)
1724
    except OSError, err:
1725
      raise errors.OpPrereqError("Failed to create directory %s: %s" %
1726
                                 (self.output_dir, err))
1727

    
1728
    self.references_files = []
1729
    self.results_name = self._ParseName()
1730
    self.results_vcpus = self._ParseVCPUs()
1731
    self.results_memory = self._ParseMemory()
1732
    if not self.options.ext_usage:
1733
      self.results_ganeti = self._ParseGaneti()
1734
    self.results_network = self._ParseNetworks()
1735
    self.results_disk = self._ParseDisks()
1736

    
1737
  def _PrepareManifest(self, path):
1738
    """Creates manifest for all the files in OVF package.
1739

1740
    @type path: string
1741
    @param path: path to manifesto file
1742

1743
    @raise errors.OpPrereqError: if error occurs when writing file
1744

1745
    """
1746
    logging.info("Preparing manifest for the OVF package")
1747
    lines = []
1748
    files_list = [self.output_path]
1749
    files_list.extend(self.references_files)
1750
    logging.warning("Calculating SHA1 checksums, this may take a while")
1751
    sha1_sums = utils.FingerprintFiles(files_list)
1752
    for file_path, value in sha1_sums.iteritems():
1753
      file_name = os.path.basename(file_path)
1754
      lines.append("SHA1(%s)= %s" % (file_name, value))
1755
    lines.append("")
1756
    data = "\n".join(lines)
1757
    try:
1758
      utils.WriteFile(path, data=data)
1759
    except errors.ProgrammerError, err:
1760
      raise errors.OpPrereqError("Saving the manifest file failed: %s" % err)
1761

    
1762
  @staticmethod
1763
  def _PrepareTarFile(tar_path, files_list):
1764
    """Creates tarfile from the files in OVF package.
1765

1766
    @type tar_path: string
1767
    @param tar_path: path to the resulting file
1768
    @type files_list: list
1769
    @param files_list: list of files in the OVF package
1770

1771
    """
1772
    logging.info("Preparing tarball for the OVF package")
1773
    open(tar_path, mode="w").close()
1774
    ova_package = tarfile.open(name=tar_path, mode="w")
1775
    for file_path in files_list:
1776
      file_name = os.path.basename(file_path)
1777
      ova_package.add(file_path, arcname=file_name)
1778
    ova_package.close()
1779

    
1780
  def Save(self):
1781
    """Saves the gathered configuration in an apropriate format.
1782

1783
    @raise errors.OpPrereqError: if unable to create output directory
1784

1785
    """
1786
    output_file = "%s%s" % (self.results_name, OVF_EXT)
1787
    output_path = utils.PathJoin(self.output_dir, output_file)
1788
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1789
    logging.info("Saving read data to %s", output_path)
1790

    
1791
    self.output_path = utils.PathJoin(self.output_dir, output_file)
1792
    files_list = [self.output_path]
1793

    
1794
    self.ovf_writer.SaveDisksData(self.results_disk)
1795
    self.ovf_writer.SaveNetworksData(self.results_network)
1796
    if not self.options.ext_usage:
1797
      self.ovf_writer.SaveGanetiData(self.results_ganeti, self.results_network)
1798

    
1799
    self.ovf_writer.SaveVirtualSystemData(self.results_name, self.results_vcpus,
1800
      self.results_memory)
1801

    
1802
    data = self.ovf_writer.PrettyXmlDump()
1803
    utils.WriteFile(self.output_path, data=data)
1804

    
1805
    manifest_file = "%s%s" % (self.results_name, MF_EXT)
1806
    manifest_path = utils.PathJoin(self.output_dir, manifest_file)
1807
    self._PrepareManifest(manifest_path)
1808
    files_list.append(manifest_path)
1809

    
1810
    files_list.extend(self.references_files)
1811

    
1812
    if self.options.ova_package:
1813
      ova_file = "%s%s" % (self.results_name, OVA_EXT)
1814
      packed_path = utils.PathJoin(self.packed_dir, ova_file)
1815
      try:
1816
        utils.Makedirs(self.packed_dir)
1817
      except OSError, err:
1818
        raise errors.OpPrereqError("Failed to create directory %s: %s" %
1819
                                   (self.packed_dir, err))
1820
      self._PrepareTarFile(packed_path, files_list)
1821
    logging.info("Creation of the OVF package was successfull")
1822
    self.Cleanup()