Statistics
| Branch: | Tag: | Revision:

root / lib / ovf.py @ d6f8db24

History | View | Annotate | Download (62.6 kB)

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

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

    
21

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

24
"""
25

    
26
# pylint: disable=F0401, E1101
27

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

    
31

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

    
48
try:
49
  ParseError = ET.ParseError # pylint: disable=E1103
50
except AttributeError:
51
  ParseError = None
52

    
53
from ganeti import constants
54
from ganeti import errors
55
from ganeti import utils
56

    
57

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

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

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

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

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

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

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

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

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

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

    
146

    
147
def LinkFile(old_path, prefix=None, suffix=None, directory=None):
148
  """Create link with a given prefix and suffix.
149

150
  This is a wrapper over os.link. It tries to create a hard link for given file,
151
  but instead of rising error when file exists, the function changes the name
152
  a little bit.
153

154
  @type old_path:string
155
  @param old_path: path to the file that is to be linked
156
  @type prefix: string
157
  @param prefix: prefix of filename for the link
158
  @type suffix: string
159
  @param suffix: suffix of the filename for the link
160
  @type directory: string
161
  @param directory: directory of the link
162

163
  @raise errors.OpPrereqError: when error on linking is different than
164
    "File exists"
165

166
  """
167
  assert(prefix is not None or suffix is not None)
168
  if directory is None:
169
    directory = os.getcwd()
170
  new_path = utils.PathJoin(directory, "%s%s" % (prefix, suffix))
171
  counter = 1
172
  while True:
173
    try:
174
      os.link(old_path, new_path)
175
      break
176
    except OSError, err:
177
      if err.errno == errno.EEXIST:
178
        new_path = utils.PathJoin(directory,
179
          "%s_%s%s" % (prefix, counter, suffix))
180
        counter += 1
181
      else:
182
        raise errors.OpPrereqError("Error moving the file %s to %s location:"
183
                                   " %s" % (old_path, new_path, err))
184
  return new_path
185

    
186

    
187
class OVFReader(object):
188
  """Reader class for OVF files.
189

190
  @type files_list: list
191
  @ivar files_list: list of files in the OVF package
192
  @type tree: ET.ElementTree
193
  @ivar tree: XML tree of the .ovf file
194
  @type schema_name: string
195
  @ivar schema_name: name of the .ovf file
196
  @type input_dir: string
197
  @ivar input_dir: directory in which the .ovf file resides
198

199
  """
200
  def __init__(self, input_path):
201
    """Initialiaze the reader - load the .ovf file to XML parser.
202

203
    It is assumed that names of manifesto (.mf), certificate (.cert) and ovf
204
    files are the same. In order to account any other files as part of the ovf
205
    package, they have to be explicitly mentioned in the Resources section
206
    of the .ovf file.
207

208
    @type input_path: string
209
    @param input_path: absolute path to the .ovf file
210

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

214
    """
215
    self.tree = ET.ElementTree()
216
    try:
217
      self.tree.parse(input_path)
218
    except (ParseError, xml.parsers.expat.ExpatError), err:
219
      raise errors.OpPrereqError("Error while reading %s file: %s" %
220
                                 (OVF_EXT, err))
221

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

    
243
  def _GetAttributes(self, path, attribute):
244
    """Get specified attribute from all nodes accessible using given path.
245

246
    Function follows the path from root node to the desired tags using path,
247
    then reads the apropriate attribute values.
248

249
    @type path: string
250
    @param path: path of nodes to visit
251
    @type attribute: string
252
    @param attribute: attribute for which we gather the information
253
    @rtype: list
254
    @return: for each accessible tag with the attribute value set, value of the
255
      attribute
256

257
    """
258
    current_list = self.tree.findall(path)
259
    results = [x.get(attribute) for x in current_list]
260
    return filter(None, results)
261

    
262
  def _GetElementMatchingAttr(self, path, match_attr):
263
    """Searches for element on a path that matches certain attribute 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 attribute value.
267

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

275
    """
276
    potential_elements = self.tree.findall(path)
277
    (attr, val) = match_attr
278
    for elem in potential_elements:
279
      if elem.get(attr) == val:
280
        return elem
281
    return None
282

    
283
  def _GetElementMatchingText(self, path, match_text):
284
    """Searches for element on a path that matches certain text value.
285

286
    Function follows the path from root node to the desired tags using path,
287
    then searches for the first one matching the text value.
288

289
    @type path: string
290
    @param path: path of nodes to visit
291
    @type match_text: tuple
292
    @param match_text: pair (node, text) for which we search
293
    @rtype: ET.ElementTree or None
294
    @return: first element matching match_text or None if nothing matches
295

296
    """
297
    potential_elements = self.tree.findall(path)
298
    (node, text) = match_text
299
    for elem in potential_elements:
300
      if elem.findtext(node) == text:
301
        return elem
302
    return None
303

    
304
  @staticmethod
305
  def _GetDictParameters(root, schema):
306
    """Reads text in all children and creates the dictionary from the contents.
307

308
    @type root: ET.ElementTree or None
309
    @param root: father of the nodes we want to collect data about
310
    @type schema: string
311
    @param schema: schema name to be removed from the tag
312
    @rtype: dict
313
    @return: dictionary containing tags and their text contents, tags have their
314
      schema fragment removed or empty dictionary, when root is None
315

316
    """
317
    if not root:
318
      return {}
319
    results = {}
320
    for element in list(root):
321
      pref_len = len("{%s}" % schema)
322
      assert(schema in element.tag)
323
      tag = element.tag[pref_len:]
324
      results[tag] = element.text
325
    return results
326

    
327
  def VerifyManifest(self):
328
    """Verifies manifest for the OVF package, if one is given.
329

330
    @raise errors.OpPrereqError: if SHA1 checksums do not match
331

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

    
355
  def GetInstanceName(self):
356
    """Provides information about instance name.
357

358
    @rtype: string
359
    @return: instance name string
360

361
    """
362
    find_name = "{%s}VirtualSystem/{%s}Name" % (OVF_SCHEMA, OVF_SCHEMA)
363
    return self.tree.findtext(find_name)
364

    
365
  def GetDiskTemplate(self):
366
    """Returns disk template from .ovf file
367

368
    @rtype: string or None
369
    @return: name of the template
370
    """
371
    find_template = ("{%s}GanetiSection/{%s}DiskTemplate" %
372
                     (GANETI_SCHEMA, GANETI_SCHEMA))
373
    return self.tree.findtext(find_template)
374

    
375
  def GetHypervisorData(self):
376
    """Provides hypervisor information - hypervisor name and options.
377

378
    @rtype: dict
379
    @return: dictionary containing name of the used hypervisor and all the
380
      specified options
381

382
    """
383
    hypervisor_search = ("{%s}GanetiSection/{%s}Hypervisor" %
384
                         (GANETI_SCHEMA, GANETI_SCHEMA))
385
    hypervisor_data = self.tree.find(hypervisor_search)
386
    if not hypervisor_data:
387
      return {"hypervisor_name": constants.VALUE_AUTO}
388
    results = {
389
      "hypervisor_name": hypervisor_data.findtext("{%s}Name" % GANETI_SCHEMA,
390
                           default=constants.VALUE_AUTO),
391
    }
392
    parameters = hypervisor_data.find("{%s}Parameters" % GANETI_SCHEMA)
393
    results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
394
    return results
395

    
396
  def GetOSData(self):
397
    """ Provides operating system information - os name and options.
398

399
    @rtype: dict
400
    @return: dictionary containing name and options for the chosen OS
401

402
    """
