Statistics
| Branch: | Tag: | Revision:

root / lib / ovf.py @ 909b3a0e

History | View | Annotate | Download (61 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
from ganeti import constants
49
from ganeti import errors
50
from ganeti import utils
51

    
52

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

    
62
# File extensions in OVF package
63
OVA_EXT = ".ova"
64
OVF_EXT = ".ovf"
65
MF_EXT = ".mf"
66
CERT_EXT = ".cert"
67
COMPRESSION_EXT = ".gz"
68
FILE_EXTENSIONS = [
69
  OVF_EXT,
70
  MF_EXT,
71
  CERT_EXT,
72
]
73

    
74
COMPRESSION_TYPE = "gzip"
75
COMPRESS = "compression"
76
DECOMPRESS = "decompression"
77
ALLOWED_ACTIONS = [COMPRESS, DECOMPRESS]
78

    
79
# ResourceType values
80
RASD_TYPE = {
81
  "vcpus": "3",
82
  "memory": "4",
83
  "scsi-controller": "6",
84
  "ethernet-adapter": "10",
85
  "disk": "17",
86
}
87

    
88
# AllocationUnits values and conversion
89
ALLOCATION_UNITS = {
90
  'b': ["bytes", "b"],
91
  'kb': ["kilobytes", "kb", "byte * 2^10", "kibibytes", "kib"],
92
  'mb': ["megabytes", "mb", "byte * 2^20", "mebibytes", "mib"],
93
  'gb': ["gigabytes", "gb", "byte * 2^30", "gibibytes", "gib"],
94
}
95
CONVERT_UNITS_TO_MB = {
96
  'b': lambda x: x / (1024 * 1024),
97
  'kb': lambda x: x / 1024,
98
  'mb': lambda x: x,
99
  'gb': lambda x: x * 1024,
100
}
101

    
102
# Names of the config fields
103
NAME = "name"
104
OS = "os"
105
HYPERV = "hypervisor"
106
VCPUS = "vcpus"
107
MEMORY = "memory"
108
AUTO_BALANCE = "auto_balance"
109
DISK_TEMPLATE = "disk_template"
110
TAGS = "tags"
111
VERSION = "version"
112

    
113
# Instance IDs of System and SCSI controller
114
SYSTEM_ID = 0
115
SCSI_ID = 3
116

    
117
# Disk format descriptions
118
DISK_FORMAT = {
119
  "raw": "http://en.wikipedia.org/wiki/Byte",
120
  "vmdk": "http://www.vmware.com/interfaces/specifications/vmdk.html"
121
          "#monolithicSparse",
122
  "cow": "http://www.gnome.org/~markmc/qcow-image-format.html",
123
}
124

    
125

    
126
def LinkFile(old_path, prefix=None, suffix=None, directory=None):
127
  """Create link with a given prefix and suffix.
128

129
  This is a wrapper over os.link. It tries to create a hard link for given file,
130
  but instead of rising error when file exists, the function changes the name
131
  a little bit.
132

133
  @type old_path:string
134
  @param old_path: path to the file that is to be linked
135
  @type prefix: string
136
  @param prefix: prefix of filename for the link
137
  @type suffix: string
138
  @param suffix: suffix of the filename for the link
139
  @type directory: string
140
  @param directory: directory of the link
141

142
  @raise errors.OpPrereqError: when error on linking is different than
143
    "File exists"
144

145
  """
146
  assert(prefix is not None or suffix is not None)
147
  if directory is None:
148
    directory = os.getcwd()
149
  new_path = utils.PathJoin(directory, "%s%s" % (prefix, suffix))
150
  counter = 1
151
  while True:
152
    try:
153
      os.link(old_path, new_path)
154
      break
155
    except OSError, err:
156
      if err.errno == errno.EEXIST:
157
        new_path = utils.PathJoin(directory,
158
          "%s_%s%s" % (prefix, counter, suffix))
159
        counter += 1
160
      else:
161
        raise errors.OpPrereqError("Error moving the file %s to %s location:"
162
                                   " %s" % (old_path, new_path, err))
163
  return new_path
164

    
165

    
166
class OVFReader(object):
167
  """Reader class for OVF files.
168

169
  @type files_list: list
170
  @ivar files_list: list of files in the OVF package
171
  @type tree: ET.ElementTree
172
  @ivar tree: XML tree of the .ovf file
173
  @type schema_name: string
174
  @ivar schema_name: name of the .ovf file
175
  @type input_dir: string
176
  @ivar input_dir: directory in which the .ovf file resides
177

178
  """
179
  def __init__(self, input_path):
180
    """Initialiaze the reader - load the .ovf file to XML parser.
181

182
    It is assumed that names of manifesto (.mf), certificate (.cert) and ovf
183
    files are the same. In order to account any other files as part of the ovf
184
    package, they have to be explicitly mentioned in the Resources section
185
    of the .ovf file.
186

187
    @type input_path: string
188
    @param input_path: absolute path to the .ovf file
189

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

193
    """
194
    self.tree = ET.ElementTree()
195
    try:
196
      self.tree.parse(input_path)
197
    except xml.parsers.expat.ExpatError, err:
198
      raise errors.OpPrereqError("Error while reading %s file: %s" %
199
                                 (OVF_EXT, err))
200

    
201
    # Create a list of all files in the OVF package
202
    (input_dir, input_file) = os.path.split(input_path)
203
    (input_name, _) = os.path.splitext(input_file)
204
    files_directory = utils.ListVisibleFiles(input_dir)
205
    files_list = []
206
    for file_name in files_directory:
207
      (name, extension) = os.path.splitext(file_name)
208
      if extension in FILE_EXTENSIONS and name == input_name:
209
        files_list.append(file_name)
210
    files_list += self._GetAttributes("{%s}References/{%s}File" %
211
                                      (OVF_SCHEMA, OVF_SCHEMA),
212
                                      "{%s}href" % OVF_SCHEMA)
213
    for file_name in files_list:
214
      file_path = utils.PathJoin(input_dir, file_name)
215
      if not os.path.exists(file_path):
216
        raise errors.OpPrereqError("File does not exist: %s" % file_path)
217
    logging.info("Files in the OVF package: %s", " ".join(files_list))
218
    self.files_list = files_list
219
    self.input_dir = input_dir
220
    self.schema_name = input_name
221

    
222
  def _GetAttributes(self, path, attribute):
223
    """Get specified attribute from all nodes accessible using given path.
224

225
    Function follows the path from root node to the desired tags using path,
226
    then reads the apropriate attribute values.
227

228
    @type path: string
229
    @param path: path of nodes to visit
230
    @type attribute: string
231
    @param attribute: attribute for which we gather the information
232
    @rtype: list
233
    @return: for each accessible tag with the attribute value set, value of the
234
      attribute
235

236
    """
237
    current_list = self.tree.findall(path)
238
    results = [x.get(attribute) for x in current_list]
239
    return filter(None, results)
240

    
241
  def _GetElementMatchingAttr(self, path, match_attr):
242
    """Searches for element on a path that matches certain attribute value.
243

244
    Function follows the path from root node to the desired tags using path,
245
    then searches for the first one matching the attribute value.
246

247
    @type path: string
248
    @param path: path of nodes to visit
249
    @type match_attr: tuple
250
    @param match_attr: pair (attribute, value) for which we search
251
    @rtype: ET.ElementTree or None
252
    @return: first element matching match_attr or None if nothing matches
253

254
    """
255
    potential_elements = self.tree.findall(path)
256
    (attr, val) = match_attr
257
    for elem in potential_elements:
258
      if elem.get(attr) == val:
259
        return elem
260
    return None
261

    
262
  def _GetElementMatchingText(self, path, match_text):
263
    """Searches for element on a path that matches certain text value.
264

265
    Function follows the path from root node to the desired tags using path,
266
    then searches for the first one matching the text value.
267

268
    @type path: string
269
    @param path: path of nodes to visit
270
    @type match_text: tuple
271
    @param match_text: pair (node, text) for which we search
272
    @rtype: ET.ElementTree or None
273
    @return: first element matching match_text or None if nothing matches
274

275
    """
276
    potential_elements = self.tree.findall(path)
277
    (node, text) = match_text
278
    for elem in potential_elements:
279
      if elem.findtext(node) == text:
280
        return elem
281
    return None
282

    
283
  @staticmethod
284
  def _GetDictParameters(root, schema):
285
    """Reads text in all children and creates the dictionary from the contents.
286

287
    @type root: ET.ElementTree or None
288
    @param root: father of the nodes we want to collect data about
289
    @type schema: string
290
    @param schema: schema name to be removed from the tag
291
    @rtype: dict
292
    @return: dictionary containing tags and their text contents, tags have their
293
      schema fragment removed or empty dictionary, when root is None
294

295
    """
296
    if not root:
297
      return {}
298
    results = {}
299
    for element in list(root):
300
      pref_len = len("{%s}" % schema)
301
      assert(schema in element.tag)
302
      tag = element.tag[pref_len:]
303
      results[tag] = element.text
304
    return results
305

    
306
  def VerifyManifest(self):
307
    """Verifies manifest for the OVF package, if one is given.
308

309
    @raise errors.OpPrereqError: if SHA1 checksums do not match
310

311
    """
312
    if "%s%s" % (self.schema_name, MF_EXT) in self.files_list:
313
      logging.warning("Verifying SHA1 checksums, this may take a while")
314
      manifest_filename = "%s%s" % (self.schema_name, MF_EXT)
315
      manifest_path = utils.PathJoin(self.input_dir, manifest_filename)
316
      manifest_content = utils.ReadFile(manifest_path).splitlines()
317
      manifest_files = {}
318
      regexp = r"SHA1\((\S+)\)= (\S+)"
319
      for line in manifest_content:
320
        match = re.match(regexp, line)
321
        if match:
322
          file_name = match.group(1)
323
          sha1_sum = match.group(2)
324
          manifest_files[file_name] = sha1_sum
325
      files_with_paths = [utils.PathJoin(self.input_dir, file_name)
326
        for file_name in self.files_list]
327
      sha1_sums = utils.FingerprintFiles(files_with_paths)
328
      for file_name, value in manifest_files.iteritems():
329
        if sha1_sums.get(utils.PathJoin(self.input_dir, file_name)) != value:
330
          raise errors.OpPrereqError("SHA1 checksum of %s does not match the"
331
                                     " value in manifest file" % file_name)
332
      logging.info("SHA1 checksums verified")
333

    
334
  def GetInstanceName(self):
335
    """Provides information about instance name.
336

337
    @rtype: string
338
    @return: instance name string
339

340
    """
341
    find_name = "{%s}VirtualSystem/{%s}Name" % (OVF_SCHEMA, OVF_SCHEMA)
342
    return self.tree.findtext(find_name)
343

    
344
  def GetDiskTemplate(self):
345
    """Returns disk template from .ovf file
346

347
    @rtype: string or None
348
    @return: name of the template
349
    """
350
    find_template = ("{%s}GanetiSection/{%s}DiskTemplate" %
351
                     (GANETI_SCHEMA, GANETI_SCHEMA))
352
    return self.tree.findtext(find_template)
353

    
354
  def GetHypervisorData(self):
355
    """Provides hypervisor information - hypervisor name and options.
356

357
    @rtype: dict
358
    @return: dictionary containing name of the used hypervisor and all the
359
      specified options
360

361
    """
362
    hypervisor_search = ("{%s}GanetiSection/{%s}Hypervisor" %
363
                         (GANETI_SCHEMA, GANETI_SCHEMA))
364
    hypervisor_data = self.tree.find(hypervisor_search)
365
    if not hypervisor_data:
366
      return {"hypervisor_name": constants.VALUE_AUTO}
367
    results = {
368
      "hypervisor_name": hypervisor_data.findtext("{%s}Name" % GANETI_SCHEMA,
369
                           default=constants.VALUE_AUTO),
370
    }
371
    parameters = hypervisor_data.find("{%s}Parameters" % GANETI_SCHEMA)
372
    results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
373
    return results
374

    
375
  def GetOSData(self):
376
    """ Provides operating system information - os name and options.
377

378
    @rtype: dict
379
    @return: dictionary containing name and options for the chosen OS
380

381
    """
382
    results = {}
383
    os_search = ("{%s}GanetiSection/{%s}OperatingSystem" %
384
                 (GANETI_SCHEMA, GANETI_SCHEMA))
385
    os_data = self.tree.find(os_search)
386
    if os_data:
387
      results["os_name"] = os_data.findtext("{%s}Name" % GANETI_SCHEMA)
388
      parameters = os_data.find("{%s}Parameters" % GANETI_SCHEMA)
389
      results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
390
    return results
391

    
392
  def GetBackendData(self):
393
    """ Provides backend information - vcpus, memory, auto balancing options.
394

395
    @rtype: dict
396
    @return: dictionary containing options for vcpus, memory and auto balance
397
      settings
398

399
    """
400
    results = {}
401

    
402
    find_vcpus = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item" %
403
                   (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
404
    match_vcpus = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["vcpus"])
405
    vcpus = self._GetElementMatchingText(find_vcpus, match_vcpus)
406
    if vcpus:
407
      vcpus_count = vcpus.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
408
        default=constants.VALUE_AUTO)
