Statistics
| Branch: | Tag: | Revision:

root / lib / ovf.py @ 70817cee

History | View | Annotate | Download (63 kB)

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

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

    
21

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

24
"""
25

    
26
# pylint: disable=F0401, E1101
27

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

    
31

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

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

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

    
57

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

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

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

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

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

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

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

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

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

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

    
146

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

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

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

    
157

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

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

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

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

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

    
197

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

431
    """
432
    results = {}
433

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

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

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

    
469
    return results
470

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

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

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

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

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

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

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

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

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

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

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

    
540
      network_name = network_name.lower()
541

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

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

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

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

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

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

569
    @rtype: list
570
    @return: list of file names, as referenced in .ovf file
571

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

    
588

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

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

    
600

    
601
class OVFWriter(object):
602
  """Writer class for OVF files.
603

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

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

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

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

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

645
    @type disks: list
646
    @param disks: list of dictionaries of disk options from config.ini
647

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

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

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

684
    @type networks: list
685
    @param networks: list of dictionaries of network options form config.ini
686

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

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

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

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

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

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

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

731
    """
732
    ganeti_section = ET.SubElement(self.tree, "gnt:GanetiSection")
733

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

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

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

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

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

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

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

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

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

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

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

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

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

    
818
  def PrettyXmlDump(self):
819
    """Formatter of the XML file.
820

821
    @rtype: string
822
    @return: XML tree in the form of nicely-formatted string
823

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

    
831

    
832
class Converter(object):
833
  """Converter class for OVF packages.
834

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

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

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

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

858
    @raise errors.OpPrereqError: if file does not exist
859

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

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

873
    @type input_path: string
874
    @param input_path: absolute path to the Converter input file
875

876
    """
877
    raise NotImplementedError()
878

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

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

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

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

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

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

927
    @raise errors.OpPrereqError: convertion of the disk failed
928

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

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

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

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

965
    @raise errors.OpPrereqError: format information cannot be retrieved
966

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

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

987
    """
988
    raise NotImplementedError()
989

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

993
    """
994
    raise NotImplementedError()
995

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

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

    
1005

    
1006
class OVFImporter(Converter):
1007
  """Converter from OVF to Ganeti config file.
1008

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

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

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

1050
    @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
1051

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

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

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

    
1081
    self.ovf_reader = OVFReader(self.input_path)
1082
    self.ovf_reader.VerifyManifest()
1083

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

1087
    @type input_path: string
1088
    @param input_path: path to the .ova package file
1089

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

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

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

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

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

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

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

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

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

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

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

    
1178
    self.results_tags = self._GetInfo("tags", self.options.tags,
1179
      self._ParseTags, self.ovf_reader.GetTagsData)
1180

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

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

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

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

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

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

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

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

1230
    @rtype: string
1231
    @return: name of an instance
1232

1233
    """
1234
    return self.options.name
1235

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

1239
    @rtype: string
1240
    @return: disk template name
1241

1242
    """
1243
    return self.options.disk_template
1244

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

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

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

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

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

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

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

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

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

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

1295
    @rtype: string
1296
    @return: string containing comma-separated tags
1297

1298
    """
1299
    return self.options.tags
1300

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

1304
    @rtype: dict
1305
    @return: dictionary of network-related options
1306

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

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

1326
    @rtype: dict
1327
    @return: dictionary of disk-related options
1328

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

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

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

1368
    @rtype: dict
1369
    @return: dictionary of disk-related options
1370

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

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

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

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

1404
    @raise errors.OpPrereqError: when saving to config file failed
1405

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

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

    
1427
    results[constants.INISECT_BEP].update(self.results_backend)
1428

    
1429
    results[constants.INISECT_EXP]["os"] = self.results_os["os_name"]
1430
    results[constants.INISECT_EXP]["version"] = self.results_version
1431

    
1432
    del self.results_os["os_name"]
1433
    results[constants.INISECT_OSP].update(self.results_os)
1434

    
1435
    del self.results_hypervisor["hypervisor_name"]
1436
    results[constants.INISECT_HYP].update(self.results_hypervisor)
1437

    
1438
    output_file_name = utils.PathJoin(self.output_dir,
1439
      constants.EXPORT_CONF_FILE)
1440

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

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

    
1456
    self.Cleanup()
1457

    
1458

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

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

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

    
1478

    
1479
class OVFExporter(Converter):
1480
  """Converter from Ganeti config file to OVF
1481

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

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

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

1518
    @raise errors.OpPrereqError: error when reading the config file
1519

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

    
1540
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1541

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

1545
    @rtype: string
1546
    @return: name of Ganeti instance
1547

1548
    @raise errors.OpPrereqError: if name of the instance is not provided
1549

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

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

1562
    @rtype: int
1563
    @return: number of virtual CPUs
1564

1565
    @raise errors.OpPrereqError: if number of VCPUs equals 0
1566

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

    
1573
  def _ParseMemory(self):
1574
    """Parses vcpus number from config file.
1575

1576
    @rtype: int
1577
    @return: amount of memory in MB
1578

1579
    @raise errors.OpPrereqError: if amount of memory equals 0
1580

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

    
1587
  def _ParseGaneti(self):
1588
    """Parses Ganeti data from config file.
1589

1590
    @rtype: dictionary
1591
    @return: dictionary of Ganeti-specific options
1592

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

    
1624
  def _ParseNetworks(self):
1625
    """Parses network data from config file.
1626

1627
    @rtype: list
1628
    @return: list of dictionaries of network options
1629

1630
    @raise errors.OpPrereqError: then network mode is not recognized
1631

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

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

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

1663
    @raise errors.OpPrereqError: when disk image does not exist
1664

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

    
1692
  def _ParseDisks(self):
1693
    """Parses disk data from config file.
1694

1695
    @rtype: list
1696
    @return: list of dictionaries of disk options
1697

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

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

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

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

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

1732
    @type path: string
1733
    @param path: path to manifesto file
1734

1735
    @raise errors.OpPrereqError: if error occurs when writing file
1736

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

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

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

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

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

1775
    @raise errors.OpPrereqError: if unable to create output directory
1776

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

    
1783
    self.output_path = utils.PathJoin(self.output_dir, output_file)
1784
    files_list = [self.output_path]
1785

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

    
1791
    self.ovf_writer.SaveVirtualSystemData(self.results_name, self.results_vcpus,
1792
      self.results_memory)
1793

    
1794
    data = self.ovf_writer.PrettyXmlDump()
1795
    utils.WriteFile(self.output_path, data=data)
1796

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

    
1802
    files_list.extend(self.references_files)
1803

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