403
    results = {}
404
    os_search = ("{%s}GanetiSection/{%s}OperatingSystem" %
405
                 (GANETI_SCHEMA, GANETI_SCHEMA))
406
    os_data = self.tree.find(os_search)
407
    if os_data:
408
      results["os_name"] = os_data.findtext("{%s}Name" % GANETI_SCHEMA)
409
      parameters = os_data.find("{%s}Parameters" % GANETI_SCHEMA)
410
      results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
411
    return results
412

    
413
  def GetBackendData(self):
414
    """ Provides backend information - vcpus, memory, auto balancing options.
415

416
    @rtype: dict
417
    @return: dictionary containing options for vcpus, memory and auto balance
418
      settings
419

420
    """
421
    results = {}
422

    
423
    find_vcpus = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item" %
424
                   (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
425
    match_vcpus = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["vcpus"])
426
    vcpus = self._GetElementMatchingText(find_vcpus, match_vcpus)
427
    if vcpus:
428
      vcpus_count = vcpus.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
429
        default=constants.VALUE_AUTO)
430
    else:
431
      vcpus_count = constants.VALUE_AUTO
432
    results["vcpus"] = str(vcpus_count)
433

    
434
    find_memory = find_vcpus
435
    match_memory = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["memory"])
436
    memory = self._GetElementMatchingText(find_memory, match_memory)
437
    memory_raw = None
438
    if memory:
439
      alloc_units = memory.findtext("{%s}AllocationUnits" % RASD_SCHEMA)
440
      matching_units = [units for units, variants in
441
        ALLOCATION_UNITS.iteritems() if alloc_units.lower() in variants]
442
      if matching_units == []:
443
        raise errors.OpPrereqError("Unit %s for RAM memory unknown",
444
          alloc_units)
445
      units = matching_units[0]
446
      memory_raw = int(memory.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
447
            default=constants.VALUE_AUTO))
448
      memory_count = CONVERT_UNITS_TO_MB[units](memory_raw)
449
    else:
450
      memory_count = constants.VALUE_AUTO
451
    results["memory"] = str(memory_count)
452

    
453
    find_balance = ("{%s}GanetiSection/{%s}AutoBalance" %
454
                   (GANETI_SCHEMA, GANETI_SCHEMA))
455
    balance = self.tree.findtext(find_balance, default=constants.VALUE_AUTO)
456
    results["auto_balance"] = balance
457

    
458
    return results
459

    
460
  def GetTagsData(self):
461
    """Provides tags information for instance.
462

463
    @rtype: string or None
464
    @return: string of comma-separated tags for the instance
465

466
    """
467
    find_tags = "{%s}GanetiSection/{%s}Tags" % (GANETI_SCHEMA, GANETI_SCHEMA)
468
    results = self.tree.findtext(find_tags)
469
    if results:
470
      return results
471
    else:
472
      return None
473

    
474
  def GetVersionData(self):
475
    """Provides version number read from .ovf file
476

477
    @rtype: string
478
    @return: string containing the version number
479

480
    """
481
    find_version = ("{%s}GanetiSection/{%s}Version" %
482
                    (GANETI_SCHEMA, GANETI_SCHEMA))
483
    return self.tree.findtext(find_version)
484

    
485
  def GetNetworkData(self):
486
    """Provides data about the network in the OVF instance.
487

488
    The method gathers the data about networks used by OVF instance. It assumes
489
    that 'name' tag means something - in essence, if it contains one of the
490
    words 'bridged' or 'routed' then that will be the mode of this network in
491
    Ganeti. The information about the network can be either in GanetiSection or
492
    VirtualHardwareSection.
493

494
    @rtype: dict
495
    @return: dictionary containing all the network information
496

497
    """
498
    results = {}
499
    networks_search = ("{%s}NetworkSection/{%s}Network" %
500
                       (OVF_SCHEMA, OVF_SCHEMA))
501
    network_names = self._GetAttributes(networks_search,
502
      "{%s}name" % OVF_SCHEMA)
503
    required = ["ip", "mac", "link", "mode"]
504
    for (counter, network_name) in enumerate(network_names):
505
      network_search = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item"
506
                        % (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
507
      ganeti_search = ("{%s}GanetiSection/{%s}Network/{%s}Nic" %
508
                       (GANETI_SCHEMA, GANETI_SCHEMA, GANETI_SCHEMA))
509
      network_match = ("{%s}Connection" % RASD_SCHEMA, network_name)
510
      ganeti_match = ("{%s}name" % OVF_SCHEMA, network_name)
511
      network_data = self._GetElementMatchingText(network_search, network_match)
512
      network_ganeti_data = self._GetElementMatchingAttr(ganeti_search,
513
        ganeti_match)
514

    
515
      ganeti_data = {}
516
      if network_ganeti_data:
517
        ganeti_data["mode"] = network_ganeti_data.findtext("{%s}Mode" %
518
                                                           GANETI_SCHEMA)
519
        ganeti_data["mac"] = network_ganeti_data.findtext("{%s}MACAddress" %
520
                                                          GANETI_SCHEMA)
521
        ganeti_data["ip"] = network_ganeti_data.findtext("{%s}IPAddress" %
522
                                                         GANETI_SCHEMA)
523
        ganeti_data["link"] = network_ganeti_data.findtext("{%s}Link" %
524
                                                           GANETI_SCHEMA)
525
      mac_data = None
526
      if network_data:
527
        mac_data = network_data.findtext("{%s}Address" % RASD_SCHEMA)
528

    
529
      network_name = network_name.lower()
530

    
531
      # First, some not Ganeti-specific information is collected
532
      if constants.NIC_MODE_BRIDGED in network_name:
533
        results["nic%s_mode" % counter] = "bridged"
534
      elif constants.NIC_MODE_ROUTED in network_name:
535
        results["nic%s_mode" % counter] = "routed"
536
      results["nic%s_mac" % counter] = mac_data
537

    
538
      # GanetiSection data overrides 'manually' collected data
539
      for name, value in ganeti_data.iteritems():
540
        results["nic%s_%s" % (counter, name)] = value
541

    
542
      # Bridged network has no IP - unless specifically stated otherwise
543
      if (results.get("nic%s_mode" % counter) == "bridged" and
544
          not results.get("nic%s_ip" % counter)):
545
        results["nic%s_ip" % counter] = constants.VALUE_NONE
546

    
547
      for option in required:
548
        if not results.get("nic%s_%s" % (counter, option)):
549
          results["nic%s_%s" % (counter, option)] = constants.VALUE_AUTO
550

    
551
    if network_names:
552
      results["nic_count"] = str(len(network_names))
553
    return results
554

    
555
  def GetDisksNames(self):
556
    """Provides list of file names for the disks used by the instance.
557

558
    @rtype: list
559
    @return: list of file names, as referenced in .ovf file
560

561
    """
562
    results = []
563
    disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA)
564
    disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA)
565
    for disk in disk_ids:
566
      disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA)
567
      disk_match = ("{%s}id" % OVF_SCHEMA, disk)
568
      disk_elem = self._GetElementMatchingAttr(disk_search, disk_match)
569
      if disk_elem is None:
570
        raise errors.OpPrereqError("%s file corrupted - disk %s not found in"
571
                                   " references" % (OVF_EXT, disk))
572
      disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA)
573
      disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA)
574
      results.append((disk_name, disk_compression))
575
    return results
576

    
577

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