409
    else:
410
      vcpus_count = constants.VALUE_AUTO
411
    results["vcpus"] = str(vcpus_count)
412

    
413
    find_memory = find_vcpus
414
    match_memory = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["memory"])
415
    memory = self._GetElementMatchingText(find_memory, match_memory)
416
    memory_raw = None
417
    if memory:
418
      alloc_units = memory.findtext("{%s}AllocationUnits" % RASD_SCHEMA)
419
      matching_units = [units for units, variants in
420
        ALLOCATION_UNITS.iteritems() if alloc_units.lower() in variants]
421
      if matching_units == []:
422
        raise errors.OpPrereqError("Unit %s for RAM memory unknown",
423
          alloc_units)
424
      units = matching_units[0]
425
      memory_raw = int(memory.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
426
            default=constants.VALUE_AUTO))
427
      memory_count = CONVERT_UNITS_TO_MB[units](memory_raw)
428
    else:
429
      memory_count = constants.VALUE_AUTO
430
    results["memory"] = str(memory_count)
431

    
432
    find_balance = ("{%s}GanetiSection/{%s}AutoBalance" %
433
                   (GANETI_SCHEMA, GANETI_SCHEMA))
434
    balance = self.tree.findtext(find_balance, default=constants.VALUE_AUTO)
435
    results["auto_balance"] = balance
436

    
437
    return results
438

    
439
  def GetTagsData(self):
440
    """Provides tags information for instance.
441

442
    @rtype: string or None
443
    @return: string of comma-separated tags for the instance
444

445
    """
446
    find_tags = "{%s}GanetiSection/{%s}Tags" % (GANETI_SCHEMA, GANETI_SCHEMA)
447
    results = self.tree.findtext(find_tags)
448
    if results:
449
      return results
450
    else:
451
      return None
452

    
453
  def GetVersionData(self):
454
    """Provides version number read from .ovf file
455

456
    @rtype: string
457
    @return: string containing the version number
458

459
    """
460
    find_version = ("{%s}GanetiSection/{%s}Version" %
461
                    (GANETI_SCHEMA, GANETI_SCHEMA))
462
    return self.tree.findtext(find_version)
463

    
464
  def GetNetworkData(self):
465
    """Provides data about the network in the OVF instance.
466

467
    The method gathers the data about networks used by OVF instance. It assumes
468
    that 'name' tag means something - in essence, if it contains one of the
469
    words 'bridged' or 'routed' then that will be the mode of this network in
470
    Ganeti. The information about the network can be either in GanetiSection or
471
    VirtualHardwareSection.
472

473
    @rtype: dict
474
    @return: dictionary containing all the network information
475

476
    """
477
    results = {}
478
    networks_search = ("{%s}NetworkSection/{%s}Network" %
479
                       (OVF_SCHEMA, OVF_SCHEMA))
480
    network_names = self._GetAttributes(networks_search,
481
      "{%s}name" % OVF_SCHEMA)
482
    required = ["ip", "mac", "link", "mode"]
483
    for (counter, network_name) in enumerate(network_names):
484
      network_search = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item"
