Statistics
| Branch: | Tag: | Revision:

root / lib / ovf.py @ a002ed79

History | View | Annotate | Download (62.9 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
def CheckQemuImg():
147
  """ Make sure that qemu-img is present before performing operations.
148

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

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

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

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

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

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

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

    
195

    
196
class OVFReader(object):
197
  """Reader class for OVF files.
198

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

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

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

217
    @type input_path: string
218
    @param input_path: absolute path to the .ovf file
219

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

339
    @raise errors.OpPrereqError: if SHA1 checksums do not match
340

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

    
364
  def GetInstanceName(self):
365
    """Provides information about instance name.
366

367
    @rtype: string
368
    @return: instance name string
369

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

    
374
  def GetDiskTemplate(self):
375
    """Returns disk template from .ovf file
376

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

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

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

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

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

408
    @rtype: dict
409
    @return: dictionary containing name and options for the chosen OS
410

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

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

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

429
    """
430
    results = {}
431

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

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

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

    
467
    return results
468

    
469
  def GetTagsData(self):
470
    """Provides tags information for instance.
471

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

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

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

486
    @rtype: string
487
    @return: string containing the version number
488

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

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

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

503
    @rtype: dict
504
    @return: dictionary containing all the network information
505

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

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

    
538
      network_name = network_name.lower()
539

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

    
547
      # GanetiSection data overrides 'manually' collected data
548
      for name, value in ganeti_data.iteritems():
549
        results["nic%s_%s" % (counter, name)] = value
550

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

    
556
      for option in required:
557
        if not results.get("nic%s_%s" % (counter, option)):
558
          results["nic%s_%s" % (counter, option)] = constants.VALUE_AUTO
559

    
560
    if network_names:
561
      results["nic_count"] = str(len(network_names))
562
    return results
563

    
564
  def GetDisksNames(self):
565
    """Provides list of file names for the disks used by the instance.
566

567
    @rtype: list
568
    @return: list of file names, as referenced in .ovf file
569

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

    
586

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

591
  """
592
  if text is None:
593
    return None
594
  elem = ET.SubElement(parent, tag, attrib=attrib, **extra)
595
  elem.text = str(text)
596
  return elem
597

    
598

    
599
class OVFWriter(object):
600
  """Writer class for OVF files.
601

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

613
  """
614
  def __init__(self, has_gnt_section):
615
    """Initialize the writer - set the top element.
616

617
    @type has_gnt_section: bool
618
    @param has_gnt_section: if the Ganeti schema should be added - i.e. this
619
      means that Ganeti section will be present
620

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

    
640
  def SaveDisksData(self, disks):
641
    """Convert disk information to certain OVF sections.
642

643
    @type disks: list
644
    @param disks: list of dictionaries of disk options from config.ini
645

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

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

    
679
  def SaveNetworksData(self, networks):
680
    """Convert network information to NetworkSection.
681

682
    @type networks: list
683
    @param networks: list of dictionaries of network options form config.ini
684

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

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

    
704
  @staticmethod
705
  def _SaveNameAndParams(root, data):
706
    """Save name and parameters information under root using data.
707

708
    @type root: ET.Element
709
    @param root: root element for the Name and Parameters
710
    @type data: dict
711
    @param data: data from which we gather the values
712

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

    
721
  def SaveGanetiData(self, ganeti, networks):
722
    """Convert Ganeti-specific information to GanetiSection.
723

724
    @type ganeti: dict
725
    @param ganeti: dictionary of Ganeti-specific options from config.ini
726
    @type networks: list
727
    @param networks: list of dictionaries of network options form config.ini
728

729
    """
730
    ganeti_section = ET.SubElement(self.tree, "gnt:GanetiSection")
731

    
732
    SubElementText(ganeti_section, "gnt:Version", ganeti.get("version"))
733
    SubElementText(ganeti_section, "gnt:DiskTemplate",
734
      ganeti.get("disk_template"))
735
    SubElementText(ganeti_section, "gnt:AutoBalance",
736
      ganeti.get("auto_balance"))
737
    SubElementText(ganeti_section, "gnt:Tags", ganeti.get("tags"))
738

    
739
    osys = ET.SubElement(ganeti_section, "gnt:OperatingSystem")
740
    self._SaveNameAndParams(osys, ganeti["os"])
741

    
742
    hypervisor = ET.SubElement(ganeti_section, "gnt:Hypervisor")
743
    self._SaveNameAndParams(hypervisor, ganeti["hypervisor"])
744

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

    
755
  def SaveVirtualSystemData(self, name, vcpus, memory):
756
    """Convert virtual system information to OVF sections.
757

758
    @type name: string
759
    @param name: name of the instance
760
    @type vcpus: int
761
    @param vcpus: number of VCPUs
762
    @type memory: int
763
    @param memory: RAM memory in MB
764

765
    """
766
    assert(vcpus > 0)
767
    assert(memory > 0)
768
    vs_attrib = {"ovf:id": name}
769
    virtual_system = ET.SubElement(self.tree, "VirtualSystem", attrib=vs_attrib)
770
    SubElementText(virtual_system, "Info", "A virtual machine")
771

    
772
    name_section = ET.SubElement(virtual_system, "Name")
773
    name_section.text = name
774
    os_attrib = {"ovf:id": "0"}
775
    os_section = ET.SubElement(virtual_system, "OperatingSystemSection",
776
      attrib=os_attrib)
777
    SubElementText(os_section, "Info", "Installed guest operating system")
778
    hardware_section = ET.SubElement(virtual_system, "VirtualHardwareSection")
779
    SubElementText(hardware_section, "Info", "Virtual hardware requirements")
780

    
781
    # System description
782
    system = ET.SubElement(hardware_section, "System")
783
    SubElementText(system, "vssd:ElementName", "Virtual Hardware Family")
784
    SubElementText(system, "vssd:InstanceID", INSTANCE_ID["system"])
785
    SubElementText(system, "vssd:VirtualSystemIdentifier", name)
786
    SubElementText(system, "vssd:VirtualSystemType", self.virtual_system_type)
787

    
788
    # Item for vcpus
789
    vcpus_item = ET.SubElement(hardware_section, "Item")
790
    SubElementText(vcpus_item, "rasd:ElementName",
791
      "%s virtual CPU(s)" % vcpus)
792
    SubElementText(vcpus_item, "rasd:InstanceID", INSTANCE_ID["vcpus"])
793
    SubElementText(vcpus_item, "rasd:ResourceType", RASD_TYPE["vcpus"])
794
    SubElementText(vcpus_item, "rasd:VirtualQuantity", vcpus)
795

    
796
    # Item for memory
797
    memory_item = ET.SubElement(hardware_section, "Item")
798
    SubElementText(memory_item, "rasd:AllocationUnits", "byte * 2^20")
799
    SubElementText(memory_item, "rasd:ElementName", "%sMB of memory" % memory)
800
    SubElementText(memory_item, "rasd:InstanceID", INSTANCE_ID["memory"])
801
    SubElementText(memory_item, "rasd:ResourceType", RASD_TYPE["memory"])
802
    SubElementText(memory_item, "rasd:VirtualQuantity", memory)
803

    
804
    # Item for scsi controller
805
    scsi_item = ET.SubElement(hardware_section, "Item")
806
    SubElementText(scsi_item, "rasd:Address", INSTANCE_ID["system"])
807
    SubElementText(scsi_item, "rasd:ElementName", "scsi_controller0")
808
    SubElementText(scsi_item, "rasd:InstanceID", INSTANCE_ID["scsi"])
809
    SubElementText(scsi_item, "rasd:ResourceSubType", SCSI_SUBTYPE)
810
    SubElementText(scsi_item, "rasd:ResourceType", RASD_TYPE["scsi-controller"])
811

    
812
    # Other items - from self.hardware_list
813
    for item in self.hardware_list:
814
      hardware_section.append(item)
815

    
816
  def PrettyXmlDump(self):
817
    """Formatter of the XML file.
818

819
    @rtype: string
820
    @return: XML tree in the form of nicely-formatted string
821

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

    
829

    
830
class Converter(object):
831
  """Converter class for OVF packages.
832

833
  Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is
834
  to provide a common interface for the two.
835

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

847
  """
848
  def __init__(self, input_path, options):
849
    """Initialize the converter.
850

851
    @type input_path: string
852
    @param input_path: path to the Converter input file
853
    @type options: optparse.Values
854
    @param options: command line options
855

856
    @raise errors.OpPrereqError: if file does not exist
857

858
    """
859
    input_path = os.path.abspath(input_path)
860
    if not os.path.isfile(input_path):
861
      raise errors.OpPrereqError("File does not exist: %s" % input_path)
862
    self.options = options
863
    self.temp_file_manager = utils.TemporaryFileManager()
864
    self.temp_dir = None
865
    self.output_dir = None
866
    self._ReadInputData(input_path)
867

    
868
  def _ReadInputData(self, input_path):
869
    """Reads the data on which the conversion will take place.
870

871
    @type input_path: string
872
    @param input_path: absolute path to the Converter input file
873

874
    """
875
    raise NotImplementedError()
876

    
877
  def _CompressDisk(self, disk_path, compression, action):
878
    """Performs (de)compression on the disk and returns the new path
879

880
    @type disk_path: string
881
    @param disk_path: path to the disk
882
    @type compression: string
883
    @param compression: compression type
884
    @type action: string
885
    @param action: whether the action is compression or decompression
886
    @rtype: string
887
    @return: new disk path after (de)compression
888

889
    @raise errors.OpPrereqError: disk (de)compression failed or "compression"
890
      is not supported
891

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

    
915
  def _ConvertDisk(self, disk_format, disk_path):
916
    """Performes conversion to specified format.
917

918
    @type disk_format: string
919
    @param disk_format: format to which the disk should be converted
920
    @type disk_path: string
921
    @param disk_path: path to the disk that should be converted
922
    @rtype: string
923
    @return path to the output disk
924

925
    @raise errors.OpPrereqError: convertion of the disk failed
926

927
    """
928
    CheckQemuImg()
929
    disk_file = os.path.basename(disk_path)
930
    (disk_name, disk_extension) = os.path.splitext(disk_file)
931
    if disk_extension != disk_format:
932
      logging.warning("Conversion of disk image to %s format, this may take"
933
                      " a while", disk_format)
934

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

    
952
  @staticmethod
953
  def _GetDiskQemuInfo(disk_path, regexp):
954
    """Figures out some information of the disk using qemu-img.
955

956
    @type disk_path: string
957
    @param disk_path: path to the disk we want to know the format of
958
    @type regexp: string
959
    @param regexp: string that has to be matched, it has to contain one group
960
    @rtype: string
961
    @return: disk format
962

963
    @raise errors.OpPrereqError: format information cannot be retrieved
964

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

    
982
  def Parse(self):
983
    """Parses the data and creates a structure containing all required info.
984

985
    """
986
    raise NotImplementedError()
987

    
988
  def Save(self):
989
    """Saves the gathered configuration in an apropriate format.
990

991
    """
992
    raise NotImplementedError()
993

    
994
  def Cleanup(self):
995
    """Cleans the temporary directory, if one was created.
996

997
    """
998
    self.temp_file_manager.Cleanup()
999
    if self.temp_dir:
1000
      shutil.rmtree(self.temp_dir)
1001
      self.temp_dir = None
1002

    
1003

    
1004
class OVFImporter(Converter):
1005
  """Converter from OVF to Ganeti config file.
1006

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

1041
  """
1042
  def _ReadInputData(self, input_path):
1043
    """Reads the data on which the conversion will take place.
1044

1045
    @type input_path: string
1046
    @param input_path: absolute path to the .ovf or .ova input file
1047

1048
    @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
1049

1050
    """
1051
    (input_dir, input_file) = os.path.split(input_path)
1052
    (_, input_extension) = os.path.splitext(input_file)
1053

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

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

    
1079
    self.ovf_reader = OVFReader(self.input_path)
1080
    self.ovf_reader.VerifyManifest()
1081

    
1082
  def _UnpackOVA(self, input_path):
1083
    """Unpacks the .ova package into temporary directory.
1084

1085
    @type input_path: string
1086
    @param input_path: path to the .ova package file
1087

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

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

    
1130
  def Parse(self):
1131
    """Parses the data and creates a structure containing all required info.
1132

1133
    The method reads the information given either as a command line option or as
1134
    a part of the OVF description.
1135

1136
    @raise errors.OpPrereqError: if some required part of the description of
1137
      virtual instance is missing or unable to create output directory
1138

1139
    """
1140
    self.results_name = self._GetInfo("instance name", self.options.name,
1141
      self._ParseNameOptions, self.ovf_reader.GetInstanceName)
1142
    if not self.results_name:
1143
      raise errors.OpPrereqError("Name of instance not provided")
1144

    
1145
    self.output_dir = utils.PathJoin(self.output_dir, self.results_name)
1146
    try:
1147
      utils.Makedirs(self.output_dir)
1148
    except OSError, err:
1149
      raise errors.OpPrereqError("Failed to create directory %s: %s" %
1150
                                 (self.output_dir, err))
1151

    
1152
    self.results_template = self._GetInfo("disk template",
1153
      self.options.disk_template, self._ParseTemplateOptions,
1154
      self.ovf_reader.GetDiskTemplate)
1155
    if not self.results_template:
1156
      logging.info("Disk template not given")
1157

    
1158
    self.results_hypervisor = self._GetInfo("hypervisor",
1159
      self.options.hypervisor, self._ParseHypervisorOptions,
1160
      self.ovf_reader.GetHypervisorData)
1161
    assert self.results_hypervisor["hypervisor_name"]
1162
    if self.results_hypervisor["hypervisor_name"] == constants.VALUE_AUTO:
1163
      logging.debug("Default hypervisor settings from the cluster will be used")
1164

    
1165
    self.results_os = self._GetInfo("OS", self.options.os,
1166
      self._ParseOSOptions, self.ovf_reader.GetOSData)
1167
    if not self.results_os.get("os_name"):
1168
      raise errors.OpPrereqError("OS name must be provided")
1169

    
1170
    self.results_backend = self._GetInfo("backend", self.options.beparams,
1171
      self._ParseBackendOptions, self.ovf_reader.GetBackendData)
1172
    assert self.results_backend.get("vcpus")
1173
    assert self.results_backend.get("memory")
1174
    assert self.results_backend.get("auto_balance") is not None
1175

    
1176
    self.results_tags = self._GetInfo("tags", self.options.tags,
1177
      self._ParseTags, self.ovf_reader.GetTagsData)
1178

    
1179
    ovf_version = self.ovf_reader.GetVersionData()
1180
    if ovf_version:
1181
      self.results_version = ovf_version
1182
    else:
1183
      self.results_version = constants.EXPORT_VERSION
1184

    
1185
    self.results_network = self._GetInfo("network", self.options.nics,
1186
      self._ParseNicOptions, self.ovf_reader.GetNetworkData,
1187
      ignore_test=self.options.no_nics)
1188

    
1189
    self.results_disk = self._GetInfo("disk", self.options.disks,
1190
      self._ParseDiskOptions, self._GetDiskInfo,
1191
      ignore_test=self.results_template == constants.DT_DISKLESS)
1192

    
1193
    if not self.results_disk and not self.results_network:
1194
      raise errors.OpPrereqError("Either disk specification or network"
1195
                                 " description must be present")
1196

    
1197
  @staticmethod
1198
  def _GetInfo(name, cmd_arg, cmd_function, nocmd_function,
1199
    ignore_test=False):
1200
    """Get information about some section - e.g. disk, network, hypervisor.
1201

1202
    @type name: string
1203
    @param name: name of the section
1204
    @type cmd_arg: dict
1205
    @param cmd_arg: command line argument specific for section 'name'
1206
    @type cmd_function: callable
1207
    @param cmd_function: function to call if 'cmd_args' exists
1208
    @type nocmd_function: callable
1209
    @param nocmd_function: function to call if 'cmd_args' is not there
1210

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

    
1225
  def _ParseNameOptions(self):
1226
    """Returns name if one was given in command line.
1227

1228
    @rtype: string
1229
    @return: name of an instance
1230

1231
    """
1232
    return self.options.name
1233

    
1234
  def _ParseTemplateOptions(self):
1235
    """Returns disk template if one was given in command line.
1236

1237
    @rtype: string
1238
    @return: disk template name
1239

1240
    """
1241
    return self.options.disk_template
1242

    
1243
  def _ParseHypervisorOptions(self):
1244
    """Parses hypervisor options given in a command line.
1245

1246
    @rtype: dict
1247
    @return: dictionary containing name of the chosen hypervisor and all the
1248
      options
1249

1250
    """
1251
    assert type(self.options.hypervisor) is tuple
1252
    assert len(self.options.hypervisor) == 2
1253
    results = {}
1254
    if self.options.hypervisor[0]:
1255
      results["hypervisor_name"] = self.options.hypervisor[0]
1256
    else:
1257
      results["hypervisor_name"] = constants.VALUE_AUTO
1258
    results.update(self.options.hypervisor[1])
1259
    return results
1260

    
1261
  def _ParseOSOptions(self):
1262
    """Parses OS options given in command line.
1263

1264
    @rtype: dict
1265
    @return: dictionary containing name of chosen OS and all its options
1266

1267
    """
1268
    assert self.options.os
1269
    results = {}
1270
    results["os_name"] = self.options.os
1271
    results.update(self.options.osparams)
1272
    return results
1273

    
1274
  def _ParseBackendOptions(self):
1275
    """Parses backend options given in command line.
1276

1277
    @rtype: dict
1278
    @return: dictionary containing vcpus, memory and auto-balance options
1279

1280
    """
1281
    assert self.options.beparams
1282
    backend = {}
1283
    backend.update(self.options.beparams)
1284
    must_contain = ["vcpus", "memory", "auto_balance"]
1285
    for element in must_contain:
1286
      if backend.get(element) is None:
1287
        backend[element] = constants.VALUE_AUTO
1288
    return backend
1289

    
1290
  def _ParseTags(self):
1291
    """Returns tags list given in command line.
1292

1293
    @rtype: string
1294
    @return: string containing comma-separated tags
1295

1296
    """
1297
    return self.options.tags
1298

    
1299
  def _ParseNicOptions(self):
1300
    """Parses network options given in a command line or as a dictionary.
1301

1302
    @rtype: dict
1303
    @return: dictionary of network-related options
1304

1305
    """
1306
    assert self.options.nics
1307
    results = {}
1308
    for (nic_id, nic_desc) in self.options.nics:
1309
      results["nic%s_mode" % nic_id] = \
1310
        nic_desc.get("mode", constants.VALUE_AUTO)
1311
      results["nic%s_mac" % nic_id] = nic_desc.get("mac", constants.VALUE_AUTO)
1312
      results["nic%s_link" % nic_id] = \
1313
        nic_desc.get("link", constants.VALUE_AUTO)
1314
      if nic_desc.get("mode") == "bridged":
1315
        results["nic%s_ip" % nic_id] = constants.VALUE_NONE
1316
      else:
1317
        results["nic%s_ip" % nic_id] = constants.VALUE_AUTO
1318
    results["nic_count"] = str(len(self.options.nics))
1319
    return results
1320

    
1321
  def _ParseDiskOptions(self):
1322
    """Parses disk options given in a command line.
1323

1324
    @rtype: dict
1325
    @return: dictionary of disk-related options
1326

1327
    @raise errors.OpPrereqError: disk description does not contain size
1328
      information or size information is invalid or creation failed
1329

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

    
1363
  def _GetDiskInfo(self):
1364
    """Gathers information about disks used by instance, perfomes conversion.
1365

1366
    @rtype: dict
1367
    @return: dictionary of disk-related options
1368

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

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

    
1388
      final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext,
1389
        directory=self.output_dir)
1390
      final_name = os.path.basename(final_disk_path)
1391
      disk_size = os.path.getsize(final_disk_path) / (1024 * 1024)
1392
      results["disk%s_dump" % counter] = final_name
1393
      results["disk%s_size" % counter] = str(disk_size)
1394
      results["disk%s_ivname" % counter] = "disk/%s" % str(counter)
1395
    if disks_list:
1396
      results["disk_count"] = str(len(disks_list))
1397
    return results
1398

    
1399
  def Save(self):
1400
    """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
1401

1402
    @raise errors.OpPrereqError: when saving to config file failed
1403

1404
    """
1405
    logging.info("Conversion was succesfull, saving %s in %s directory",
1406
                 constants.EXPORT_CONF_FILE, self.output_dir)
1407
    results = {
1408
      constants.INISECT_INS: {},
1409
      constants.INISECT_BEP: {},
1410
      constants.INISECT_EXP: {},
1411
      constants.INISECT_OSP: {},
1412
      constants.INISECT_HYP: {},
1413
    }
1414

    
1415
    results[constants.INISECT_INS].update(self.results_disk)
1416
    results[constants.INISECT_INS].update(self.results_network)
1417
    results[constants.INISECT_INS]["hypervisor"] = \
1418
      self.results_hypervisor["hypervisor_name"]
1419
    results[constants.INISECT_INS]["name"] = self.results_name
1420
    if self.results_template:
1421
      results[constants.INISECT_INS]["disk_template"] = self.results_template
1422
    if self.results_tags:
1423
      results[constants.INISECT_INS]["tags"] = self.results_tags
1424

    
1425
    results[constants.INISECT_BEP].update(self.results_backend)
1426

    
1427
    results[constants.INISECT_EXP]["os"] = self.results_os["os_name"]
1428
    results[constants.INISECT_EXP]["version"] = self.results_version
1429

    
1430
    del self.results_os["os_name"]
1431
    results[constants.INISECT_OSP].update(self.results_os)
1432

    
1433
    del self.results_hypervisor["hypervisor_name"]
1434
    results[constants.INISECT_HYP].update(self.results_hypervisor)
1435

    
1436
    output_file_name = utils.PathJoin(self.output_dir,
1437
      constants.EXPORT_CONF_FILE)
1438

    
1439
    output = []
1440
    for section, options in results.iteritems():
1441
      output.append("[%s]" % section)
1442
      for name, value in options.iteritems():
1443
        if value is None:
1444
          value = ""
1445
        output.append("%s = %s" % (name, value))
1446
      output.append("")
1447
    output_contents = "\n".join(output)
1448

    
1449
    try:
1450
      utils.WriteFile(output_file_name, data=output_contents)
1451
    except errors.ProgrammerError, err:
1452
      raise errors.OpPrereqError("Saving the config file failed: %s" % err)
1453

    
1454
    self.Cleanup()
1455

    
1456

    
1457
class ConfigParserWithDefaults(ConfigParser.SafeConfigParser):
1458
  """This is just a wrapper on SafeConfigParser, that uses default values
1459

1460
  """
1461
  def get(self, section, options, raw=None, vars=None): # pylint: disable=W0622
1462
    try:
1463
      result = ConfigParser.SafeConfigParser.get(self, section, options, \
1464
        raw=raw, vars=vars)
1465
    except ConfigParser.NoOptionError:
1466
      result = None
1467
    return result
1468

    
1469
  def getint(self, section, options):
1470
    try:
1471
      result = ConfigParser.SafeConfigParser.get(self, section, options)
1472
    except ConfigParser.NoOptionError:
1473
      result = 0
1474
    return int(result)
1475

    
1476

    
1477
class OVFExporter(Converter):
1478
  """Converter from Ganeti config file to OVF
1479

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

1509
  """
1510
  def _ReadInputData(self, input_path):
1511
    """Reads the data on which the conversion will take place.
1512

1513
    @type input_path: string
1514
    @param input_path: absolute path to the config.ini input file
1515

1516
    @raise errors.OpPrereqError: error when reading the config file
1517

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

    
1538
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1539

    
1540
  def _ParseName(self):
1541
    """Parses name from command line options or config file.
1542

1543
    @rtype: string
1544
    @return: name of Ganeti instance
1545

1546
    @raise errors.OpPrereqError: if name of the instance is not provided
1547

1548
    """
1549
    if self.options.name:
1550
      name = self.options.name
1551
    else:
1552
      name = self.config_parser.get(constants.INISECT_INS, NAME)
1553
    if name is None:
1554
      raise errors.OpPrereqError("No instance name found")
1555
    return name
1556

    
1557
  def _ParseVCPUs(self):
1558
    """Parses vcpus number from config file.
1559

1560
    @rtype: int
1561
    @return: number of virtual CPUs
1562

1563
    @raise errors.OpPrereqError: if number of VCPUs equals 0
1564

1565
    """
1566
    vcpus = self.config_parser.getint(constants.INISECT_BEP, VCPUS)
1567
    if vcpus == 0:
1568
      raise errors.OpPrereqError("No CPU information found")
1569
    return vcpus
1570

    
1571
  def _ParseMemory(self):
1572
    """Parses vcpus number from config file.
1573

1574
    @rtype: int
1575
    @return: amount of memory in MB
1576

1577
    @raise errors.OpPrereqError: if amount of memory equals 0
1578

1579
    """
1580
    memory = self.config_parser.getint(constants.INISECT_BEP, MEMORY)
1581
    if memory == 0:
1582
      raise errors.OpPrereqError("No memory information found")
1583
    return memory
1584

    
1585
  def _ParseGaneti(self):
1586
    """Parses Ganeti data from config file.
1587

1588
    @rtype: dictionary
1589
    @return: dictionary of Ganeti-specific options
1590

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

    
1622
  def _ParseNetworks(self):
1623
    """Parses network data from config file.
1624

1625
    @rtype: list
1626
    @return: list of dictionaries of network options
1627

1628
    @raise errors.OpPrereqError: then network mode is not recognized
1629

1630
    """
1631
    results = []
1632
    counter = 0
1633
    while True:
1634
      data_link = \
1635
        self.config_parser.get(constants.INISECT_INS, "nic%s_link" % counter)
1636
      if data_link is None:
1637
        break
1638
      results.append({
1639
        "mode": self.config_parser.get(constants.INISECT_INS,
1640
           "nic%s_mode" % counter),
1641
        "mac": self.config_parser.get(constants.INISECT_INS,
1642
           "nic%s_mac" % counter),
1643
        "ip": self.config_parser.get(constants.INISECT_INS,
1644
           "nic%s_ip" % counter),
1645
        "link": data_link,
1646
      })
1647
      if results[counter]["mode"] not in constants.NIC_VALID_MODES:
1648
        raise errors.OpPrereqError("Network mode %s not recognized"
1649
                                   % results[counter]["mode"])
1650
      counter += 1
1651
    return results
1652

    
1653
  def _GetDiskOptions(self, disk_file, compression):
1654
    """Convert the disk and gather disk info for .ovf file.
1655

1656
    @type disk_file: string
1657
    @param disk_file: name of the disk (without the full path)
1658
    @type compression: bool
1659
    @param compression: whether the disk should be compressed or not
1660

1661
    @raise errors.OpPrereqError: when disk image does not exist
1662

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

    
1690
  def _ParseDisks(self):
1691
    """Parses disk data from config file.
1692

1693
    @rtype: list
1694
    @return: list of dictionaries of disk options
1695

1696
    """
1697
    results = []
1698
    counter = 0
1699
    while True:
1700
      disk_file = \
1701
        self.config_parser.get(constants.INISECT_INS, "disk%s_dump" % counter)
1702
      if disk_file is None:
1703
        break
1704
      results.append(self._GetDiskOptions(disk_file, self.options.compression))
1705
      counter += 1
1706
    return results
1707

    
1708
  def Parse(self):
1709
    """Parses the data and creates a structure containing all required info.
1710

1711
    """
1712
    try:
1713
      utils.Makedirs(self.output_dir)
1714
    except OSError, err:
1715
      raise errors.OpPrereqError("Failed to create directory %s: %s" %
1716
                                 (self.output_dir, err))
1717

    
1718
    self.references_files = []
1719
    self.results_name = self._ParseName()
1720
    self.results_vcpus = self._ParseVCPUs()
1721
    self.results_memory = self._ParseMemory()
1722
    if not self.options.ext_usage:
1723
      self.results_ganeti = self._ParseGaneti()
1724
    self.results_network = self._ParseNetworks()
1725
    self.results_disk = self._ParseDisks()
1726

    
1727
  def _PrepareManifest(self, path):
1728
    """Creates manifest for all the files in OVF package.
1729

1730
    @type path: string
1731
    @param path: path to manifesto file
1732

1733
    @raise errors.OpPrereqError: if error occurs when writing file
1734

1735
    """
1736
    logging.info("Preparing manifest for the OVF package")
1737
    lines = []
1738
    files_list = [self.output_path]
1739
    files_list.extend(self.references_files)
1740
    logging.warning("Calculating SHA1 checksums, this may take a while")
1741
    sha1_sums = utils.FingerprintFiles(files_list)
1742
    for file_path, value in sha1_sums.iteritems():
1743
      file_name = os.path.basename(file_path)
1744
      lines.append("SHA1(%s)= %s" % (file_name, value))
1745
    lines.append("")
1746
    data = "\n".join(lines)
1747
    try:
1748
      utils.WriteFile(path, data=data)
1749
    except errors.ProgrammerError, err:
1750
      raise errors.OpPrereqError("Saving the manifest file failed: %s" % err)
1751

    
1752
  @staticmethod
1753
  def _PrepareTarFile(tar_path, files_list):
1754
    """Creates tarfile from the files in OVF package.
1755

1756
    @type tar_path: string
1757
    @param tar_path: path to the resulting file
1758
    @type files_list: list
1759
    @param files_list: list of files in the OVF package
1760

1761
    """
1762
    logging.info("Preparing tarball for the OVF package")
1763
    open(tar_path, mode="w").close()
1764
    ova_package = tarfile.open(name=tar_path, mode="w")
1765
    for file_path in files_list:
1766
      file_name = os.path.basename(file_path)
1767
      ova_package.add(file_path, arcname=file_name)
1768
    ova_package.close()
1769

    
1770
  def Save(self):
1771
    """Saves the gathered configuration in an apropriate format.
1772

1773
    @raise errors.OpPrereqError: if unable to create output directory
1774

1775
    """
1776
    output_file = "%s%s" % (self.results_name, OVF_EXT)
1777
    output_path = utils.PathJoin(self.output_dir, output_file)
1778
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1779
    logging.info("Saving read data to %s", output_path)
1780

    
1781
    self.output_path = utils.PathJoin(self.output_dir, output_file)
1782
    files_list = [self.output_path]
1783

    
1784
    self.ovf_writer.SaveDisksData(self.results_disk)
1785
    self.ovf_writer.SaveNetworksData(self.results_network)
1786
    if not self.options.ext_usage:
1787
      self.ovf_writer.SaveGanetiData(self.results_ganeti, self.results_network)
1788

    
1789
    self.ovf_writer.SaveVirtualSystemData(self.results_name, self.results_vcpus,
1790
      self.results_memory)
1791

    
1792
    data = self.ovf_writer.PrettyXmlDump()
1793
    utils.WriteFile(self.output_path, data=data)
1794

    
1795
    manifest_file = "%s%s" % (self.results_name, MF_EXT)
1796
    manifest_path = utils.PathJoin(self.output_dir, manifest_file)
1797
    self._PrepareManifest(manifest_path)
1798
    files_list.append(manifest_path)
1799

    
1800
    files_list.extend(self.references_files)
1801

    
1802
    if self.options.ova_package:
1803
      ova_file = "%s%s" % (self.results_name, OVA_EXT)
1804
      packed_path = utils.PathJoin(self.packed_dir, ova_file)
1805
      try:
1806
        utils.Makedirs(self.packed_dir)
1807
      except OSError, err:
1808
        raise errors.OpPrereqError("Failed to create directory %s: %s" %
1809
                                   (self.packed_dir, err))
1810
      self._PrepareTarFile(packed_path, files_list)
1811
    logging.info("Creation of the OVF package was successfull")
1812
    self.Cleanup()