582
  """
583
  if text is None:
584
    return None
585
  elem = ET.SubElement(parent, tag, attrib=attrib, **extra)
586
  elem.text = str(text)
587
  return elem
588

    
589

    
590
class OVFWriter(object):
591
  """Writer class for OVF files.
592

593
  @type tree: ET.ElementTree
594
  @ivar tree: XML tree that we are constructing
595
  @type virtual_system_type: string
596
  @ivar virtual_system_type: value of vssd:VirtualSystemType, for external usage
597
    in VMWare this requires to be vmx
598
  @type hardware_list: list
599
  @ivar hardware_list: list of items prepared for VirtualHardwareSection
600
  @type next_instance_id: int
601
  @ivar next_instance_id: next instance id to be used when creating elements on
602
    hardware_list
603

604
  """
605
  def __init__(self, has_gnt_section):
606
    """Initialize the writer - set the top element.
607

608
    @type has_gnt_section: bool
609
    @param has_gnt_section: if the Ganeti schema should be added - i.e. this
610
      means that Ganeti section will be present
611

612
    """
613
    env_attribs = {
614
      "xmlns:xsi": XML_SCHEMA,
615
      "xmlns:vssd": VSSD_SCHEMA,
616
      "xmlns:rasd": RASD_SCHEMA,
617
      "xmlns:ovf": OVF_SCHEMA,
618
      "xmlns": OVF_SCHEMA,
619
      "xml:lang": "en-US",
620
    }
621
    if has_gnt_section:
622
      env_attribs["xmlns:gnt"] = GANETI_SCHEMA
623
      self.virtual_system_type = VS_TYPE["ganeti"]
624
    else:
625
      self.virtual_system_type = VS_TYPE["external"]
626
    self.tree = ET.Element("Envelope", attrib=env_attribs)
627
    self.hardware_list = []
628
    # INSTANCE_ID contains statically assigned IDs, starting from 0
629
    self.next_instance_id = len(INSTANCE_ID) # FIXME: hackish
630

    
631
  def SaveDisksData(self, disks):
632
    """Convert disk information to certain OVF sections.
633

634
    @type disks: list
635
    @param disks: list of dictionaries of disk options from config.ini
636

637
    """
638
    references = ET.SubElement(self.tree, "References")
639
    disk_section = ET.SubElement(self.tree, "DiskSection")
640
    SubElementText(disk_section, "Info", "Virtual disk information")
641
    for counter, disk in enumerate(disks):
642
      file_id = "file%s" % counter
643
      disk_id = "disk%s" % counter
644
      file_attribs = {
645
        "ovf:href": disk["path"],
646
        "ovf:size": str(disk["real-size"]),
647
        "ovf:id": file_id,
648
      }
649
      disk_attribs = {
650
        "ovf:capacity": str(disk["virt-size"]),
651
        "ovf:diskId": disk_id,
652
        "ovf:fileRef": file_id,
653
        "ovf:format": DISK_FORMAT.get(disk["format"], disk["format"]),
654
      }
655
      if "compression" in disk:
656
        file_attribs["ovf:compression"] = disk["compression"]
657
      ET.SubElement(references, "File", attrib=file_attribs)
658
      ET.SubElement(disk_section, "Disk", attrib=disk_attribs)
659

    
660
      # Item in VirtualHardwareSection creation
661
      disk_item = ET.Element("Item")
662
      SubElementText(disk_item, "rasd:ElementName", disk_id)
663
      SubElementText(disk_item, "rasd:HostResource", "ovf:/disk/%s" % disk_id)
664
      SubElementText(disk_item, "rasd:InstanceID", self.next_instance_id)
665
      SubElementText(disk_item, "rasd:Parent", INSTANCE_ID["scsi"])
666
      SubElementText(disk_item, "rasd:ResourceType", RASD_TYPE["disk"])
667
      self.hardware_list.append(disk_item)
668
      self.next_instance_id += 1
669

    
670
  def SaveNetworksData(self, networks):
671
    """Convert network information to NetworkSection.
672

673
    @type networks: list
674
    @param networks: list of dictionaries of network options form config.ini
675

676
    """
677
    network_section = ET.SubElement(self.tree, "NetworkSection")
678
    SubElementText(network_section, "Info", "List of logical networks")
679
    for counter, network in enumerate(networks):
680
      network_name = "%s%s" % (network["mode"], counter)
681
      network_attrib = {"ovf:name": network_name}
682
      ET.SubElement(network_section, "Network", attrib=network_attrib)
683

    
684
      # Item in VirtualHardwareSection creation
685
      network_item = ET.Element("Item")
686
      SubElementText(network_item, "rasd:Address", network["mac"])
687
      SubElementText(network_item, "rasd:Connection", network_name)
688
      SubElementText(network_item, "rasd:ElementName", network_name)
689
      SubElementText(network_item, "rasd:InstanceID", self.next_instance_id)
690
      SubElementText(network_item, "rasd:ResourceType",
691
        RASD_TYPE["ethernet-adapter"])
692
      self.hardware_list.append(network_item)
693
      self.next_instance_id += 1
694

    
695
  @staticmethod
696
  def _SaveNameAndParams(root, data):
697
    """Save name and parameters information under root using data.
698

699
    @type root: ET.Element
700
    @param root: root element for the Name and Parameters
701
    @type data: dict
702
    @param data: data from which we gather the values
703

704
    """
705
    assert(data.get("name"))
706
    name = SubElementText(root, "gnt:Name", data["name"])
707
    params = ET.SubElement(root, "gnt:Parameters")
708
    for name, value in data.iteritems():
709
      if name != "name":
710
        SubElementText(params, "gnt:%s" % name, value)
711

    
712
  def SaveGanetiData(self, ganeti, networks):
713
    """Convert Ganeti-specific information to GanetiSection.
714

715
    @type ganeti: dict
716
    @param ganeti: dictionary of Ganeti-specific options from config.ini
717
    @type networks: list
718
    @param networks: list of dictionaries of network options form config.ini
719

720
    """
721
    ganeti_section = ET.SubElement(self.tree, "gnt:GanetiSection")
722

    
723
    SubElementText(ganeti_section, "gnt:Version", ganeti.get("version"))
724
    SubElementText(ganeti_section, "gnt:DiskTemplate",
725
      ganeti.get("disk_template"))
726
    SubElementText(ganeti_section, "gnt:AutoBalance",
727
      ganeti.get("auto_balance"))
728
    SubElementText(ganeti_section, "gnt:Tags", ganeti.get("tags"))
729

    
730
    osys = ET.SubElement(ganeti_section, "gnt:OperatingSystem")
731
    self._SaveNameAndParams(osys, ganeti["os"])
732

    
733
    hypervisor = ET.SubElement(ganeti_section, "gnt:Hypervisor")
734
    self._SaveNameAndParams(hypervisor, ganeti["hypervisor"])
735

    
736
    network_section = ET.SubElement(ganeti_section, "gnt:Network")
737
    for counter, network in enumerate(networks):
738
      network_name = "%s%s" % (network["mode"], counter)
739
      nic_attrib = {"ovf:name": network_name}
740
      nic = ET.SubElement(network_section, "gnt:Nic", attrib=nic_attrib)
741
      SubElementText(nic, "gnt:Mode", network["mode"])
742
      SubElementText(nic, "gnt:MACAddress", network["mac"])
743
      SubElementText(nic, "gnt:IPAddress", network["ip"])
744
      SubElementText(nic, "gnt:Link", network["link"])
745

    
746
  def SaveVirtualSystemData(self, name, vcpus, memory):
747
    """Convert virtual system information to OVF sections.