485
                        % (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
486
      ganeti_search = ("{%s}GanetiSection/{%s}Network/{%s}Nic" %
487
                       (GANETI_SCHEMA, GANETI_SCHEMA, GANETI_SCHEMA))
488
      network_match = ("{%s}Connection" % RASD_SCHEMA, network_name)
489
      ganeti_match = ("{%s}name" % OVF_SCHEMA, network_name)
490
      network_data = self._GetElementMatchingText(network_search, network_match)
491
      network_ganeti_data = self._GetElementMatchingAttr(ganeti_search,
492
        ganeti_match)
493

    
494
      ganeti_data = {}
495
      if network_ganeti_data:
496
        ganeti_data["mode"] = network_ganeti_data.findtext("{%s}Mode" %
497
                                                           GANETI_SCHEMA)
498
        ganeti_data["mac"] = network_ganeti_data.findtext("{%s}MACAddress" %
499
                                                          GANETI_SCHEMA)
500
        ganeti_data["ip"] = network_ganeti_data.findtext("{%s}IPAddress" %
501
                                                         GANETI_SCHEMA)
502
        ganeti_data["link"] = network_ganeti_data.findtext("{%s}Link" %
503
                                                           GANETI_SCHEMA)
504
      mac_data = None
505
      if network_data:
506
        mac_data = network_data.findtext("{%s}Address" % RASD_SCHEMA)
507

    
508
      network_name = network_name.lower()
509

    
510
      # First, some not Ganeti-specific information is collected
511
      if constants.NIC_MODE_BRIDGED in network_name:
512
        results["nic%s_mode" % counter] = "bridged"
513
      elif constants.NIC_MODE_ROUTED in network_name:
514
        results["nic%s_mode" % counter] = "routed"
515
      results["nic%s_mac" % counter] = mac_data
516

    
517
      # GanetiSection data overrides 'manually' collected data
518
      for name, value in ganeti_data.iteritems():
519
        results["nic%s_%s" % (counter, name)] = value
520

    
521
      # Bridged network has no IP - unless specifically stated otherwise
522
      if (results.get("nic%s_mode" % counter) == "bridged" and
523
          not results.get("nic%s_ip" % counter)):
524
        results["nic%s_ip" % counter] = constants.VALUE_NONE
525

    
526
      for option in required:
527
        if not results.get("nic%s_%s" % (counter, option)):
528
          results["nic%s_%s" % (counter, option)] = constants.VALUE_AUTO
529

    
530
    if network_names:
531
      results["nic_count"] = str(len(network_names))
532
    return results
533

    
534
  def GetDisksNames(self):
535
    """Provides list of file names for the disks used by the instance.
536

537
    @rtype: list
538
    @return: list of file names, as referenced in .ovf file
539

540
    """
541
    results = []
542
    disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA)
543
    disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA)
544
    for disk in disk_ids:
545
      disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA)
546
      disk_match = ("{%s}id" % OVF_SCHEMA, disk)
547
      disk_elem = self._GetElementMatchingAttr(disk_search, disk_match)
548
      if disk_elem is None:
549
        raise errors.OpPrereqError("%s file corrupted - disk %s not found in"
550
                                   " references" % (OVF_EXT, disk))
551
      disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA)
552
      disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA)
553
      results.append((disk_name, disk_compression))
554
    return results
555

    
556

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

561
  """
562
  if text is None:
563
    return None
564
  elem = ET.SubElement(parent, tag, attrib=attrib, **extra)
565
  elem.text = str(text)
566
  return elem
567

    
568

    
569
class OVFWriter(object):
570
  """Writer class for OVF files.
571

572
  @type tree: ET.ElementTree
573
  @ivar tree: XML tree that we are constructing
574
  @type hardware_list: list
575
  @ivar hardware_list: list of items prepared for VirtualHardwareSection
576

577
  """
578
  def __init__(self, has_gnt_section):
579
    """Initialize the writer - set the top element.
580

581
    @type has_gnt_section: bool
582
    @param has_gnt_section: if the Ganeti schema should be added - i.e. this
583
      means that Ganeti section will be present
584

585
    """
586
    env_attribs = {
587
      "xmlns:xsi": XML_SCHEMA,
588
      "xmlns:vssd": VSSD_SCHEMA,
589
      "xmlns:rasd": RASD_SCHEMA,
590
      "xmlns:ovf": OVF_SCHEMA,
591
      "xmlns": OVF_SCHEMA,
592
      "xml:lang": "en-US",
593
    }
594
    if has_gnt_section:
595
      env_attribs["xmlns:gnt"] = GANETI_SCHEMA
596
    self.tree = ET.Element("Envelope", attrib=env_attribs)
597
    self.hardware_list = []
598

    
599
  def SaveDisksData(self, disks):
600
    """Convert disk information to certain OVF sections.
601

602
    @type disks: list
603
    @param disks: list of dictionaries of disk options from config.ini
604

605
    """
606
    references = ET.SubElement(self.tree, "References")
607
    disk_section = ET.SubElement(self.tree, "DiskSection")
608
    for counter, disk in enumerate(disks):
609
      file_id = "file%s" % counter
610
      disk_id = "disk%s" % counter
611
      file_attribs = {
612
        "ovf:href": disk["path"],
613
        "ovf:size": str(disk["real-size"]),
614
        "ovf:id": file_id,
615
      }
616
      disk_attribs = {
617
        "ovf:capacity": str(disk["virt-size"]),
618
        "ovf:diskId": disk_id,
619
        "ovf:fileRef": file_id,
620
        "ovf:format": DISK_FORMAT.get(disk["format"], disk["format"]),
621
      }
622
      if "compression" in disk:
623
        file_attribs["ovf:compression"] = disk["compression"]
624
      ET.SubElement(references, "File", attrib=file_attribs)
625
      ET.SubElement(disk_section, "Disk", attrib=disk_attribs)
626

    
627
      # Item in VirtualHardwareSection creation
628
      disk_item = ET.Element("Item")
629
      SubElementText(disk_item, "rasd:ElementName", disk_id)
630
      SubElementText(disk_item, "rasd:ResourceType", RASD_TYPE["disk"])
631
      SubElementText(disk_item, "rasd:HostResource", "ovf:/disk/%s" % disk_id)
632
      SubElementText(disk_item, "rasd:Parent", SCSI_ID)
633
      self.hardware_list.append(disk_item)
634

    
635
  def SaveNetworksData(self, networks):
636
    """Convert network information to NetworkSection.
637

638
    @type networks: list
639
    @param networks: list of dictionaries of network options form config.ini
640

641
    """
642
    network_section = ET.SubElement(self.tree, "NetworkSection")
643
    for counter, network in enumerate(networks):
644
      network_name = "%s%s" % (network["mode"], counter)
645
      network_attrib = {"ovf:name": network_name}
646
      ET.SubElement(network_section, "Network", attrib=network_attrib)
647

    
648
      # Item in VirtualHardwareSection creation
649
      network_item = ET.Element("Item")
650
      SubElementText(network_item, "rasd:ElementName", network_name)
651
      SubElementText(network_item, "rasd:ResourceType",
652
        RASD_TYPE["ethernet-adapter"])
653
      SubElementText(network_item, "rasd:Connection", network_name)
654
      SubElementText(network_item, "rasd:Address", network["mac"])
655
      self.hardware_list.append(network_item)
656

    
657
  @staticmethod
658
  def _SaveNameAndParams(root, data):
659
    """Save name and parameters information under root using data.
660

661
    @type root: ET.Element
662
    @param root: root element for the Name and Parameters
663
    @type data: dict
664
    @param data: data from which we gather the values
665

666
    """
667
    assert(data.get("name"))
668
    name = SubElementText(root, "gnt:Name", data["name"])
669
    params = ET.SubElement(root, "gnt:Parameters")
670
    for name, value in data.iteritems():
671
      if name != "name":
672
        SubElementText(params, "gnt:%s" % name, value)
673

    
674
  def SaveGanetiData(self, ganeti, networks):
675
    """Convert Ganeti-specific information to GanetiSection.
676

677
    @type ganeti: dict
678
    @param ganeti: dictionary of Ganeti-specific options from config.ini
679
    @type networks: list
680
    @param networks: list of dictionaries of network options form config.ini
681

682
    """
683
    ganeti_section = ET.SubElement(self.tree, "gnt:GanetiSection")
684

    
685
    SubElementText(ganeti_section, "gnt:Version", ganeti.get("version"))
686
    SubElementText(ganeti_section, "gnt:DiskTemplate",
687
      ganeti.get("disk_template"))
688
    SubElementText(ganeti_section, "gnt:AutoBalance",
689
      ganeti.get("auto_balance"))
690
    SubElementText(ganeti_section, "gnt:Tags", ganeti.get("tags"))
691

    
692
    osys = ET.SubElement(ganeti_section, "gnt:OperatingSystem")
693
    self._SaveNameAndParams(osys, ganeti["os"])
694

    
695
    hypervisor = ET.SubElement(ganeti_section, "gnt:Hypervisor")
696
    self._SaveNameAndParams(hypervisor, ganeti["hypervisor"])
697

    
698
    network_section = ET.SubElement(ganeti_section, "gnt:Network")
699
    for counter, network in enumerate(networks):
700
      network_name = "%s%s" % (network["mode"], counter)
701
      nic_attrib = {"ovf:name": network_name}
702
      nic = ET.SubElement(network_section, "gnt:Nic", attrib=nic_attrib)
703
      SubElementText(nic, "gnt:Mode", network["mode"])
704
      SubElementText(nic, "gnt:MACAddress", network["mac"])
705
      SubElementText(nic, "gnt:IPAddress", network["ip"])
706
      SubElementText(nic, "gnt:Link", network["link"])
707

    
708
  def SaveVirtualSystemData(self, name, vcpus, memory):
709
    """Convert virtual system information to OVF sections.