748

749
    @type name: string
750
    @param name: name of the instance
751
    @type vcpus: int
752
    @param vcpus: number of VCPUs
753
    @type memory: int
754
    @param memory: RAM memory in MB
755

756
    """
757
    assert(vcpus > 0)
758
    assert(memory > 0)
759
    vs_attrib = {"ovf:id": name}
760
    virtual_system = ET.SubElement(self.tree, "VirtualSystem", attrib=vs_attrib)
761
    SubElementText(virtual_system, "Info", "A virtual machine")
762

    
763
    name_section = ET.SubElement(virtual_system, "Name")
764
    name_section.text = name
765
    os_attrib = {"ovf:id": "0"}
766
    os_section = ET.SubElement(virtual_system, "OperatingSystemSection",
767
      attrib=os_attrib)
768
    SubElementText(os_section, "Info", "Installed guest operating system")
769
    hardware_section = ET.SubElement(virtual_system, "VirtualHardwareSection")
770
    SubElementText(hardware_section, "Info", "Virtual hardware requirements")
771

    
772
    # System description
773
    system = ET.SubElement(hardware_section, "System")
774
    SubElementText(system, "vssd:ElementName", "Virtual Hardware Family")
775
    SubElementText(system, "vssd:InstanceID", INSTANCE_ID["system"])
776
    SubElementText(system, "vssd:VirtualSystemIdentifier", name)
777
    SubElementText(system, "vssd:VirtualSystemType", self.virtual_system_type)
778

    
779
    # Item for vcpus
780
    vcpus_item = ET.SubElement(hardware_section, "Item")
781
    SubElementText(vcpus_item, "rasd:ElementName",
782
      "%s virtual CPU(s)" % vcpus)
783
    SubElementText(vcpus_item, "rasd:InstanceID", INSTANCE_ID["vcpus"])
784
    SubElementText(vcpus_item, "rasd:ResourceType", RASD_TYPE["vcpus"])
785
    SubElementText(vcpus_item, "rasd:VirtualQuantity", vcpus)
786

    
787
    # Item for memory
788
    memory_item = ET.SubElement(hardware_section, "Item")
789
    SubElementText(memory_item, "rasd:AllocationUnits", "byte * 2^20")
790
    SubElementText(memory_item, "rasd:ElementName", "%sMB of memory" % memory)
791
    SubElementText(memory_item, "rasd:InstanceID", INSTANCE_ID["memory"])
792
    SubElementText(memory_item, "rasd:ResourceType", RASD_TYPE["memory"])
793
    SubElementText(memory_item, "rasd:VirtualQuantity", memory)
794

    
795
    # Item for scsi controller
796
    scsi_item = ET.SubElement(hardware_section, "Item")
797
    SubElementText(scsi_item, "rasd:Address", INSTANCE_ID["system"])
798
    SubElementText(scsi_item, "rasd:ElementName", "scsi_controller0")
799
    SubElementText(scsi_item, "rasd:InstanceID", INSTANCE_ID["scsi"])
800
    SubElementText(scsi_item, "rasd:ResourceSubType", SCSI_SUBTYPE)
801
    SubElementText(scsi_item, "rasd:ResourceType", RASD_TYPE["scsi-controller"])
802

    
803
    # Other items - from self.hardware_list
804
    for item in self.hardware_list:
805
      hardware_section.append(item)
806

    
807
  def PrettyXmlDump(self):
808
    """Formatter of the XML file.
809

810
    @rtype: string
811
    @return: XML tree in the form of nicely-formatted string
812

813
    """
814
    raw_string = ET.tostring(self.tree)
815
    parsed_xml = xml.dom.minidom.parseString(raw_string)
816
    xml_string = parsed_xml.toprettyxml(indent="  ")
817
    text_re = re.compile(">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL)
818
    return text_re.sub(">\g<1></", xml_string)
819

    
820

    
821
class Converter(object):
822
  """Converter class for OVF packages.
823

824
  Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is
825
  to provide a common interface for the two.
826

827
  @type options: optparse.Values
828
  @ivar options: options parsed from the command line
829
  @type output_dir: string
830
  @ivar output_dir: directory to which the results of conversion shall be
831
    written
832
  @type temp_file_manager: L{utils.TemporaryFileManager}
833
  @ivar temp_file_manager: container for temporary files created during
834
    conversion
835
  @type temp_dir: string
836
  @ivar temp_dir: temporary directory created then we deal with OVA
837

838
  """
839
  def __init__(self, input_path, options):
840
    """Initialize the converter.
841

842
    @type input_path: string
843
    @param input_path: path to the Converter input file
844
    @type options: optparse.Values
845
    @param options: command line options
846

847
    @raise errors.OpPrereqError: if file does not exist
848

849
    """
850
    input_path = os.path.abspath(input_path)
851
    if not os.path.isfile(input_path):
852
      raise errors.OpPrereqError("File does not exist: %s" % input_path)
853
    self.options = options
854
    self.temp_file_manager = utils.TemporaryFileManager()
855
    self.temp_dir = None
856
    self.output_dir = None
857
    self._ReadInputData(input_path)
858

    
859
  def _ReadInputData(self, input_path):
860
    """Reads the data on which the conversion will take place.
861

862
    @type input_path: string
863
    @param input_path: absolute path to the Converter input file
864

865
    """
866
    raise NotImplementedError()
867

    
868
  def _CompressDisk(self, disk_path, compression, action):
869
    """Performs (de)compression on the disk and returns the new path
870

871
    @type disk_path: string
872
    @param disk_path: path to the disk
873
    @type compression: string
874
    @param compression: compression type
875
    @type action: string
876
    @param action: whether the action is compression or decompression
877
    @rtype: string
878
    @return: new disk path after (de)compression
879

880
    @raise errors.OpPrereqError: disk (de)compression failed or "compression"
881
      is not supported
882

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

    
906
  def _ConvertDisk(self, disk_format, disk_path):
907
    """Performes conversion to specified format.
908

909
    @type disk_format: string
910
    @param disk_format: format to which the disk should be converted
911
    @type disk_path: string
912
    @param disk_path: path to the disk that should be converted
913
    @rtype: string
914
    @return path to the output disk
915

916
    @raise errors.OpPrereqError: convertion of the disk failed
917

918
    """
919
    disk_file = os.path.basename(disk_path)
920
    (disk_name, disk_extension) = os.path.splitext(disk_file)
921
    if disk_extension != disk_format:
922
      logging.warning("Conversion of disk image to %s format, this may take"
923
                      " a while", disk_format)
924

    
925
    new_disk_path = utils.GetClosedTempfile(suffix=".%s" % disk_format,
926
      prefix=disk_name, dir=self.output_dir)
927
    self.temp_file_manager.Add(new_disk_path)
928
    args = [
929
      "qemu-img",
930
      "convert",
931
      "-O",
932
      disk_format,
933
      disk_path,
934
      new_disk_path,
935
    ]
936
    run_result = utils.RunCmd(args, cwd=os.getcwd())
937
    if run_result.failed:
938
      raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was"
939
                                 ": %s" % (disk_format, run_result.stderr))
940
    return (".%s" % disk_format, new_disk_path)
941

    
942
  @staticmethod
943
  def _GetDiskQemuInfo(disk_path, regexp):
944
    """Figures out some information of the disk using qemu-img.
945

946
    @type disk_path: string
947
    @param disk_path: path to the disk we want to know the format of
948
    @type regexp: string
949
    @param regexp: string that has to be matched, it has to contain one group
950
    @rtype: string
951
    @return: disk format
952

953
    @raise errors.OpPrereqError: format information cannot be retrieved
954

955
    """
956
    args = ["qemu-img", "info", disk_path]
957
    run_result = utils.RunCmd(args, cwd=os.getcwd())
958
    if run_result.failed:
959
      raise errors.OpPrereqError("Gathering info about the disk using qemu-img"
960
                                 " failed, output was: %s" % run_result.stderr)
961
    result = run_result.output
962
    regexp = r"%s" % regexp
963
    match = re.search(regexp, result)
964
    if match:
965
      disk_format = match.group(1)
966
    else:
967
      raise errors.OpPrereqError("No file information matching %s found in:"
968
                                 " %s" % (regexp, result))
969
    return disk_format
970

    
971
  def Parse(self):
972
    """Parses the data and creates a structure containing all required info.
973

974
    """
975
    raise NotImplementedError()
976

    
977
  def Save(self):
978
    """Saves the gathered configuration in an apropriate format.
979

980
    """
981
    raise NotImplementedError()
982

    
983
  def Cleanup(self):
984
    """Cleans the temporary directory, if one was created.
985

986
    """
987
    self.temp_file_manager.Cleanup()
988
    if self.temp_dir:
989
      shutil.rmtree(self.temp_dir)
990
      self.temp_dir = None
991

    
992

    
993
class OVFImporter(Converter):
994
  """Converter from OVF to Ganeti config file.
995

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

1030
  """
1031
  def _ReadInputData(self, input_path):
1032
    """Reads the data on which the conversion will take place.
1033

1034
    @type input_path: string
1035
    @param input_path: absolute path to the .ovf or .ova input file
1036

1037
    @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
1038

1039
    """
1040
    (input_dir, input_file) = os.path.split(input_path)
1041
    (_, input_extension) = os.path.splitext(input_file)
1042

    
1043
    if input_extension == OVF_EXT:
1044
      logging.info("%s file extension found, no unpacking necessary", OVF_EXT)
1045
      self.input_dir = input_dir
1046
      self.input_path = input_path
1047
      self.temp_dir = None
1048
    elif input_extension == OVA_EXT:
1049
      logging.info("%s file extension found, proceeding to unpacking", OVA_EXT)
1050
      self._UnpackOVA(input_path)
1051
    else:
1052
      raise errors.OpPrereqError("Unknown file extension; expected %s or %s"
1053
                                 " file" % (OVA_EXT, OVF_EXT))
1054
    assert ((input_extension == OVA_EXT and self.temp_dir) or
1055
            (input_extension == OVF_EXT and not self.temp_dir))
1056
    assert self.input_dir in self.input_path
1057

    
1058
    if self.options.output_dir:
1059
      self.output_dir = os.path.abspath(self.options.output_dir)
1060
      if (os.path.commonprefix([constants.EXPORT_DIR, self.output_dir]) !=
1061
          constants.EXPORT_DIR):
1062
        logging.warning("Export path is not under %s directory, import to"
1063
                        " Ganeti using gnt-backup may fail",
1064
                        constants.EXPORT_DIR)
1065
    else:
1066
      self.output_dir = constants.EXPORT_DIR
1067

    
1068
    self.ovf_reader = OVFReader(self.input_path)
1069
    self.ovf_reader.VerifyManifest()
1070

    
1071
  def _UnpackOVA(self, input_path):
1072
    """Unpacks the .ova package into temporary directory.
1073

1074
    @type input_path: string
1075
    @param input_path: path to the .ova package file
1076

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

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

    
1119
  def Parse(self):
1120
    """Parses the data and creates a structure containing all required info.
1121

1122
    The method reads the information given either as a command line option or as
1123
    a part of the OVF description.
1124

1125
    @raise errors.OpPrereqError: if some required part of the description of
1126
      virtual instance is missing or unable to create output directory
1127

1128
    """
1129
    self.results_name = self._GetInfo("instance name", self.options.name,
1130
      self._ParseNameOptions, self.ovf_reader.GetInstanceName)
1131
    if not self.results_name:
1132
      raise errors.OpPrereqError("Name of instance not provided")
1133

    
1134
    self.output_dir = utils.PathJoin(self.output_dir, self.results_name)
1135
    try:
1136
      utils.Makedirs(self.output_dir)
1137
    except OSError, err:
1138
      raise errors.OpPrereqError("Failed to create directory %s: %s" %
1139
                                 (self.output_dir, err))
1140

    
1141
    self.results_template = self._GetInfo("disk template",
1142
      self.options.disk_template, self._ParseTemplateOptions,
1143
      self.ovf_reader.GetDiskTemplate)
1144
    if not self.results_template:
1145
      logging.info("Disk template not given")
1146

    
1147
    self.results_hypervisor = self._GetInfo("hypervisor",
1148
      self.options.hypervisor, self._ParseHypervisorOptions,
1149
      self.ovf_reader.GetHypervisorData)
1150
    assert self.results_hypervisor["hypervisor_name"]
1151
    if self.results_hypervisor["hypervisor_name"] == constants.VALUE_AUTO:
1152
      logging.debug("Default hypervisor settings from the cluster will be used")
1153

    
1154
    self.results_os = self._GetInfo("OS", self.options.os,
1155
      self._ParseOSOptions, self.ovf_reader.GetOSData)
1156
    if not self.results_os.get("os_name"):
1157
      raise errors.OpPrereqError("OS name must be provided")
1158

    
1159
    self.results_backend = self._GetInfo("backend", self.options.beparams,
1160
      self._ParseBackendOptions, self.ovf_reader.GetBackendData)
1161
    assert self.results_backend.get("vcpus")
1162
    assert self.results_backend.get("memory")
1163
    assert self.results_backend.get("auto_balance") is not None
1164

    
1165
    self.results_tags = self._GetInfo("tags", self.options.tags,
1166
      self._ParseTags, self.ovf_reader.GetTagsData)
1167

    
1168
    ovf_version = self.ovf_reader.GetVersionData()
1169
    if ovf_version:
1170
      self.results_version = ovf_version
1171
    else:
1172
      self.results_version = constants.EXPORT_VERSION
1173

    
1174
    self.results_network = self._GetInfo("network", self.options.nics,
1175
      self._ParseNicOptions, self.ovf_reader.GetNetworkData,
1176
      ignore_test=self.options.no_nics)
1177

    
1178
    self.results_disk = self._GetInfo("disk", self.options.disks,
1179
      self._ParseDiskOptions, self._GetDiskInfo,
1180
      ignore_test=self.results_template == constants.DT_DISKLESS)
1181

    
1182
    if not self.results_disk and not self.results_network:
1183
      raise errors.OpPrereqError("Either disk specification or network"
1184
                                 " description must be present")
1185

    
1186
  @staticmethod
1187
  def _GetInfo(name, cmd_arg, cmd_function, nocmd_function,
1188
    ignore_test=False):
1189
    """Get information about some section - e.g. disk, network, hypervisor.
1190

1191
    @type name: string
1192
    @param name: name of the section
1193
    @type cmd_arg: dict
1194
    @param cmd_arg: command line argument specific for section 'name'
1195
    @type cmd_function: callable
1196
    @param cmd_function: function to call if 'cmd_args' exists
1197
    @type nocmd_function: callable
1198
    @param nocmd_function: function to call if 'cmd_args' is not there
1199