710

711
    @type name: string
712
    @param name: name of the instance
713
    @type vcpus: int
714
    @param vcpus: number of VCPUs
715
    @type memory: int
716
    @param memory: RAM memory in MB
717

718
    """
719
    assert(vcpus > 0)
720
    assert(memory > 0)
721
    vs_attrib = {"ovf:id": name}
722
    virtual_system = ET.SubElement(self.tree, "VirtualSystem", attrib=vs_attrib)
723

    
724
    name_section = ET.SubElement(virtual_system, "Name")
725
    name_section.text = name
726
    os_attrib = {"ovf:id": "0"}
727
    ET.SubElement(virtual_system, "OperatingSystemSection",
728
      attrib=os_attrib)
729
    hardware_section = ET.SubElement(virtual_system, "VirtualHardwareSection")
730

    
731
    # System description
732
    system = ET.SubElement(hardware_section, "System")
733
    SubElementText(system, "vssd:ElementName", "Virtual Hardware Family")
734
    SubElementText(system, "vssd:InstanceId", SYSTEM_ID)
735
    SubElementText(system, "vssd:VirtualSystemIdentifier", name)
736
    SubElementText(system, "vssd:VirtualSystemType", "ganeti-ovf")
737

    
738
    # Item for vcpus
739
    vcpus_item = ET.SubElement(hardware_section, "Item")
740
    SubElementText(vcpus_item, "rasd:ElementName",
741
      "%s virtual CPU(s)" % vcpus)
742
    SubElementText(vcpus_item, "rasd:InstanceID", "1")
743
    SubElementText(vcpus_item, "rasd:ResourceType", RASD_TYPE["vcpus"])
744
    SubElementText(vcpus_item, "rasd:VirtualQuantity", vcpus)
745

    
746
    # Item for memory
747
    memory_item = ET.SubElement(hardware_section, "Item")
748
    SubElementText(memory_item, "rasd:AllocationUnits", "byte * 2^20")
749
    SubElementText(memory_item, "rasd:ElementName", "%sMB of memory" % memory)
750
    SubElementText(memory_item, "rasd:InstanceID", "2")
751
    SubElementText(memory_item, "rasd:ResourceType", RASD_TYPE["memory"])
752
    SubElementText(memory_item, "rasd:VirtualQuantity", memory)
753

    
754
    # Item for scsi controller
755
    scsi_item = ET.SubElement(hardware_section, "Item")
756
    SubElementText(scsi_item, "rasd:Address", SYSTEM_ID)
757
    SubElementText(scsi_item, "rasd:ElementName", "scsi_controller0")
758
    SubElementText(scsi_item, "rasd:ResourceType", RASD_TYPE["scsi-controller"])
759
    SubElementText(scsi_item, "rasd:InstanceId", "3")
760

    
761
    # Other items - from self.hardware_list
762
    for counter, item in enumerate(self.hardware_list):
763
      SubElementText(item, "rasd:InstanceID", counter + 4)
764
      hardware_section.append(item)
765

    
766
  def PrettyXmlDump(self):
767
    """Formatter of the XML file.
768

769
    @rtype: string
770
    @return: XML tree in the form of nicely-formatted string
771

772
    """
773
    raw_string = ET.tostring(self.tree)
774
    parsed_xml = xml.dom.minidom.parseString(raw_string)
775
    xml_string = parsed_xml.toprettyxml(indent="  ")
776
    text_re = re.compile(">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL)
777
    return text_re.sub(">\g<1></", xml_string)
778

    
779

    
780
class Converter(object):
781
  """Converter class for OVF packages.
782

783
  Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is
784
  to provide a common interface for the two.
785

786
  @type options: optparse.Values
787
  @ivar options: options parsed from the command line
788
  @type output_dir: string
789
  @ivar output_dir: directory to which the results of conversion shall be
790
    written
791
  @type temp_file_manager: L{utils.TemporaryFileManager}
792
  @ivar temp_file_manager: container for temporary files created during
793
    conversion
794
  @type temp_dir: string
795
  @ivar temp_dir: temporary directory created then we deal with OVA
796

797
  """
798
  def __init__(self, input_path, options):
799
    """Initialize the converter.
800

801
    @type input_path: string
802
    @param input_path: path to the Converter input file
803
    @type options: optparse.Values
804
    @param options: command line options
805

806
    @raise errors.OpPrereqError: if file does not exist
807

808
    """
809
    input_path = os.path.abspath(input_path)
810
    if not os.path.isfile(input_path):
811
      raise errors.OpPrereqError("File does not exist: %s" % input_path)
812
    self.options = options
813
    self.temp_file_manager = utils.TemporaryFileManager()
814
    self.temp_dir = None
815
    self.output_dir = None
816
    self._ReadInputData(input_path)
817

    
818
  def _ReadInputData(self, input_path):
819
    """Reads the data on which the conversion will take place.
820

821
    @type input_path: string
822
    @param input_path: absolute path to the Converter input file
823

824
    """
825
    raise NotImplementedError()
826

    
827
  def _CompressDisk(self, disk_path, compression, action):
828
    """Performs (de)compression on the disk and returns the new path
829

830
    @type disk_path: string
831
    @param disk_path: path to the disk
832
    @type compression: string
833
    @param compression: compression type
834
    @type action: string
835
    @param action: whether the action is compression or decompression
836
    @rtype: string
837
    @return: new disk path after (de)compression
838

839
    @raise errors.OpPrereqError: disk (de)compression failed or "compression"
840
      is not supported
841

842
    """
843
    assert(action in ALLOWED_ACTIONS)
844
    # For now we only support gzip, as it is used in ovftool
845
    if compression != COMPRESSION_TYPE:
846
      raise errors.OpPrereqError("Unsupported compression type: %s"
847
                                 % compression)
848
    disk_file = os.path.basename(disk_path)
849
    if action == DECOMPRESS:
850
      (disk_name, _) = os.path.splitext(disk_file)
851
      prefix = disk_name
852
    elif action == COMPRESS:
853
      prefix = disk_file
854
    new_path = utils.GetClosedTempfile(suffix=COMPRESSION_EXT, prefix=prefix,
855
      dir=self.output_dir)
856
    self.temp_file_manager.Add(new_path)
857
    args = ["gzip", "-c", disk_path]
858
    run_result = utils.RunCmd(args, output=new_path)
859
    if run_result.failed:
860
      raise errors.OpPrereqError("Disk %s failed with output: %s"
861
                                 % (action, run_result.stderr))
862
    logging.info("The %s of the disk is completed", action)
863
    return (COMPRESSION_EXT, new_path)
864

    
865
  def _ConvertDisk(self, disk_format, disk_path):
866
    """Performes conversion to specified format.
867

868
    @type disk_format: string
869
    @param disk_format: format to which the disk should be converted
870
    @type disk_path: string
871
    @param disk_path: path to the disk that should be converted
872
    @rtype: string
873
    @return path to the output disk
874

875
    @raise errors.OpPrereqError: convertion of the disk failed
876

877
    """
878
    disk_file = os.path.basename(disk_path)
879
    (disk_name, disk_extension) = os.path.splitext(disk_file)
880
    if disk_extension != disk_format:
881
      logging.warning("Conversion of disk image to %s format, this may take"
882
                      " a while", disk_format)
883

    
884
    new_disk_path = utils.GetClosedTempfile(suffix=".%s" % disk_format,
885
      prefix=disk_name, dir=self.output_dir)
886
    self.temp_file_manager.Add(new_disk_path)
887
    args = [
888
      "qemu-img",
889
      "convert",
890
      "-O",
891
      disk_format,
892
      disk_path,
893
      new_disk_path,
894
    ]
895
    run_result = utils.RunCmd(args, cwd=os.getcwd())
896
    if run_result.failed:
897
      raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was"
898
                                 ": %s" % (disk_format, run_result.stderr))
899
    return (".%s" % disk_format, new_disk_path)
900

    
901
  @staticmethod
902
  def _GetDiskQemuInfo(disk_path, regexp):
903
    """Figures out some information of the disk using qemu-img.
904

905
    @type disk_path: string
906
    @param disk_path: path to the disk we want to know the format of