1200
    """
1201
    if ignore_test:
1202
      logging.info("Information for %s will be ignored", name)
1203
      return {}
1204
    if cmd_arg:
1205
      logging.info("Information for %s will be parsed from command line", name)
1206
      results = cmd_function()
1207
    else:
1208
      logging.info("Information for %s will be parsed from %s file",
1209
        name, OVF_EXT)
1210
      results = nocmd_function()
1211
    logging.info("Options for %s were succesfully read", name)
1212
    return results
1213

    
1214
  def _ParseNameOptions(self):
1215
    """Returns name if one was given in command line.
1216

1217
    @rtype: string
1218
    @return: name of an instance
1219

1220
    """
1221
    return self.options.name
1222

    
1223
  def _ParseTemplateOptions(self):
1224
    """Returns disk template if one was given in command line.
1225

1226
    @rtype: string
1227
    @return: disk template name
1228

1229
    """
1230
    return self.options.disk_template
1231

    
1232
  def _ParseHypervisorOptions(self):
1233
    """Parses hypervisor options given in a command line.
1234

1235
    @rtype: dict
1236
    @return: dictionary containing name of the chosen hypervisor and all the
1237
      options
1238

1239
    """
1240
    assert type(self.options.hypervisor) is tuple
1241
    assert len(self.options.hypervisor) == 2
1242
    results = {}
1243
    if self.options.hypervisor[0]:
1244
      results["hypervisor_name"] = self.options.hypervisor[0]
1245
    else:
1246
      results["hypervisor_name"] = constants.VALUE_AUTO
1247
    results.update(self.options.hypervisor[1])
1248
    return results
1249

    
1250
  def _ParseOSOptions(self):
1251
    """Parses OS options given in command line.
1252

1253
    @rtype: dict
1254
    @return: dictionary containing name of chosen OS and all its options
1255

1256
    """
1257
    assert self.options.os
1258
    results = {}
1259
    results["os_name"] = self.options.os
1260
    results.update(self.options.osparams)
1261
    return results
1262

    
1263
  def _ParseBackendOptions(self):
1264
    """Parses backend options given in command line.
1265

1266
    @rtype: dict
1267
    @return: dictionary containing vcpus, memory and auto-balance options
1268

1269
    """
1270
    assert self.options.beparams
1271
    backend = {}
1272
    backend.update(self.options.beparams)
1273
    must_contain = ["vcpus", "memory", "auto_balance"]
1274
    for element in must_contain:
1275
      if backend.get(element) is None:
1276
        backend[element] = constants.VALUE_AUTO
1277
    return backend
1278

    
1279
  def _ParseTags(self):
1280
    """Returns tags list given in command line.
1281

1282
    @rtype: string
1283
    @return: string containing comma-separated tags
1284

1285
    """
1286
    return self.options.tags
1287

    
1288
  def _ParseNicOptions(self):
1289
    """Parses network options given in a command line or as a dictionary.
1290

1291
    @rtype: dict
1292
    @return: dictionary of network-related options
1293

1294
    """
1295
    assert self.options.nics
1296
    results = {}
1297
    for (nic_id, nic_desc) in self.options.nics:
1298
      results["nic%s_mode" % nic_id] = \
1299
        nic_desc.get("mode", constants.VALUE_AUTO)
1300
      results["nic%s_mac" % nic_id] = nic_desc.get("mac", constants.VALUE_AUTO)
1301
      results["nic%s_link" % nic_id] = \
1302
        nic_desc.get("link", constants.VALUE_AUTO)
1303
      if nic_desc.get("mode") == "bridged":
1304
        results["nic%s_ip" % nic_id] = constants.VALUE_NONE
1305
      else:
1306
        results["nic%s_ip" % nic_id] = constants.VALUE_AUTO
1307
    results["nic_count"] = str(len(self.options.nics))
1308
    return results
1309

    
1310
  def _ParseDiskOptions(self):
1311
    """Parses disk options given in a command line.
1312

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

1316
    @raise errors.OpPrereqError: disk description does not contain size
1317
      information or size information is invalid or creation failed
1318

1319
    """
1320
    assert self.options.disks
1321
    results = {}
1322
    for (disk_id, disk_desc) in self.options.disks:
1323
      results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id
1324
      if disk_desc.get("size"):
1325
        try:
1326
          disk_size = utils.ParseUnit(disk_desc["size"])
1327
        except ValueError:
1328
          raise errors.OpPrereqError("Invalid disk size for disk %s: %s" %
1329
                                     (disk_id, disk_desc["size"]))
1330
        new_path = utils.PathJoin(self.output_dir, str(disk_id))
1331
        args = [
1332
          "qemu-img",
1333
          "create",
1334
          "-f",
1335
          "raw",
1336
          new_path,
1337
          disk_size,
1338
        ]
1339
        run_result = utils.RunCmd(args)
1340
        if run_result.failed:
1341
          raise errors.OpPrereqError("Creation of disk %s failed, output was:"
1342
                                     " %s" % (new_path, run_result.stderr))
1343
        results["disk%s_size" % disk_id] = str(disk_size)
1344
        results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id
1345
      else:
1346
        raise errors.OpPrereqError("Disks created for import must have their"
1347
                                   " size specified")
1348
    results["disk_count"] = str(len(self.options.disks))
1349
    return results
1350

    
1351
  def _GetDiskInfo(self):
1352
    """Gathers information about disks used by instance, perfomes conversion.
1353

1354
    @rtype: dict
1355
    @return: dictionary of disk-related options
1356

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

1359
    """
1360
    results = {}
1361
    disks_list = self.ovf_reader.GetDisksNames()
1362
    for (counter, (disk_name, disk_compression)) in enumerate(disks_list):
1363
      if os.path.dirname(disk_name):
1364
        raise errors.OpPrereqError("Disks are not allowed to have absolute"
1365
                                   " paths or paths outside main OVF directory")
1366
      disk, _ = os.path.splitext(disk_name)
1367
      disk_path = utils.PathJoin(self.input_dir, disk_name)
1368
      if disk_compression not in NO_COMPRESSION:
1369
        _, disk_path = self._CompressDisk(disk_path, disk_compression,
1370
          DECOMPRESS)
1371
        disk, _ = os.path.splitext(disk)
1372
      if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw":
1373
        logging.info("Conversion to raw format is required")
1374
      ext, new_disk_path = self._ConvertDisk("raw", disk_path)
1375

    
1376
      final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext,
1377
        directory=self.output_dir)
1378
      final_name = os.path.basename(final_disk_path)
1379
      disk_size = os.path.getsize(final_disk_path) / (1024 * 1024)
1380
      results["disk%s_dump" % counter] = final_name
1381
      results["disk%s_size" % counter] = str(disk_size)
1382
      results["disk%s_ivname" % counter] = "disk/%s" % str(counter)
1383
    if disks_list:
1384
      results["disk_count"] = str(len(disks_list))
1385
    return results
1386

    
1387
  def Save(self):
1388
    """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
1389

1390
    @raise errors.OpPrereqError: when saving to config file failed
1391