907
    @type regexp: string
908
    @param regexp: string that has to be matched, it has to contain one group
909
    @rtype: string
910
    @return: disk format
911

912
    @raise errors.OpPrereqError: format information cannot be retrieved
913

914
    """
915
    args = ["qemu-img", "info", disk_path]
916
    run_result = utils.RunCmd(args, cwd=os.getcwd())
917
    if run_result.failed:
918
      raise errors.OpPrereqError("Gathering info about the disk using qemu-img"
919
                                 " failed, output was: %s" % run_result.stderr)
920
    result = run_result.output
921
    regexp = r"%s" % regexp
922
    match = re.search(regexp, result)
923
    if match:
924
      disk_format = match.group(1)
925
    else:
926
      raise errors.OpPrereqError("No file information matching %s found in:"
927
                                 " %s" % (regexp, result))
928
    return disk_format
929

    
930
  def Parse(self):
931
    """Parses the data and creates a structure containing all required info.
932

933
    """
934
    raise NotImplementedError()
935

    
936
  def Save(self):
937
    """Saves the gathered configuration in an apropriate format.
938

939
    """
940
    raise NotImplementedError()
941

    
942
  def Cleanup(self):
943
    """Cleans the temporary directory, if one was created.
944

945
    """
946
    self.temp_file_manager.Cleanup()
947
    if self.temp_dir:
948
      shutil.rmtree(self.temp_dir)
949
      self.temp_dir = None
950

    
951

    
952
class OVFImporter(Converter):
953
  """Converter from OVF to Ganeti config file.
954

955
  @type input_dir: string
956
  @ivar input_dir: directory in which the .ovf file resides
957
  @type output_dir: string
958
  @ivar output_dir: directory to which the results of conversion shall be
959
    written
960
  @type input_path: string
961
  @ivar input_path: complete path to the .ovf file
962
  @type ovf_reader: L{OVFReader}
963
  @ivar ovf_reader: OVF reader instance collects data from .ovf file
964
  @type results_name: string
965
  @ivar results_name: name of imported instance
966
  @type results_template: string
967
  @ivar results_template: disk template read from .ovf file or command line
968
    arguments
969
  @type results_hypervisor: dict
970
  @ivar results_hypervisor: hypervisor information gathered from .ovf file or
971
    command line arguments
972
  @type results_os: dict
973
  @ivar results_os: operating system information gathered from .ovf file or
974
    command line arguments
975
  @type results_backend: dict
976
  @ivar results_backend: backend information gathered from .ovf file or
977
    command line arguments
978
  @type results_tags: string
979
  @ivar results_tags: string containing instance-specific tags
980
  @type results_version: string
981
  @ivar results_version: version as required by Ganeti import
982
  @type results_network: dict
983
  @ivar results_network: network information gathered from .ovf file or command
984
    line arguments
985
  @type results_disk: dict
986
  @ivar results_disk: disk information gathered from .ovf file or command line
987
    arguments
988

989
  """
990
  def _ReadInputData(self, input_path):
991
    """Reads the data on which the conversion will take place.
992

993
    @type input_path: string
994
    @param input_path: absolute path to the .ovf or .ova input file
995

996
    @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
997

998
    """
999
    (input_dir, input_file) = os.path.split(input_path)
1000
    (_, input_extension) = os.path.splitext(input_file)
1001

    
1002
    if input_extension == OVF_EXT:
1003
      logging.info("%s file extension found, no unpacking necessary", OVF_EXT)
1004
      self.input_dir = input_dir
1005
      self.input_path = input_path
1006
      self.temp_dir = None
1007
    elif input_extension == OVA_EXT:
1008
      logging.info("%s file extension found, proceeding to unpacking", OVA_EXT)
1009
      self._UnpackOVA(input_path)
1010
    else:
1011
      raise errors.OpPrereqError("Unknown file extension; expected %s or %s"
1012
                                 " file" % (OVA_EXT, OVF_EXT))
1013
    assert ((input_extension == OVA_EXT and self.temp_dir) or
1014
            (input_extension == OVF_EXT and not self.temp_dir))
1015
    assert self.input_dir in self.input_path
1016

    
1017
    if self.options.output_dir:
1018
      self.output_dir = os.path.abspath(self.options.output_dir)
1019
      if (os.path.commonprefix([constants.EXPORT_DIR, self.output_dir]) !=
1020
          constants.EXPORT_DIR):
1021
        logging.warning("Export path is not under %s directory, import to"
1022
                        " Ganeti using gnt-backup may fail",
1023
                        constants.EXPORT_DIR)
1024
    else:
1025
      self.output_dir = constants.EXPORT_DIR
1026

    
1027
    self.ovf_reader = OVFReader(self.input_path)
1028
    self.ovf_reader.VerifyManifest()
1029

    
1030
  def _UnpackOVA(self, input_path):
1031
    """Unpacks the .ova package into temporary directory.
1032

1033
    @type input_path: string
1034
    @param input_path: path to the .ova package file
1035

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

1040
    """
1041
    input_name = None
1042
    if not tarfile.is_tarfile(input_path):
1043
      raise errors.OpPrereqError("The provided %s file is not a proper tar"
1044
                                 " archive", OVA_EXT)
1045
    ova_content = tarfile.open(input_path)
1046
    temp_dir = tempfile.mkdtemp()
1047
    self.temp_dir = temp_dir
1048
    for file_name in ova_content.getnames():
1049
      file_normname = os.path.normpath(file_name)
1050
      try:
1051
        utils.PathJoin(temp_dir, file_normname)
1052
      except ValueError, err:
1053
        raise errors.OpPrereqError("File %s inside %s package is not safe" %
1054
                                   (file_name, OVA_EXT))
1055
      if file_name.endswith(OVF_EXT):
1056
        input_name = file_name
1057
    if not input_name:
1058
      raise errors.OpPrereqError("No %s file in %s package found" %
1059
                                 (OVF_EXT, OVA_EXT))
1060
    logging.warning("Unpacking the %s archive, this may take a while",
1061
      input_path)
1062
    self.input_dir = temp_dir
1063
    self.input_path = utils.PathJoin(self.temp_dir, input_name)
1064
    try:
1065
      try:
1066
        extract = ova_content.extractall
1067
      except AttributeError:
1068
        # This is a prehistorical case of using python < 2.5
1069
        for member in ova_content.getmembers():
1070
          ova_content.extract(member, path=self.temp_dir)
1071
      else:
1072
        extract(self.temp_dir)
1073
    except tarfile.TarError, err:
1074
      raise errors.OpPrereqError("Error while extracting %s archive: %s" %
1075
                                 (OVA_EXT, err))
1076
    logging.info("OVA package extracted to %s directory", self.temp_dir)
1077

    
1078
  def Parse(self):
1079
    """Parses the data and creates a structure containing all required info.
1080

1081
    The method reads the information given either as a command line option or as
1082
    a part of the OVF description.
1083

1084
    @raise errors.OpPrereqError: if some required part of the description of
1085
      virtual instance is missing or unable to create output directory
1086

1087
    """
1088
    self.results_name = self._GetInfo("instance name", self.options.name,
1089
      self._ParseNameOptions, self.ovf_reader.GetInstanceName)
1090
    if not self.results_name:
1091
      raise errors.OpPrereqError("Name of instance not provided")
1092

    
1093
    self.output_dir = utils.PathJoin(self.output_dir, self.results_name)
1094
    try:
1095
      utils.Makedirs(self.output_dir)
1096
    except OSError, err:
1097
      raise errors.OpPrereqError("Failed to create directory %s: %s" %
1098
                                 (self.output_dir, err))
1099

    
1100
    self.results_template = self._GetInfo("disk template",
1101
      self.options.disk_template, self._ParseTemplateOptions,
1102
      self.ovf_reader.GetDiskTemplate)
1103
    if not self.results_template:
1104
      logging.info("Disk template not given")
1105

    
1106
    self.results_hypervisor = self._GetInfo("hypervisor",
1107
      self.options.hypervisor, self._ParseHypervisorOptions,
1108
      self.ovf_reader.GetHypervisorData)
1109
    assert self.results_hypervisor["hypervisor_name"]
1110
    if self.results_hypervisor["hypervisor_name"] == constants.VALUE_AUTO:
1111
      logging.debug("Default hypervisor settings from the cluster will be used")
1112

    
1113
    self.results_os = self._GetInfo("OS", self.options.os,
1114
      self._ParseOSOptions, self.ovf_reader.GetOSData)
1115
    if not self.results_os.get("os_name"):
1116
      raise errors.OpPrereqError("OS name must be provided")
1117

    
1118
    self.results_backend = self._GetInfo("backend", self.options.beparams,
1119
      self._ParseBackendOptions, self.ovf_reader.GetBackendData)
1120
    assert self.results_backend.get("vcpus")
1121
    assert self.results_backend.get("memory")
1122
    assert self.results_backend.get("auto_balance") is not None
1123

    
1124
    self.results_tags = self._GetInfo("tags", self.options.tags,
1125
      self._ParseTags, self.ovf_reader.GetTagsData)
1126

    
1127
    ovf_version = self.ovf_reader.GetVersionData()
1128
    if ovf_version:
1129
      self.results_version = ovf_version
1130
    else:
1131
      self.results_version = constants.EXPORT_VERSION
1132

    
1133
    self.results_network = self._GetInfo("network", self.options.nics,
1134
      self._ParseNicOptions, self.ovf_reader.GetNetworkData,
1135
      ignore_test=self.options.no_nics)
1136

    
1137
    self.results_disk = self._GetInfo("disk", self.options.disks,
1138
      self._ParseDiskOptions, self._GetDiskInfo,
1139
      ignore_test=self.results_template == constants.DT_DISKLESS)
1140

    
1141
    if not self.results_disk and not self.results_network:
1142
      raise errors.OpPrereqError("Either disk specification or network"
1143
                                 " description must be present")
1144

    
1145
  @staticmethod
1146
  def _GetInfo(name, cmd_arg, cmd_function, nocmd_function,
1147
    ignore_test=False):
1148
    """Get information about some section - e.g. disk, network, hypervisor.
1149

1150
    @type name: string
1151
    @param name: name of the section
1152
    @type cmd_arg: dict
1153
    @param cmd_arg: command line argument specific for section 'name'
1154
    @type cmd_function: callable
1155
    @param cmd_function: function to call if 'cmd_args' exists
1156
    @type nocmd_function: callable
1157
    @param nocmd_function: function to call if 'cmd_args' is not there
1158

1159
    """
1160
    if ignore_test:
1161
      logging.info("Information for %s will be ignored", name)
1162
      return {}
1163
    if cmd_arg:
1164
      logging.info("Information for %s will be parsed from command line", name)
1165
      results = cmd_function()
1166
    else:
1167
      logging.info("Information for %s will be parsed from %s file",
1168
        name, OVF_EXT)
1169
      results = nocmd_function()
1170
    logging.info("Options for %s were succesfully read", name)
1171
    return results
1172

    
1173
  def _ParseNameOptions(self):
1174
    """Returns name if one was given in command line.
1175

1176
    @rtype: string
1177
    @return: name of an instance
1178

1179
    """
1180
    return self.options.name
1181

    
1182
  def _ParseTemplateOptions(self):
1183
    """Returns disk template if one was given in command line.
1184

1185
    @rtype: string
1186
    @return: disk template name
1187

1188
    """
1189
    return self.options.disk_template
1190

    
1191
  def _ParseHypervisorOptions(self):
1192
    """Parses hypervisor options given in a command line.
1193

1194
    @rtype: dict
1195
    @return: dictionary containing name of the chosen hypervisor and all the
1196
      options
1197

1198
    """
1199
    assert type(self.options.hypervisor) is tuple
1200
    assert len(self.options.hypervisor) == 2
1201
    results = {}
1202
    if self.options.hypervisor[0]:
1203
      results["hypervisor_name"] = self.options.hypervisor[0]
1204
    else:
1205
      results["hypervisor_name"] = constants.VALUE_AUTO
1206
    results.update(self.options.hypervisor[1])
1207
    return results
1208

    
1209
  def _ParseOSOptions(self):
1210
    """Parses OS options given in command line.
1211

1212
    @rtype: dict
1213
    @return: dictionary containing name of chosen OS and all its options
1214

1215
    """
1216
    assert self.options.os
1217
    results = {}
1218
    results["os_name"] = self.options.os
1219
    results.update(self.options.osparams)
1220
    return results
1221

    
1222
  def _ParseBackendOptions(self):
1223
    """Parses backend options given in command line.
1224

1225
    @rtype: dict
1226
    @return: dictionary containing vcpus, memory and auto-balance options
1227

1228
    """
1229
    assert self.options.beparams
1230
    backend = {}
1231
    backend.update(self.options.beparams)
1232
    must_contain = ["vcpus", "memory", "auto_balance"]
1233
    for element in must_contain:
1234
      if backend.get(element) is None:
1235
        backend[element] = constants.VALUE_AUTO
1236
    return backend
1237

    
1238
  def _ParseTags(self):
1239
    """Returns tags list given in command line.
1240

1241
    @rtype: string
1242
    @return: string containing comma-separated tags
1243

1244
    """
1245
    return self.options.tags
1246

    
1247
  def _ParseNicOptions(self):
1248
    """Parses network options given in a command line or as a dictionary.
1249

1250
    @rtype: dict
1251
    @return: dictionary of network-related options
1252

1253
    """
1254
    assert self.options.nics
1255
    results = {}
1256
    for (nic_id, nic_desc) in self.options.nics:
1257
      results["nic%s_mode" % nic_id] = \
1258
        nic_desc.get("mode", constants.VALUE_AUTO)
1259
      results["nic%s_mac" % nic_id] = nic_desc.get("mac", constants.VALUE_AUTO)
1260
      results["nic%s_link" % nic_id] = \
1261
        nic_desc.get("link", constants.VALUE_AUTO)
1262
      if nic_desc.get("mode") == "bridged":
1263
        results["nic%s_ip" % nic_id] = constants.VALUE_NONE
1264
      else:
1265
        results["nic%s_ip" % nic_id] = constants.VALUE_AUTO
1266
    results["nic_count"] = str(len(self.options.nics))
1267
    return results
1268

    
1269
  def _ParseDiskOptions(self):
1270
    """Parses disk options given in a command line.
1271

1272
    @rtype: dict
1273
    @return: dictionary of disk-related options
1274

1275
    @raise errors.OpPrereqError: disk description does not contain size
1276
      information or size information is invalid or creation failed
1277

1278
    """
1279
    assert self.options.disks
1280
    results = {}
1281
    for (disk_id, disk_desc) in self.options.disks:
1282
      results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id
1283
      if disk_desc.get("size"):
1284
        try:
1285
          disk_size = utils.ParseUnit(disk_desc["size"])
1286
        except ValueError:
1287
          raise errors.OpPrereqError("Invalid disk size for disk %s: %s" %
1288
                                     (disk_id, disk_desc["size"]))
1289
        new_path = utils.PathJoin(self.output_dir, str(disk_id))
1290
        args = [
1291
          "qemu-img",
1292
          "create",
1293
          "-f",
1294
          "raw",
1295
          new_path,
1296
          disk_size,
1297
        ]
1298
        run_result = utils.RunCmd(args)
1299
        if run_result.failed:
1300
          raise errors.OpPrereqError("Creation of disk %s failed, output was:"
1301
                                     " %s" % (new_path, run_result.stderr))
1302
        results["disk%s_size" % disk_id] = str(disk_size)
1303
        results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id
1304
      else:
1305
        raise errors.OpPrereqError("Disks created for import must have their"
1306
                                   " size specified")
1307
    results["disk_count"] = str(len(self.options.disks))
1308
    return results
1309

    
1310
  def _GetDiskInfo(self):
1311
    """Gathers information about disks used by instance, perfomes conversion.
1312

1313
    @rtype: dict
1314
    @return: dictionary of disk-related options
1315

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

1318
    """
1319
    results = {}
1320
    disks_list = self.ovf_reader.GetDisksNames()
1321
    for (counter, (disk_name, disk_compression)) in enumerate(disks_list):
1322
      if os.path.dirname(disk_name):
1323
        raise errors.OpPrereqError("Disks are not allowed to have absolute"
1324
                                   " paths or paths outside main OVF directory")
1325
      disk, _ = os.path.splitext(disk_name)
1326
      disk_path = utils.PathJoin(self.input_dir, disk_name)
1327
      if disk_compression:
1328
        _, disk_path = self._CompressDisk(disk_path, disk_compression,
1329
          DECOMPRESS)
1330
        disk, _ = os.path.splitext(disk)
1331
      if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw":
1332
        logging.info("Conversion to raw format is required")
1333
      ext, new_disk_path = self._ConvertDisk("raw", disk_path)
1334

    
1335
      final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext,
1336
        directory=self.output_dir)
1337
      final_name = os.path.basename(final_disk_path)
1338
      disk_size = os.path.getsize(final_disk_path) / (1024 * 1024)
1339
      results["disk%s_dump" % counter] = final_name
1340
      results["disk%s_size" % counter] = str(disk_size)
1341
      results["disk%s_ivname" % counter] = "disk/%s" % str(counter)
1342
    if disks_list:
1343
      results["disk_count"] = str(len(disks_list))
1344
    return results
1345

    
1346
  def Save(self):
1347
    """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