1392
    """
1393
    logging.info("Conversion was succesfull, saving %s in %s directory",
1394
                 constants.EXPORT_CONF_FILE, self.output_dir)
1395
    results = {
1396
      constants.INISECT_INS: {},
1397
      constants.INISECT_BEP: {},
1398
      constants.INISECT_EXP: {},
1399
      constants.INISECT_OSP: {},
1400
      constants.INISECT_HYP: {},
1401
    }
1402

    
1403
    results[constants.INISECT_INS].update(self.results_disk)
1404
    results[constants.INISECT_INS].update(self.results_network)
1405
    results[constants.INISECT_INS]["hypervisor"] = \
1406
      self.results_hypervisor["hypervisor_name"]
1407
    results[constants.INISECT_INS]["name"] = self.results_name
1408
    if self.results_template:
1409
      results[constants.INISECT_INS]["disk_template"] = self.results_template
1410
    if self.results_tags:
1411
      results[constants.INISECT_INS]["tags"] = self.results_tags
1412

    
1413
    results[constants.INISECT_BEP].update(self.results_backend)
1414

    
1415
    results[constants.INISECT_EXP]["os"] = self.results_os["os_name"]
1416
    results[constants.INISECT_EXP]["version"] = self.results_version
1417

    
1418
    del self.results_os["os_name"]
1419
    results[constants.INISECT_OSP].update(self.results_os)
1420

    
1421
    del self.results_hypervisor["hypervisor_name"]
1422
    results[constants.INISECT_HYP].update(self.results_hypervisor)
1423

    
1424
    output_file_name = utils.PathJoin(self.output_dir,
1425
      constants.EXPORT_CONF_FILE)
1426

    
1427
    output = []
1428
    for section, options in results.iteritems():
1429
      output.append("[%s]" % section)
1430
      for name, value in options.iteritems():
1431
        if value is None:
1432
          value = ""
1433
        output.append("%s = %s" % (name, value))
1434
      output.append("")
1435
    output_contents = "\n".join(output)
1436

    
1437
    try:
1438
      utils.WriteFile(output_file_name, data=output_contents)
1439
    except errors.ProgrammerError, err:
1440
      raise errors.OpPrereqError("Saving the config file failed: %s" % err)
1441

    
1442
    self.Cleanup()
1443

    
1444

    
1445
class ConfigParserWithDefaults(ConfigParser.SafeConfigParser):
1446
  """This is just a wrapper on SafeConfigParser, that uses default values
1447

1448
  """
1449
  def get(self, section, options, raw=None, vars=None): # pylint: disable=W0622
1450
    try:
1451
      result = ConfigParser.SafeConfigParser.get(self, section, options, \
1452
        raw=raw, vars=vars)
1453
    except ConfigParser.NoOptionError:
1454
      result = None
1455
    return result
1456

    
1457
  def getint(self, section, options):
1458
    try:
1459
      result = ConfigParser.SafeConfigParser.get(self, section, options)
1460
    except ConfigParser.NoOptionError:
1461
      result = 0
1462
    return int(result)
1463

    
1464

    
1465
class OVFExporter(Converter):
1466
  """Converter from Ganeti config file to OVF
1467

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

1497
  """
1498
  def _ReadInputData(self, input_path):
1499
    """Reads the data on which the conversion will take place.
1500

1501
    @type input_path: string
1502
    @param input_path: absolute path to the config.ini input file
1503

1504
    @raise errors.OpPrereqError: error when reading the config file
1505

1506
    """
1507
    input_dir = os.path.dirname(input_path)
1508
    self.input_path = input_path
1509
    self.input_dir = input_dir
1510
    if self.options.output_dir:
1511
      self.output_dir = os.path.abspath(self.options.output_dir)
1512
    else:
1513
      self.output_dir = input_dir
1514
    self.config_parser = ConfigParserWithDefaults()
1515
    logging.info("Reading configuration from %s file", input_path)
1516
    try:
1517
      self.config_parser.read(input_path)
1518
    except ConfigParser.MissingSectionHeaderError, err:
1519
      raise errors.OpPrereqError("Error when trying to read %s: %s" %
1520
                                 (input_path, err))
1521
    if self.options.ova_package:
1522
      self.temp_dir = tempfile.mkdtemp()
1523
      self.packed_dir = self.output_dir
1524
      self.output_dir = self.temp_dir
1525

    
1526
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1527

    
1528
  def _ParseName(self):
1529
    """Parses name from command line options or config file.
1530

1531
    @rtype: string
1532
    @return: name of Ganeti instance
1533

1534
    @raise errors.OpPrereqError: if name of the instance is not provided
1535

1536
    """
1537
    if self.options.name:
1538
      name = self.options.name
1539
    else:
1540
      name = self.config_parser.get(constants.INISECT_INS, NAME)
1541
    if name is None:
1542
      raise errors.OpPrereqError("No instance name found")
1543
    return name
1544

    
1545
  def _ParseVCPUs(self):
1546
    """Parses vcpus number from config file.
1547

1548
    @rtype: int
1549
    @return: number of virtual CPUs
1550

1551
    @raise errors.OpPrereqError: if number of VCPUs equals 0
1552

1553
    """
1554
    vcpus = self.config_parser.getint(constants.INISECT_BEP, VCPUS)
1555
    if vcpus == 0:
1556
      raise errors.OpPrereqError("No CPU information found")
1557
    return vcpus
1558

    
1559
  def _ParseMemory(self):
1560
    """Parses vcpus number from config file.
1561

1562
    @rtype: int
1563
    @return: amount of memory in MB
1564

1565
    @raise errors.OpPrereqError: if amount of memory equals 0
1566

1567
    """
1568
    memory = self.config_parser.getint(constants.INISECT_BEP, MEMORY)
1569
    if memory == 0:
1570
      raise errors.OpPrereqError("No memory information found")
1571
    return memory
1572

    
1573
  def _ParseGaneti(self):
1574
    """Parses Ganeti data from config file.
1575

1576
    @rtype: dictionary
1577
    @return: dictionary of Ganeti-specific options
1578

1579
    """
1580
    results = {}
1581
    # hypervisor
1582
    results["hypervisor"] = {}
1583
    hyp_name = self.config_parser.get(constants.INISECT_INS, HYPERV)
1584
    if hyp_name is None:
1585
      raise errors.OpPrereqError("No hypervisor information found")
1586
    results["hypervisor"]["name"] = hyp_name
1587
    pairs = self.config_parser.items(constants.INISECT_HYP)
1588
    for (name, value) in pairs:
1589
      results["hypervisor"][name] = value
1590
    # os
1591
    results["os"] = {}
1592
    os_name = self.config_parser.get(constants.INISECT_EXP, OS)
1593
    if os_name is None:
1594
      raise errors.OpPrereqError("No operating system information found")
1595
    results["os"]["name"] = os_name
1596
    pairs = self.config_parser.items(constants.INISECT_OSP)
1597
    for (name, value) in pairs:
1598
      results["os"][name] = value
1599
    # other
1600
    others = [
1601
      (constants.INISECT_INS, DISK_TEMPLATE, "disk_template"),
1602
      (constants.INISECT_BEP, AUTO_BALANCE, "auto_balance"),
1603
      (constants.INISECT_INS, TAGS, "tags"),
1604
      (constants.INISECT_EXP, VERSION, "version"),
1605
    ]
1606
    for (section, element, name) in others:
1607
      results[name] = self.config_parser.get(section, element)
1608
    return results
1609

    
1610
  def _ParseNetworks(self):
1611
    """Parses network data from config file.
1612

1613
    @rtype: list
1614
    @return: list of dictionaries of network options
1615

1616
    @raise errors.OpPrereqError: then network mode is not recognized
1617