1348

1349
    @raise errors.OpPrereqError: when saving to config file failed
1350

1351
    """
1352
    logging.info("Conversion was succesfull, saving %s in %s directory",
1353
                 constants.EXPORT_CONF_FILE, self.output_dir)
1354
    results = {
1355
      constants.INISECT_INS: {},
1356
      constants.INISECT_BEP: {},
1357
      constants.INISECT_EXP: {},
1358
      constants.INISECT_OSP: {},
1359
      constants.INISECT_HYP: {},
1360
    }
1361

    
1362
    results[constants.INISECT_INS].update(self.results_disk)
1363
    results[constants.INISECT_INS].update(self.results_network)
1364
    results[constants.INISECT_INS]["hypervisor"] = \
1365
      self.results_hypervisor["hypervisor_name"]
1366
    results[constants.INISECT_INS]["name"] = self.results_name
1367
    if self.results_template:
1368
      results[constants.INISECT_INS]["disk_template"] = self.results_template
1369
    if self.results_tags:
1370
      results[constants.INISECT_INS]["tags"] = self.results_tags
1371

    
1372
    results[constants.INISECT_BEP].update(self.results_backend)
1373

    
1374
    results[constants.INISECT_EXP]["os"] = self.results_os["os_name"]
1375
    results[constants.INISECT_EXP]["version"] = self.results_version
1376

    
1377
    del self.results_os["os_name"]
1378
    results[constants.INISECT_OSP].update(self.results_os)
1379

    
1380
    del self.results_hypervisor["hypervisor_name"]
1381
    results[constants.INISECT_HYP].update(self.results_hypervisor)
1382

    
1383
    output_file_name = utils.PathJoin(self.output_dir,
1384
      constants.EXPORT_CONF_FILE)
1385

    
1386
    output = []
1387
    for section, options in results.iteritems():
1388
      output.append("[%s]" % section)
1389
      for name, value in options.iteritems():
1390
        if value is None:
1391
          value = ""
1392
        output.append("%s = %s" % (name, value))
1393
      output.append("")
1394
    output_contents = "\n".join(output)
1395

    
1396
    try:
1397
      utils.WriteFile(output_file_name, data=output_contents)
1398
    except errors.ProgrammerError, err:
1399
      raise errors.OpPrereqError("Saving the config file failed: %s" % err)
1400

    
1401
    self.Cleanup()
1402

    
1403

    
1404
class ConfigParserWithDefaults(ConfigParser.SafeConfigParser):
1405
  """This is just a wrapper on SafeConfigParser, that uses default values
1406

1407
  """
1408
  def get(self, section, options, raw=None, vars=None): # pylint: disable=W0622
1409
    try:
1410
      result = ConfigParser.SafeConfigParser.get(self, section, options, \
1411
        raw=raw, vars=vars)
1412
    except ConfigParser.NoOptionError:
1413
      result = None
1414
    return result
1415

    
1416
  def getint(self, section, options):
1417
    try:
1418
      result = ConfigParser.SafeConfigParser.get(self, section, options)
1419
    except ConfigParser.NoOptionError:
1420
      result = 0
1421
    return int(result)
1422

    
1423

    
1424
class OVFExporter(Converter):
1425
  """Converter from Ganeti config file to OVF
1426

1427
  @type input_dir: string
1428
  @ivar input_dir: directory in which the config.ini file resides
1429
  @type output_dir: string
1430
  @ivar output_dir: directory to which the results of conversion shall be
1431
    written
1432
  @type packed_dir: string
1433
  @ivar packed_dir: if we want OVA package, this points to the real (i.e. not
1434
    temp) output directory
1435
  @type input_path: string
1436
  @ivar input_path: complete path to the config.ini file
1437
  @type output_path: string
1438
  @ivar output_path: complete path to .ovf file
1439
  @type config_parser: L{ConfigParserWithDefaults}
1440
  @ivar config_parser: parser for the config.ini file
1441
  @type reference_files: list
1442
  @ivar reference_files: files referenced in the ovf file
1443
  @type results_disk: list
1444
  @ivar results_disk: list of dictionaries of disk options from config.ini
1445
  @type results_network: list
1446
  @ivar results_network: list of dictionaries of network options form config.ini
1447
  @type results_name: string
1448
  @ivar results_name: name of the instance
1449
  @type results_vcpus: string
1450
  @ivar results_vcpus: number of VCPUs
1451
  @type results_memory: string
1452
  @ivar results_memory: RAM memory in MB
1453
  @type results_ganeti: dict
1454
  @ivar results_ganeti: dictionary of Ganeti-specific options from config.ini
1455

1456
  """
1457
  def _ReadInputData(self, input_path):
1458
    """Reads the data on which the conversion will take place.
1459

1460
    @type input_path: string
1461
    @param input_path: absolute path to the config.ini input file
1462

1463
    @raise errors.OpPrereqError: error when reading the config file
1464

1465
    """
1466
    input_dir = os.path.dirname(input_path)
1467
    self.input_path = input_path
1468
    self.input_dir = input_dir
1469
    if self.options.output_dir:
1470
      self.output_dir = os.path.abspath(self.options.output_dir)
1471
    else:
1472
      self.output_dir = input_dir
1473
    self.config_parser = ConfigParserWithDefaults()
1474
    logging.info("Reading configuration from %s file", input_path)
1475
    try:
1476
      self.config_parser.read(input_path)
1477
    except ConfigParser.MissingSectionHeaderError, err:
1478
      raise errors.OpPrereqError("Error when trying to read %s: %s" %
1479
                                 (input_path, err))
1480
    if self.options.ova_package:
1481
      self.temp_dir = tempfile.mkdtemp()
1482
      self.packed_dir = self.output_dir
1483
      self.output_dir = self.temp_dir
1484

    
1485
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1486

    
1487
  def _ParseName(self):
1488
    """Parses name from command line options or config file.
1489

1490
    @rtype: string
1491
    @return: name of Ganeti instance
1492

1493
    @raise errors.OpPrereqError: if name of the instance is not provided
1494

1495
    """
1496
    if self.options.name:
1497
      name = self.options.name
1498
    else:
1499
      name = self.config_parser.get(constants.INISECT_INS, NAME)
1500
    if name is None:
1501
      raise errors.OpPrereqError("No instance name found")
1502
    return name
1503

    
1504
  def _ParseVCPUs(self):
1505
    """Parses vcpus number from config file.
1506

1507
    @rtype: int
1508
    @return: number of virtual CPUs
1509

1510
    @raise errors.OpPrereqError: if number of VCPUs equals 0
1511

1512
    """
1513
    vcpus = self.config_parser.getint(constants.INISECT_BEP, VCPUS)
1514
    if vcpus == 0:
1515
      raise errors.OpPrereqError("No CPU information found")
1516
    return vcpus
1517

    
1518
  def _ParseMemory(self):
1519
    """Parses vcpus number from config file.
1520

1521
    @rtype: int
1522
    @return: amount of memory in MB
1523

1524
    @raise errors.OpPrereqError: if amount of memory equals 0
1525

1526
    """
1527
    memory = self.config_parser.getint(constants.INISECT_BEP, MEMORY)
1528
    if memory == 0:
1529
      raise errors.OpPrereqError("No memory information found")
1530
    return memory
1531

    
1532
  def _ParseGaneti(self):
1533
    """Parses Ganeti data from config file.
1534

1535
    @rtype: dictionary
1536
    @return: dictionary of Ganeti-specific options
1537

1538
    """
1539
    results = {}
1540
    # hypervisor
1541
    results["hypervisor"] = {}
1542
    hyp_name = self.config_parser.get(constants.INISECT_INS, HYPERV)
1543
    if hyp_name is None:
1544
      raise errors.OpPrereqError("No hypervisor information found")
1545
    results["hypervisor"]["name"] = hyp_name
1546
    pairs = self.config_parser.items(constants.INISECT_HYP)
1547
    for (name, value) in pairs:
1548
      results["hypervisor"][name] = value
1549
    # os
1550
    results["os"] = {}
1551
    os_name = self.config_parser.get(constants.INISECT_EXP, OS)
1552
    if os_name is None:
1553
      raise errors.OpPrereqError("No operating system information found")
1554
    results["os"]["name"] = os_name
1555
    pairs = self.config_parser.items(constants.INISECT_OSP)
1556
    for (name, value) in pairs:
1557
      results["os"][name] = value
1558
    # other
1559
    others = [
1560
      (constants.INISECT_INS, DISK_TEMPLATE, "disk_template"),
1561
      (constants.INISECT_BEP, AUTO_BALANCE, "auto_balance"),
1562
      (constants.INISECT_INS, TAGS, "tags"),
1563
      (constants.INISECT_EXP, VERSION, "version"),
1564
    ]
1565
    for (section, element, name) in others:
1566
      results[name] = self.config_parser.get(section, element)
1567
    return results
1568

    
1569
  def _ParseNetworks(self):
1570
    """Parses network data from config file.