1618
    """
1619
    results = []
1620
    counter = 0
1621
    while True:
1622
      data_link = \
1623
        self.config_parser.get(constants.INISECT_INS, "nic%s_link" % counter)
1624
      if data_link is None:
1625
        break
1626
      results.append({
1627
        "mode": self.config_parser.get(constants.INISECT_INS,
1628
           "nic%s_mode" % counter),
1629
        "mac": self.config_parser.get(constants.INISECT_INS,
1630
           "nic%s_mac" % counter),
1631
        "ip": self.config_parser.get(constants.INISECT_INS,
1632
           "nic%s_ip" % counter),
1633
        "link": data_link,
1634
      })
1635
      if results[counter]["mode"] not in constants.NIC_VALID_MODES:
1636
        raise errors.OpPrereqError("Network mode %s not recognized"
1637
                                   % results[counter]["mode"])
1638
      counter += 1
1639
    return results
1640

    
1641
  def _GetDiskOptions(self, disk_file, compression):
1642
    """Convert the disk and gather disk info for .ovf file.
1643

1644
    @type disk_file: string
1645
    @param disk_file: name of the disk (without the full path)
1646
    @type compression: bool
1647
    @param compression: whether the disk should be compressed or not
1648

1649
    @raise errors.OpPrereqError: when disk image does not exist
1650

1651
    """
1652
    disk_path = utils.PathJoin(self.input_dir, disk_file)
1653
    results = {}
1654
    if not os.path.isfile(disk_path):
1655
      raise errors.OpPrereqError("Disk image does not exist: %s" % disk_path)
1656
    if os.path.dirname(disk_file):
1657
      raise errors.OpPrereqError("Path for the disk: %s contains a directory"
1658
                                 " name" % disk_path)
1659
    disk_name, _ = os.path.splitext(disk_file)
1660
    ext, new_disk_path = self._ConvertDisk(self.options.disk_format, disk_path)
1661
    results["format"] = self.options.disk_format
1662
    results["virt-size"] = self._GetDiskQemuInfo(new_disk_path,
1663
      "virtual size: \S+ \((\d+) bytes\)")
1664
    if compression:
1665
      ext2, new_disk_path = self._CompressDisk(new_disk_path, "gzip",
1666
        COMPRESS)
1667
      disk_name, _ = os.path.splitext(disk_name)
1668
      results["compression"] = "gzip"
1669
      ext += ext2
1670
    final_disk_path = LinkFile(new_disk_path, prefix=disk_name, suffix=ext,
1671
      directory=self.output_dir)
1672
    final_disk_name = os.path.basename(final_disk_path)
1673
    results["real-size"] = os.path.getsize(final_disk_path)
1674
    results["path"] = final_disk_name
1675
    self.references_files.append(final_disk_path)
1676
    return results
1677

    
1678
  def _ParseDisks(self):
1679
    """Parses disk data from config file.
1680

1681
    @rtype: list
1682
    @return: list of dictionaries of disk options
1683

1684
    """
1685
    results = []
1686
    counter = 0
1687
    while True:
1688
      disk_file = \
1689
        self.config_parser.get(constants.INISECT_INS, "disk%s_dump" % counter)
1690
      if disk_file is None:
1691
        break
1692
      results.append(self._GetDiskOptions(disk_file, self.options.compression))
1693
      counter += 1
1694
    return results
1695

    
1696
  def Parse(self):
1697
    """Parses the data and creates a structure containing all required info.
1698

1699
    """
1700
    try:
1701
      utils.Makedirs(self.output_dir)
1702
    except OSError, err:
1703
      raise errors.OpPrereqError("Failed to create directory %s: %s" %
1704
                                 (self.output_dir, err))
1705

    
1706
    self.references_files = []
1707
    self.results_name = self._ParseName()
1708
    self.results_vcpus = self._ParseVCPUs()
1709
    self.results_memory = self._ParseMemory()
1710
    if not self.options.ext_usage:
1711
      self.results_ganeti = self._ParseGaneti()
1712
    self.results_network = self._ParseNetworks()
1713
    self.results_disk = self._ParseDisks()
1714

    
1715
  def _PrepareManifest(self, path):
1716
    """Creates manifest for all the files in OVF package.
1717

1718
    @type path: string
1719
    @param path: path to manifesto file
1720

1721
    @raise errors.OpPrereqError: if error occurs when writing file
1722

1723
    """
1724
    logging.info("Preparing manifest for the OVF package")
1725
    lines = []
1726
    files_list = [self.output_path]
1727
    files_list.extend(self.references_files)
1728
    logging.warning("Calculating SHA1 checksums, this may take a while")
1729
    sha1_sums = utils.FingerprintFiles(files_list)
1730
    for file_path, value in sha1_sums.iteritems():
1731
      file_name = os.path.basename(file_path)
1732
      lines.append("SHA1(%s)= %s" % (file_name, value))
1733
    lines.append("")
1734
    data = "\n".join(lines)
1735
    try:
1736
      utils.WriteFile(path, data=data)
1737
    except errors.ProgrammerError, err:
1738
      raise errors.OpPrereqError("Saving the manifest file failed: %s" % err)
1739

    
1740
  @staticmethod
1741
  def _PrepareTarFile(tar_path, files_list):
1742
    """Creates tarfile from the files in OVF package.
1743

1744
    @type tar_path: string
1745
    @param tar_path: path to the resulting file
1746
    @type files_list: list
1747
    @param files_list: list of files in the OVF package
1748

1749
    """
1750
    logging.info("Preparing tarball for the OVF package")
1751
    open(tar_path, mode="w").close()
1752
    ova_package = tarfile.open(name=tar_path, mode="w")
1753
    for file_path in files_list:
1754
      file_name = os.path.basename(file_path)
1755
      ova_package.add(file_path, arcname=file_name)
1756
    ova_package.close()
1757

    
1758
  def Save(self):
1759
    """Saves the gathered configuration in an apropriate format.
1760

1761
    @raise errors.OpPrereqError: if unable to create output directory
1762

1763
    """
1764
    output_file = "%s%s" % (self.results_name, OVF_EXT)
1765
    output_path = utils.PathJoin(self.output_dir, output_file)
1766
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1767
    logging.info("Saving read data to %s", output_path)
1768

    
1769
    self.output_path = utils.PathJoin(self.output_dir, output_file)
1770
    files_list = [self.output_path]
1771

    
1772
    self.ovf_writer.SaveDisksData(self.results_disk)
1773
    self.ovf_writer.SaveNetworksData(self.results_network)
1774
    if not self.options.ext_usage:
1775
      self.ovf_writer.SaveGanetiData(self.results_ganeti, self.results_network)
1776

    
1777
    self.ovf_writer.SaveVirtualSystemData(self.results_name, self.results_vcpus,
1778
      self.results_memory)
1779

    
1780
    data = self.ovf_writer.PrettyXmlDump()
1781
    utils.WriteFile(self.output_path, data=data)
1782

    
1783
    manifest_file = "%s%s" % (self.results_name, MF_EXT)
1784
    manifest_path = utils.PathJoin(self.output_dir, manifest_file)
1785
    self._PrepareManifest(manifest_path)
1786
    files_list.append(manifest_path)
1787

    
1788
    files_list.extend(self.references_files)
1789

    
1790
    if self.options.ova_package:
1791
      ova_file = "%s%s" % (self.results_name, OVA_EXT)
1792
      packed_path = utils.PathJoin(self.packed_dir, ova_file)
1793
      try:
1794
        utils.Makedirs(self.packed_dir)
1795
      except OSError, err:
1796
        raise errors.OpPrereqError("Failed to create directory %s: %s" %
1797
                                   (self.packed_dir, err))
1798
      self._PrepareTarFile(packed_path, files_list)
1799
    logging.info("Creation of the OVF package was successfull")
1800
    self.Cleanup()