1571

1572
    @rtype: list
1573
    @return: list of dictionaries of network options
1574

1575
    @raise errors.OpPrereqError: then network mode is not recognized
1576

1577
    """
1578
    results = []
1579
    counter = 0
1580
    while True:
1581
      data_link = \
1582
        self.config_parser.get(constants.INISECT_INS, "nic%s_link" % counter)
1583
      if data_link is None:
1584
        break
1585
      results.append({
1586
        "mode": self.config_parser.get(constants.INISECT_INS,
1587
           "nic%s_mode" % counter),
1588
        "mac": self.config_parser.get(constants.INISECT_INS,
1589
           "nic%s_mac" % counter),
1590
        "ip": self.config_parser.get(constants.INISECT_INS,
1591
           "nic%s_ip" % counter),
1592
        "link": data_link,
1593
      })
1594
      if results[counter]["mode"] not in constants.NIC_VALID_MODES:
1595
        raise errors.OpPrereqError("Network mode %s not recognized"
1596
                                   % results[counter]["mode"])
1597
      counter += 1
1598
    return results
1599

    
1600
  def _GetDiskOptions(self, disk_file, compression):
1601
    """Convert the disk and gather disk info for .ovf file.
1602

1603
    @type disk_file: string
1604
    @param disk_file: name of the disk (without the full path)
1605
    @type compression: bool
1606
    @param compression: whether the disk should be compressed or not
1607

1608
    @raise errors.OpPrereqError: when disk image does not exist
1609

1610
    """
1611
    disk_path = utils.PathJoin(self.input_dir, disk_file)
1612
    results = {}
1613
    if not os.path.isfile(disk_path):
1614
      raise errors.OpPrereqError("Disk image does not exist: %s" % disk_path)
1615
    if os.path.dirname(disk_file):
1616
      raise errors.OpPrereqError("Path for the disk: %s contains a directory"
1617
                                 " name" % disk_path)
1618
    disk_name, _ = os.path.splitext(disk_file)
1619
    ext, new_disk_path = self._ConvertDisk(self.options.disk_format, disk_path)
1620
    results["format"] = self.options.disk_format
1621
    results["virt-size"] = self._GetDiskQemuInfo(new_disk_path,
1622
      "virtual size: \S+ \((\d+) bytes\)")
1623
    if compression:
1624
      ext2, new_disk_path = self._CompressDisk(new_disk_path, "gzip",
1625
        COMPRESS)
1626
      disk_name, _ = os.path.splitext(disk_name)
1627
      results["compression"] = "gzip"
1628
      ext += ext2
1629
    final_disk_path = LinkFile(new_disk_path, prefix=disk_name, suffix=ext,
1630
      directory=self.output_dir)
1631
    final_disk_name = os.path.basename(final_disk_path)
1632
    results["real-size"] = os.path.getsize(final_disk_path)
1633
    results["path"] = final_disk_name
1634
    self.references_files.append(final_disk_path)
1635
    return results
1636

    
1637
  def _ParseDisks(self):
1638
    """Parses disk data from config file.
1639

1640
    @rtype: list
1641
    @return: list of dictionaries of disk options
1642

1643
    """
1644
    results = []
1645
    counter = 0
1646
    while True:
1647
      disk_file = \
1648
        self.config_parser.get(constants.INISECT_INS, "disk%s_dump" % counter)
1649
      if disk_file is None:
1650
        break
1651
      results.append(self._GetDiskOptions(disk_file, self.options.compression))
1652
      counter += 1
1653
    return results
1654

    
1655
  def Parse(self):
1656
    """Parses the data and creates a structure containing all required info.
1657

1658
    """
1659
    try:
1660
      utils.Makedirs(self.output_dir)
1661
    except OSError, err:
1662
      raise errors.OpPrereqError("Failed to create directory %s: %s" %
1663
                                 (self.output_dir, err))
1664

    
1665
    self.references_files = []
1666
    self.results_name = self._ParseName()
1667
    self.results_vcpus = self._ParseVCPUs()
1668
    self.results_memory = self._ParseMemory()
1669
    if not self.options.ext_usage:
1670
      self.results_ganeti = self._ParseGaneti()
1671
    self.results_network = self._ParseNetworks()
1672
    self.results_disk = self._ParseDisks()
1673

    
1674
  def _PrepareManifest(self, path):
1675
    """Creates manifest for all the files in OVF package.
1676

1677
    @type path: string
1678
    @param path: path to manifesto file
1679

1680
    @raise errors.OpPrereqError: if error occurs when writing file
1681

1682
    """
1683
    logging.info("Preparing manifest for the OVF package")
1684
    lines = []
1685
    files_list = [self.output_path]
1686
    files_list.extend(self.references_files)
1687
    logging.warning("Calculating SHA1 checksums, this may take a while")
1688
    sha1_sums = utils.FingerprintFiles(files_list)
1689
    for file_path, value in sha1_sums.iteritems():
1690
      file_name = os.path.basename(file_path)
1691
      lines.append("SHA1(%s)= %s" % (file_name, value))
1692
    lines.append("")
1693
    data = "\n".join(lines)
1694
    try:
1695
      utils.WriteFile(path, data=data)
1696
    except errors.ProgrammerError, err:
1697
      raise errors.OpPrereqError("Saving the manifest file failed: %s" % err)
1698

    
1699
  @staticmethod
1700
  def _PrepareTarFile(tar_path, files_list):
1701
    """Creates tarfile from the files in OVF package.
1702

1703
    @type tar_path: string
1704
    @param tar_path: path to the resulting file
1705
    @type files_list: list
1706
    @param files_list: list of files in the OVF package
1707

1708
    """
1709
    logging.info("Preparing tarball for the OVF package")
1710
    open(tar_path, mode="w").close()
1711
    ova_package = tarfile.open(name=tar_path, mode="w")
1712
    for file_name in files_list:
1713
      ova_package.add(file_name)
1714
    ova_package.close()
1715

    
1716
  def Save(self):
1717
    """Saves the gathered configuration in an apropriate format.
1718

1719
    @raise errors.OpPrereqError: if unable to create output directory
1720

1721
    """
1722
    output_file = "%s%s" % (self.results_name, OVF_EXT)
1723
    output_path = utils.PathJoin(self.output_dir, output_file)
1724
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1725
    logging.info("Saving read data to %s", output_path)
1726

    
1727
    self.output_path = utils.PathJoin(self.output_dir, output_file)
1728
    files_list = [self.output_path]
1729

    
1730
    self.ovf_writer.SaveDisksData(self.results_disk)
1731
    self.ovf_writer.SaveNetworksData(self.results_network)
1732
    if not self.options.ext_usage:
1733
      self.ovf_writer.SaveGanetiData(self.results_ganeti, self.results_network)
1734

    
1735
    self.ovf_writer.SaveVirtualSystemData(self.results_name, self.results_vcpus,
1736
      self.results_memory)
1737

    
1738
    data = self.ovf_writer.PrettyXmlDump()
1739
    utils.WriteFile(self.output_path, data=data)
1740

    
1741
    manifest_file = "%s%s" % (self.results_name, MF_EXT)
1742
    manifest_path = utils.PathJoin(self.output_dir, manifest_file)
1743
    self._PrepareManifest(manifest_path)
1744
    files_list.append(manifest_path)
1745

    
1746
    files_list.extend(self.references_files)
1747

    
1748
    if self.options.ova_package:
1749
      ova_file = "%s%s" % (self.results_name, OVA_EXT)
1750
      packed_path = utils.PathJoin(self.packed_dir, ova_file)
1751
      try:
1752
        utils.Makedirs(self.packed_dir)
1753
      except OSError, err:
1754
        raise errors.OpPrereqError("Failed to create directory %s: %s" %
1755
                                   (self.packed_dir, err))
1756
      self._PrepareTarFile(packed_path, files_list)
1757
    logging.info("Creation of the OVF package was successfull")
1758
    self.Cleanup()