Statistics
| Branch: | Tag: | Revision:

root / lib / ovf.py @ b179ce72

History | View | Annotate | Download (53.1 kB)

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

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

    
21

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

24
"""
25

    
26
# pylint: disable=F0401, E1101
27

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

    
31

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

    
48
from ganeti import constants
49
from ganeti import errors
50
from ganeti import utils
51

    
52

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

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

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

    
79
# ResourceType values
80
RASD_TYPE = {
81
  "vcpus": "3",
82
  "memory": "4",
83
}
84

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

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

    
110

    
111
def LinkFile(old_path, prefix=None, suffix=None, directory=None):
112
  """Create link with a given prefix and suffix.
113

114
  This is a wrapper over os.link. It tries to create a hard link for given file,
115
  but instead of rising error when file exists, the function changes the name
116
  a little bit.
117

118
  @type old_path:string
119
  @param old_path: path to the file that is to be linked
120
  @type prefix: string
121
  @param prefix: prefix of filename for the link
122
  @type suffix: string
123
  @param suffix: suffix of the filename for the link
124
  @type directory: string
125
  @param directory: directory of the link
126

127
  @raise errors.OpPrereqError: when error on linking is different than
128
    "File exists"
129

130
  """
131
  assert(prefix is not None or suffix is not None)
132
  if directory is None:
133
    directory = os.getcwd()
134
  new_path = utils.PathJoin(directory, "%s%s" % (prefix, suffix))
135
  counter = 1
136
  while True:
137
    try:
138
      os.link(old_path, new_path)
139
      break
140
    except OSError, err:
141
      if err.errno == errno.EEXIST:
142
        new_path = utils.PathJoin(directory,
143
          "%s_%s%s" % (prefix, counter, suffix))
144
        counter += 1
145
      else:
146
        raise errors.OpPrereqError("Error moving the file %s to %s location:"
147
                                   " %s" % (old_path, new_path, err))
148
  return new_path
149

    
150

    
151
class OVFReader(object):
152
  """Reader class for OVF files.
153

154
  @type files_list: list
155
  @ivar files_list: list of files in the OVF package
156
  @type tree: ET.ElementTree
157
  @ivar tree: XML tree of the .ovf file
158
  @type schema_name: string
159
  @ivar schema_name: name of the .ovf file
160
  @type input_dir: string
161
  @ivar input_dir: directory in which the .ovf file resides
162

163
  """
164
  def __init__(self, input_path):
165
    """Initialiaze the reader - load the .ovf file to XML parser.
166

167
    It is assumed that names of manifesto (.mf), certificate (.cert) and ovf
168
    files are the same. In order to account any other files as part of the ovf
169
    package, they have to be explicitly mentioned in the Resources section
170
    of the .ovf file.
171

172
    @type input_path: string
173
    @param input_path: absolute path to the .ovf file
174

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

178
    """
179
    self.tree = ET.ElementTree()
180
    try:
181
      self.tree.parse(input_path)
182
    except xml.parsers.expat.ExpatError, err:
183
      raise errors.OpPrereqError("Error while reading %s file: %s" %
184
                                 (OVF_EXT, err))
185

    
186
    # Create a list of all files in the OVF package
187
    (input_dir, input_file) = os.path.split(input_path)
188
    (input_name, _) = os.path.splitext(input_file)
189
    files_directory = utils.ListVisibleFiles(input_dir)
190
    files_list = []
191
    for file_name in files_directory:
192
      (name, extension) = os.path.splitext(file_name)
193
      if extension in FILE_EXTENSIONS and name == input_name:
194
        files_list.append(file_name)
195
    files_list += self._GetAttributes("{%s}References/{%s}File" %
196
                                      (OVF_SCHEMA, OVF_SCHEMA),
197
                                      "{%s}href" % OVF_SCHEMA)
198
    for file_name in files_list:
199
      file_path = utils.PathJoin(input_dir, file_name)
200
      if not os.path.exists(file_path):
201
        raise errors.OpPrereqError("File does not exist: %s" % file_path)
202
    logging.info("Files in the OVF package: %s", " ".join(files_list))
203
    self.files_list = files_list
204
    self.input_dir = input_dir
205
    self.schema_name = input_name
206

    
207
  def _GetAttributes(self, path, attribute):
208
    """Get specified attribute from all nodes accessible using given path.
209

210
    Function follows the path from root node to the desired tags using path,
211
    then reads the apropriate attribute values.
212

213
    @type path: string
214
    @param path: path of nodes to visit
215
    @type attribute: string
216
    @param attribute: attribute for which we gather the information
217
    @rtype: list
218
    @return: for each accessible tag with the attribute value set, value of the
219
      attribute
220

221
    """
222
    current_list = self.tree.findall(path)
223
    results = [x.get(attribute) for x in current_list]
224
    return filter(None, results)
225

    
226
  def _GetElementMatchingAttr(self, path, match_attr):
227
    """Searches for element on a path that matches certain attribute value.
228

229
    Function follows the path from root node to the desired tags using path,
230
    then searches for the first one matching the attribute value.
231

232
    @type path: string
233
    @param path: path of nodes to visit
234
    @type match_attr: tuple
235
    @param match_attr: pair (attribute, value) for which we search
236
    @rtype: ET.ElementTree or None
237
    @return: first element matching match_attr or None if nothing matches
238

239
    """
240
    potential_elements = self.tree.findall(path)
241
    (attr, val) = match_attr
242
    for elem in potential_elements:
243
      if elem.get(attr) == val:
244
        return elem
245
    return None
246

    
247
  def _GetElementMatchingText(self, path, match_text):
248
    """Searches for element on a path that matches certain text value.
249

250
    Function follows the path from root node to the desired tags using path,
251
    then searches for the first one matching the text value.
252

253
    @type path: string
254
    @param path: path of nodes to visit
255
    @type match_text: tuple
256
    @param match_text: pair (node, text) for which we search
257
    @rtype: ET.ElementTree or None
258
    @return: first element matching match_text or None if nothing matches
259

260
    """
261
    potential_elements = self.tree.findall(path)
262
    (node, text) = match_text
263
    for elem in potential_elements:
264
      if elem.findtext(node) == text:
265
        return elem
266
    return None
267

    
268
  @staticmethod
269
  def _GetDictParameters(root, schema):
270
    """Reads text in all children and creates the dictionary from the contents.
271

272
    @type root: ET.ElementTree or None
273
    @param root: father of the nodes we want to collect data about
274
    @type schema: string
275
    @param schema: schema name to be removed from the tag
276
    @rtype: dict
277
    @return: dictionary containing tags and their text contents, tags have their
278
      schema fragment removed or empty dictionary, when root is None
279

280
    """
281
    if not root:
282
      return {}
283
    results = {}
284
    for element in list(root):
285
      pref_len = len("{%s}" % schema)
286
      assert(schema in element.tag)
287
      tag = element.tag[pref_len:]
288
      results[tag] = element.text
289
    return results
290

    
291
  def VerifyManifest(self):
292
    """Verifies manifest for the OVF package, if one is given.
293

294
    @raise errors.OpPrereqError: if SHA1 checksums do not match
295

296
    """
297
    if "%s%s" % (self.schema_name, MF_EXT) in self.files_list:
298
      logging.warning("Verifying SHA1 checksums, this may take a while")
299
      manifest_filename = "%s%s" % (self.schema_name, MF_EXT)
300
      manifest_path = utils.PathJoin(self.input_dir, manifest_filename)
301
      manifest_content = utils.ReadFile(manifest_path).splitlines()
302
      manifest_files = {}
303
      regexp = r"SHA1\((\S+)\)= (\S+)"
304
      for line in manifest_content:
305
        match = re.match(regexp, line)
306
        if match:
307
          file_name = match.group(1)
308
          sha1_sum = match.group(2)
309
          manifest_files[file_name] = sha1_sum
310
      files_with_paths = [utils.PathJoin(self.input_dir, file_name)
311
        for file_name in self.files_list]
312
      sha1_sums = utils.FingerprintFiles(files_with_paths)
313
      for file_name, value in manifest_files.iteritems():
314
        if sha1_sums.get(utils.PathJoin(self.input_dir, file_name)) != value:
315
          raise errors.OpPrereqError("SHA1 checksum of %s does not match the"
316
                                     " value in manifest file" % file_name)
317
      logging.info("SHA1 checksums verified")
318

    
319
  def GetInstanceName(self):
320
    """Provides information about instance name.
321

322
    @rtype: string
323
    @return: instance name string
324

325
    """
326
    find_name = "{%s}VirtualSystem/{%s}Name" % (OVF_SCHEMA, OVF_SCHEMA)
327
    return self.tree.findtext(find_name)
328

    
329
  def GetDiskTemplate(self):
330
    """Returns disk template from .ovf file
331

332
    @rtype: string or None
333
    @return: name of the template
334
    """
335
    find_template = ("{%s}GanetiSection/{%s}DiskTemplate" %
336
                     (GANETI_SCHEMA, GANETI_SCHEMA))
337
    return self.tree.findtext(find_template)
338

    
339
  def GetHypervisorData(self):
340
    """Provides hypervisor information - hypervisor name and options.
341

342
    @rtype: dict
343
    @return: dictionary containing name of the used hypervisor and all the
344
      specified options
345

346
    """
347
    hypervisor_search = ("{%s}GanetiSection/{%s}Hypervisor" %
348
                         (GANETI_SCHEMA, GANETI_SCHEMA))
349
    hypervisor_data = self.tree.find(hypervisor_search)
350
    if not hypervisor_data:
351
      return {"hypervisor_name": constants.VALUE_AUTO}
352
    results = {
353
      "hypervisor_name": hypervisor_data.findtext("{%s}Name" % GANETI_SCHEMA,
354
                           default=constants.VALUE_AUTO),
355
    }
356
    parameters = hypervisor_data.find("{%s}Parameters" % GANETI_SCHEMA)
357
    results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
358
    return results
359

    
360
  def GetOSData(self):
361
    """ Provides operating system information - os name and options.
362

363
    @rtype: dict
364
    @return: dictionary containing name and options for the chosen OS
365

366
    """
367
    results = {}
368
    os_search = ("{%s}GanetiSection/{%s}OperatingSystem" %
369
                 (GANETI_SCHEMA, GANETI_SCHEMA))
370
    os_data = self.tree.find(os_search)
371
    if os_data:
372
      results["os_name"] = os_data.findtext("{%s}Name" % GANETI_SCHEMA)
373
      parameters = os_data.find("{%s}Parameters" % GANETI_SCHEMA)
374
      results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
375
    return results
376

    
377
  def GetBackendData(self):
378
    """ Provides backend information - vcpus, memory, auto balancing options.
379

380
    @rtype: dict
381
    @return: dictionary containing options for vcpus, memory and auto balance
382
      settings
383

384
    """
385
    results = {}
386

    
387
    find_vcpus = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item" %
388
                   (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
389
    match_vcpus = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["vcpus"])
390
    vcpus = self._GetElementMatchingText(find_vcpus, match_vcpus)
391
    if vcpus:
392
      vcpus_count = vcpus.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
393
        default=constants.VALUE_AUTO)
394
    else:
395
      vcpus_count = constants.VALUE_AUTO
396
    results["vcpus"] = str(vcpus_count)
397

    
398
    find_memory = find_vcpus
399
    match_memory = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["memory"])
400
    memory = self._GetElementMatchingText(find_memory, match_memory)
401
    memory_raw = None
402
    if memory:
403
      alloc_units = memory.findtext("{%s}AllocationUnits" % RASD_SCHEMA)
404
      matching_units = [units for units, variants in
405
        ALLOCATION_UNITS.iteritems() if alloc_units.lower() in variants]
406
      if matching_units == []:
407
        raise errors.OpPrereqError("Unit %s for RAM memory unknown",
408
          alloc_units)
409
      units = matching_units[0]
410
      memory_raw = int(memory.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
411
            default=constants.VALUE_AUTO))
412
      memory_count = CONVERT_UNITS_TO_MB[units](memory_raw)
413
    else:
414
      memory_count = constants.VALUE_AUTO
415
    results["memory"] = str(memory_count)
416

    
417
    find_balance = ("{%s}GanetiSection/{%s}AutoBalance" %
418
                   (GANETI_SCHEMA, GANETI_SCHEMA))
419
    balance = self.tree.findtext(find_balance, default=constants.VALUE_AUTO)
420
    results["auto_balance"] = balance
421

    
422
    return results
423

    
424
  def GetTagsData(self):
425
    """Provides tags information for instance.
426

427
    @rtype: string or None
428
    @return: string of comma-separated tags for the instance
429

430
    """
431
    find_tags = "{%s}GanetiSection/{%s}Tags" % (GANETI_SCHEMA, GANETI_SCHEMA)
432
    results = self.tree.findtext(find_tags)
433
    if results:
434
      return results
435
    else:
436
      return None
437

    
438
  def GetVersionData(self):
439
    """Provides version number read from .ovf file
440

441
    @rtype: string
442
    @return: string containing the version number
443

444
    """
445
    find_version = ("{%s}GanetiSection/{%s}Version" %
446
                    (GANETI_SCHEMA, GANETI_SCHEMA))
447
    return self.tree.findtext(find_version)
448

    
449
  def GetNetworkData(self):
450
    """Provides data about the network in the OVF instance.
451

452
    The method gathers the data about networks used by OVF instance. It assumes
453
    that 'name' tag means something - in essence, if it contains one of the
454
    words 'bridged' or 'routed' then that will be the mode of this network in
455
    Ganeti. The information about the network can be either in GanetiSection or
456
    VirtualHardwareSection.
457

458
    @rtype: dict
459
    @return: dictionary containing all the network information
460

461
    """
462
    results = {}
463
    networks_search = ("{%s}NetworkSection/{%s}Network" %
464
                       (OVF_SCHEMA, OVF_SCHEMA))
465
    network_names = self._GetAttributes(networks_search,
466
      "{%s}name" % OVF_SCHEMA)
467
    required = ["ip", "mac", "link", "mode"]
468
    for (counter, network_name) in enumerate(network_names):
469
      network_search = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item"
470
                        % (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
471
      ganeti_search = ("{%s}GanetiSection/{%s}Network/{%s}Nic" %
472
                       (GANETI_SCHEMA, GANETI_SCHEMA, GANETI_SCHEMA))
473
      network_match = ("{%s}Connection" % RASD_SCHEMA, network_name)
474
      ganeti_match = ("{%s}name" % OVF_SCHEMA, network_name)
475
      network_data = self._GetElementMatchingText(network_search, network_match)
476
      network_ganeti_data = self._GetElementMatchingAttr(ganeti_search,
477
        ganeti_match)
478

    
479
      ganeti_data = {}
480
      if network_ganeti_data:
481
        ganeti_data["mode"] = network_ganeti_data.findtext("{%s}Mode" %
482
                                                           GANETI_SCHEMA)
483
        ganeti_data["mac"] = network_ganeti_data.findtext("{%s}MACAddress" %
484
                                                          GANETI_SCHEMA)
485
        ganeti_data["ip"] = network_ganeti_data.findtext("{%s}IPAddress" %
486
                                                         GANETI_SCHEMA)
487
        ganeti_data["link"] = network_ganeti_data.findtext("{%s}Link" %
488
                                                           GANETI_SCHEMA)
489
      mac_data = None
490
      if network_data:
491
        mac_data = network_data.findtext("{%s}Address" % RASD_SCHEMA)
492

    
493
      network_name = network_name.lower()
494

    
495
      # First, some not Ganeti-specific information is collected
496
      if constants.NIC_MODE_BRIDGED in network_name:
497
        results["nic%s_mode" % counter] = "bridged"
498
      elif constants.NIC_MODE_ROUTED in network_name:
499
        results["nic%s_mode" % counter] = "routed"
500
      results["nic%s_mac" % counter] = mac_data
501

    
502
      # GanetiSection data overrides 'manually' collected data
503
      for name, value in ganeti_data.iteritems():
504
        results["nic%s_%s" % (counter, name)] = value
505

    
506
      # Bridged network has no IP - unless specifically stated otherwise
507
      if (results.get("nic%s_mode" % counter) == "bridged" and
508
          not results.get("nic%s_ip" % counter)):
509
        results["nic%s_ip" % counter] = constants.VALUE_NONE
510

    
511
      for option in required:
512
        if not results.get("nic%s_%s" % (counter, option)):
513
          results["nic%s_%s" % (counter, option)] = constants.VALUE_AUTO
514

    
515
    if network_names:
516
      results["nic_count"] = str(len(network_names))
517
    return results
518

    
519
  def GetDisksNames(self):
520
    """Provides list of file names for the disks used by the instance.
521

522
    @rtype: list
523
    @return: list of file names, as referenced in .ovf file
524

525
    """
526
    results = []
527
    disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA)
528
    disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA)
529
    for disk in disk_ids:
530
      disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA)
531
      disk_match = ("{%s}id" % OVF_SCHEMA, disk)
532
      disk_elem = self._GetElementMatchingAttr(disk_search, disk_match)
533
      if disk_elem is None:
534
        raise errors.OpPrereqError("%s file corrupted - disk %s not found in"
535
                                   " references" % (OVF_EXT, disk))
536
      disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA)
537
      disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA)
538
      results.append((disk_name, disk_compression))
539
    return results
540

    
541

    
542
class OVFWriter(object):
543
  """Writer class for OVF files.
544

545
  @type tree: ET.ElementTree
546
  @ivar tree: XML tree that we are constructing
547

548
  """
549
  def __init__(self, has_gnt_section):
550
    """Initialize the writer - set the top element.
551

552
    @type has_gnt_section: bool
553
    @param has_gnt_section: if the Ganeti schema should be added - i.e. this
554
      means that Ganeti section will be present
555

556
    """
557
    env_attribs = {
558
      "xmlns:xsi": XML_SCHEMA,
559
      "xmlns:vssd": VSSD_SCHEMA,
560
      "xmlns:rasd": RASD_SCHEMA,
561
      "xmlns:ovf": OVF_SCHEMA,
562
      "xmlns": OVF_SCHEMA,
563
      "xml:lang": "en-US",
564
    }
565
    if has_gnt_section:
566
      env_attribs["xmlns:gnt"] = GANETI_SCHEMA
567
    self.tree = ET.Element("Envelope", attrib=env_attribs)
568

    
569
  def PrettyXmlDump(self):
570
    """Formatter of the XML file.
571

572
    @rtype: string
573
    @return: XML tree in the form of nicely-formatted string
574

575
    """
576
    raw_string = ET.tostring(self.tree)
577
    parsed_xml = xml.dom.minidom.parseString(raw_string)
578
    xml_string = parsed_xml.toprettyxml(indent="  ")
579
    text_re = re.compile(">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL)
580
    return text_re.sub(">\g<1></", xml_string)
581

    
582

    
583
class Converter(object):
584
  """Converter class for OVF packages.
585

586
  Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is
587
  to provide a common interface for the two.
588

589
  @type options: optparse.Values
590
  @ivar options: options parsed from the command line
591
  @type output_dir: string
592
  @ivar output_dir: directory to which the results of conversion shall be
593
    written
594
  @type temp_file_manager: L{utils.TemporaryFileManager}
595
  @ivar temp_file_manager: container for temporary files created during
596
    conversion
597
  @type temp_dir: string
598
  @ivar temp_dir: temporary directory created then we deal with OVA
599

600
  """
601
  def __init__(self, input_path, options):
602
    """Initialize the converter.
603

604
    @type input_path: string
605
    @param input_path: path to the Converter input file
606
    @type options: optparse.Values
607
    @param options: command line options
608

609
    @raise errors.OpPrereqError: if file does not exist
610

611
    """
612
    input_path = os.path.abspath(input_path)
613
    if not os.path.isfile(input_path):
614
      raise errors.OpPrereqError("File does not exist: %s" % input_path)
615
    self.options = options
616
    self.temp_file_manager = utils.TemporaryFileManager()
617
    self.temp_dir = None
618
    self.output_dir = None
619
    self._ReadInputData(input_path)
620

    
621
  def _ReadInputData(self, input_path):
622
    """Reads the data on which the conversion will take place.
623

624
    @type input_path: string
625
    @param input_path: absolute path to the Converter input file
626

627
    """
628
    raise NotImplementedError()
629

    
630
  def _CompressDisk(self, disk_path, compression, action):
631
    """Performs (de)compression on the disk and returns the new path
632

633
    @type disk_path: string
634
    @param disk_path: path to the disk
635
    @type compression: string
636
    @param compression: compression type
637
    @type action: string
638
    @param action: whether the action is compression or decompression
639
    @rtype: string
640
    @return: new disk path after (de)compression
641

642
    @raise errors.OpPrereqError: disk (de)compression failed or "compression"
643
      is not supported
644

645
    """
646
    assert(action in ALLOWED_ACTIONS)
647
    # For now we only support gzip, as it is used in ovftool
648
    if compression != COMPRESSION_TYPE:
649
      raise errors.OpPrereqError("Unsupported compression type: %s"
650
                                 % compression)
651
    disk_file = os.path.basename(disk_path)
652
    if action == DECOMPRESS:
653
      (disk_name, _) = os.path.splitext(disk_file)
654
      prefix = disk_name
655
    elif action == COMPRESS:
656
      prefix = disk_file
657
    new_path = utils.GetClosedTempfile(suffix=COMPRESSION_EXT, prefix=prefix,
658
      dir=self.output_dir)
659
    self.temp_file_manager.Add(new_path)
660
    args = ["gzip", "-c", disk_path]
661
    run_result = utils.RunCmd(args, output=new_path)
662
    if run_result.failed:
663
      raise errors.OpPrereqError("Disk %s failed with output: %s"
664
                                 % (action, run_result.stderr))
665
    logging.info("The %s of the disk is completed", action)
666
    return (COMPRESSION_EXT, new_path)
667

    
668
  def _ConvertDisk(self, disk_format, disk_path):
669
    """Performes conversion to specified format.
670

671
    @type disk_format: string
672
    @param disk_format: format to which the disk should be converted
673
    @type disk_path: string
674
    @param disk_path: path to the disk that should be converted
675
    @rtype: string
676
    @return path to the output disk
677

678
    @raise errors.OpPrereqError: convertion of the disk failed
679

680
    """
681
    disk_file = os.path.basename(disk_path)
682
    (disk_name, disk_extension) = os.path.splitext(disk_file)
683
    if disk_extension != disk_format:
684
      logging.warning("Conversion of disk image to %s format, this may take"
685
                      " a while", disk_format)
686

    
687
    new_disk_path = utils.GetClosedTempfile(suffix=".%s" % disk_format,
688
      prefix=disk_name, dir=self.output_dir)
689
    self.temp_file_manager.Add(new_disk_path)
690
    args = [
691
      "qemu-img",
692
      "convert",
693
      "-O",
694
      disk_format,
695
      disk_path,
696
      new_disk_path,
697
    ]
698
    run_result = utils.RunCmd(args, cwd=os.getcwd())
699
    if run_result.failed:
700
      raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was"
701
                                 ": %s" % (disk_format, run_result.stderr))
702
    return (".%s" % disk_format, new_disk_path)
703

    
704
  @staticmethod
705
  def _GetDiskQemuInfo(disk_path, regexp):
706
    """Figures out some information of the disk using qemu-img.
707

708
    @type disk_path: string
709
    @param disk_path: path to the disk we want to know the format of
710
    @type regexp: string
711
    @param regexp: string that has to be matched, it has to contain one group
712
    @rtype: string
713
    @return: disk format
714

715
    @raise errors.OpPrereqError: format information cannot be retrieved
716

717
    """
718
    args = ["qemu-img", "info", disk_path]
719
    run_result = utils.RunCmd(args, cwd=os.getcwd())
720
    if run_result.failed:
721
      raise errors.OpPrereqError("Gathering info about the disk using qemu-img"
722
                                 " failed, output was: %s" % run_result.stderr)
723
    result = run_result.output
724
    regexp = r"%s" % regexp
725
    match = re.search(regexp, result)
726
    if match:
727
      disk_format = match.group(1)
728
    else:
729
      raise errors.OpPrereqError("No file information matching %s found in:"
730
                                 " %s" % (regexp, result))
731
    return disk_format
732

    
733
  def Parse(self):
734
    """Parses the data and creates a structure containing all required info.
735

736
    """
737
    raise NotImplementedError()
738

    
739
  def Save(self):
740
    """Saves the gathered configuration in an apropriate format.
741

742
    """
743
    raise NotImplementedError()
744

    
745
  def Cleanup(self):
746
    """Cleans the temporary directory, if one was created.
747

748
    """
749
    self.temp_file_manager.Cleanup()
750
    if self.temp_dir:
751
      shutil.rmtree(self.temp_dir)
752
      self.temp_dir = None
753

    
754

    
755
class OVFImporter(Converter):
756
  """Converter from OVF to Ganeti config file.
757

758
  @type input_dir: string
759
  @ivar input_dir: directory in which the .ovf file resides
760
  @type output_dir: string
761
  @ivar output_dir: directory to which the results of conversion shall be
762
    written
763
  @type input_path: string
764
  @ivar input_path: complete path to the .ovf file
765
  @type ovf_reader: L{OVFReader}
766
  @ivar ovf_reader: OVF reader instance collects data from .ovf file
767
  @type results_name: string
768
  @ivar results_name: name of imported instance
769
  @type results_template: string
770
  @ivar results_template: disk template read from .ovf file or command line
771
    arguments
772
  @type results_hypervisor: dict
773
  @ivar results_hypervisor: hypervisor information gathered from .ovf file or
774
    command line arguments
775
  @type results_os: dict
776
  @ivar results_os: operating system information gathered from .ovf file or
777
    command line arguments
778
  @type results_backend: dict
779
  @ivar results_backend: backend information gathered from .ovf file or
780
    command line arguments
781
  @type results_tags: string
782
  @ivar results_tags: string containing instance-specific tags
783
  @type results_version: string
784
  @ivar results_version: version as required by Ganeti import
785
  @type results_network: dict
786
  @ivar results_network: network information gathered from .ovf file or command
787
    line arguments
788
  @type results_disk: dict
789
  @ivar results_disk: disk information gathered from .ovf file or command line
790
    arguments
791

792
  """
793
  def _ReadInputData(self, input_path):
794
    """Reads the data on which the conversion will take place.
795

796
    @type input_path: string
797
    @param input_path: absolute path to the .ovf or .ova input file
798

799
    @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
800

801
    """
802
    (input_dir, input_file) = os.path.split(input_path)
803
    (_, input_extension) = os.path.splitext(input_file)
804

    
805
    if input_extension == OVF_EXT:
806
      logging.info("%s file extension found, no unpacking necessary", OVF_EXT)
807
      self.input_dir = input_dir
808
      self.input_path = input_path
809
      self.temp_dir = None
810
    elif input_extension == OVA_EXT:
811
      logging.info("%s file extension found, proceeding to unpacking", OVA_EXT)
812
      self._UnpackOVA(input_path)
813
    else:
814
      raise errors.OpPrereqError("Unknown file extension; expected %s or %s"
815
                                 " file" % (OVA_EXT, OVF_EXT))
816
    assert ((input_extension == OVA_EXT and self.temp_dir) or
817
            (input_extension == OVF_EXT and not self.temp_dir))
818
    assert self.input_dir in self.input_path
819

    
820
    if self.options.output_dir:
821
      self.output_dir = os.path.abspath(self.options.output_dir)
822
      if (os.path.commonprefix([constants.EXPORT_DIR, self.output_dir]) !=
823
          constants.EXPORT_DIR):
824
        logging.warning("Export path is not under %s directory, import to"
825
                        " Ganeti using gnt-backup may fail",
826
                        constants.EXPORT_DIR)
827
    else:
828
      self.output_dir = constants.EXPORT_DIR
829

    
830
    self.ovf_reader = OVFReader(self.input_path)
831
    self.ovf_reader.VerifyManifest()
832

    
833
  def _UnpackOVA(self, input_path):
834
    """Unpacks the .ova package into temporary directory.
835

836
    @type input_path: string
837
    @param input_path: path to the .ova package file
838

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

843
    """
844
    input_name = None
845
    if not tarfile.is_tarfile(input_path):
846
      raise errors.OpPrereqError("The provided %s file is not a proper tar"
847
                                 " archive", OVA_EXT)
848
    ova_content = tarfile.open(input_path)
849
    temp_dir = tempfile.mkdtemp()
850
    self.temp_dir = temp_dir
851
    for file_name in ova_content.getnames():
852
      file_normname = os.path.normpath(file_name)
853
      try:
854
        utils.PathJoin(temp_dir, file_normname)
855
      except ValueError, err:
856
        raise errors.OpPrereqError("File %s inside %s package is not safe" %
857
                                   (file_name, OVA_EXT))
858
      if file_name.endswith(OVF_EXT):
859
        input_name = file_name
860
    if not input_name:
861
      raise errors.OpPrereqError("No %s file in %s package found" %
862
                                 (OVF_EXT, OVA_EXT))
863
    logging.warning("Unpacking the %s archive, this may take a while",
864
      input_path)
865
    self.input_dir = temp_dir
866
    self.input_path = utils.PathJoin(self.temp_dir, input_name)
867
    try:
868
      try:
869
        extract = ova_content.extractall
870
      except AttributeError:
871
        # This is a prehistorical case of using python < 2.5
872
        for member in ova_content.getmembers():
873
          ova_content.extract(member, path=self.temp_dir)
874
      else:
875
        extract(self.temp_dir)
876
    except tarfile.TarError, err:
877
      raise errors.OpPrereqError("Error while extracting %s archive: %s" %
878
                                 (OVA_EXT, err))
879
    logging.info("OVA package extracted to %s directory", self.temp_dir)
880

    
881
  def Parse(self):
882
    """Parses the data and creates a structure containing all required info.
883

884
    The method reads the information given either as a command line option or as
885
    a part of the OVF description.
886

887
    @raise errors.OpPrereqError: if some required part of the description of
888
      virtual instance is missing or unable to create output directory
889

890
    """
891
    self.results_name = self._GetInfo("instance name", self.options.name,
892
      self._ParseNameOptions, self.ovf_reader.GetInstanceName)
893
    if not self.results_name:
894
      raise errors.OpPrereqError("Name of instance not provided")
895

    
896
    self.output_dir = utils.PathJoin(self.output_dir, self.results_name)
897
    try:
898
      utils.Makedirs(self.output_dir)
899
    except OSError, err:
900
      raise errors.OpPrereqError("Failed to create directory %s: %s" %
901
                                 (self.output_dir, err))
902

    
903
    self.results_template = self._GetInfo("disk template",
904
      self.options.disk_template, self._ParseTemplateOptions,
905
      self.ovf_reader.GetDiskTemplate)
906
    if not self.results_template:
907
      logging.info("Disk template not given")
908

    
909
    self.results_hypervisor = self._GetInfo("hypervisor",
910
      self.options.hypervisor, self._ParseHypervisorOptions,
911
      self.ovf_reader.GetHypervisorData)
912
    assert self.results_hypervisor["hypervisor_name"]
913
    if self.results_hypervisor["hypervisor_name"] == constants.VALUE_AUTO:
914
      logging.debug("Default hypervisor settings from the cluster will be used")
915

    
916
    self.results_os = self._GetInfo("OS", self.options.os,
917
      self._ParseOSOptions, self.ovf_reader.GetOSData)
918
    if not self.results_os.get("os_name"):
919
      raise errors.OpPrereqError("OS name must be provided")
920

    
921
    self.results_backend = self._GetInfo("backend", self.options.beparams,
922
      self._ParseBackendOptions, self.ovf_reader.GetBackendData)
923
    assert self.results_backend.get("vcpus")
924
    assert self.results_backend.get("memory")
925
    assert self.results_backend.get("auto_balance") is not None
926

    
927
    self.results_tags = self._GetInfo("tags", self.options.tags,
928
      self._ParseTags, self.ovf_reader.GetTagsData)
929

    
930
    ovf_version = self.ovf_reader.GetVersionData()
931
    if ovf_version:
932
      self.results_version = ovf_version
933
    else:
934
      self.results_version = constants.EXPORT_VERSION
935

    
936
    self.results_network = self._GetInfo("network", self.options.nics,
937
      self._ParseNicOptions, self.ovf_reader.GetNetworkData,
938
      ignore_test=self.options.no_nics)
939

    
940
    self.results_disk = self._GetInfo("disk", self.options.disks,
941
      self._ParseDiskOptions, self._GetDiskInfo,
942
      ignore_test=self.results_template == constants.DT_DISKLESS)
943

    
944
    if not self.results_disk and not self.results_network:
945
      raise errors.OpPrereqError("Either disk specification or network"
946
                                 " description must be present")
947

    
948
  @staticmethod
949
  def _GetInfo(name, cmd_arg, cmd_function, nocmd_function,
950
    ignore_test=False):
951
    """Get information about some section - e.g. disk, network, hypervisor.
952

953
    @type name: string
954
    @param name: name of the section
955
    @type cmd_arg: dict
956
    @param cmd_arg: command line argument specific for section 'name'
957
    @type cmd_function: callable
958
    @param cmd_function: function to call if 'cmd_args' exists
959
    @type nocmd_function: callable
960
    @param nocmd_function: function to call if 'cmd_args' is not there
961

962
    """
963
    if ignore_test:
964
      logging.info("Information for %s will be ignored", name)
965
      return {}
966
    if cmd_arg:
967
      logging.info("Information for %s will be parsed from command line", name)
968
      results = cmd_function()
969
    else:
970
      logging.info("Information for %s will be parsed from %s file",
971
        name, OVF_EXT)
972
      results = nocmd_function()
973
    logging.info("Options for %s were succesfully read", name)
974
    return results
975

    
976
  def _ParseNameOptions(self):
977
    """Returns name if one was given in command line.
978

979
    @rtype: string
980
    @return: name of an instance
981

982
    """
983
    return self.options.name
984

    
985
  def _ParseTemplateOptions(self):
986
    """Returns disk template if one was given in command line.
987

988
    @rtype: string
989
    @return: disk template name
990

991
    """
992
    return self.options.disk_template
993

    
994
  def _ParseHypervisorOptions(self):
995
    """Parses hypervisor options given in a command line.
996

997
    @rtype: dict
998
    @return: dictionary containing name of the chosen hypervisor and all the
999
      options
1000

1001
    """
1002
    assert type(self.options.hypervisor) is tuple
1003
    assert len(self.options.hypervisor) == 2
1004
    results = {}
1005
    if self.options.hypervisor[0]:
1006
      results["hypervisor_name"] = self.options.hypervisor[0]
1007
    else:
1008
      results["hypervisor_name"] = constants.VALUE_AUTO
1009
    results.update(self.options.hypervisor[1])
1010
    return results
1011

    
1012
  def _ParseOSOptions(self):
1013
    """Parses OS options given in command line.
1014

1015
    @rtype: dict
1016
    @return: dictionary containing name of chosen OS and all its options
1017

1018
    """
1019
    assert self.options.os
1020
    results = {}
1021
    results["os_name"] = self.options.os
1022
    results.update(self.options.osparams)
1023
    return results
1024

    
1025
  def _ParseBackendOptions(self):
1026
    """Parses backend options given in command line.
1027

1028
    @rtype: dict
1029
    @return: dictionary containing vcpus, memory and auto-balance options
1030

1031
    """
1032
    assert self.options.beparams
1033
    backend = {}
1034
    backend.update(self.options.beparams)
1035
    must_contain = ["vcpus", "memory", "auto_balance"]
1036
    for element in must_contain:
1037
      if backend.get(element) is None:
1038
        backend[element] = constants.VALUE_AUTO
1039
    return backend
1040

    
1041
  def _ParseTags(self):
1042
    """Returns tags list given in command line.
1043

1044
    @rtype: string
1045
    @return: string containing comma-separated tags
1046

1047
    """
1048
    return self.options.tags
1049

    
1050
  def _ParseNicOptions(self):
1051
    """Parses network options given in a command line or as a dictionary.
1052

1053
    @rtype: dict
1054
    @return: dictionary of network-related options
1055

1056
    """
1057
    assert self.options.nics
1058
    results = {}
1059
    for (nic_id, nic_desc) in self.options.nics:
1060
      results["nic%s_mode" % nic_id] = \
1061
        nic_desc.get("mode", constants.VALUE_AUTO)
1062
      results["nic%s_mac" % nic_id] = nic_desc.get("mac", constants.VALUE_AUTO)
1063
      results["nic%s_link" % nic_id] = \
1064
        nic_desc.get("link", constants.VALUE_AUTO)
1065
      if nic_desc.get("mode") == "bridged":
1066
        results["nic%s_ip" % nic_id] = constants.VALUE_NONE
1067
      else:
1068
        results["nic%s_ip" % nic_id] = constants.VALUE_AUTO
1069
    results["nic_count"] = str(len(self.options.nics))
1070
    return results
1071

    
1072
  def _ParseDiskOptions(self):
1073
    """Parses disk options given in a command line.
1074

1075
    @rtype: dict
1076
    @return: dictionary of disk-related options
1077

1078
    @raise errors.OpPrereqError: disk description does not contain size
1079
      information or size information is invalid or creation failed
1080

1081
    """
1082
    assert self.options.disks
1083
    results = {}
1084
    for (disk_id, disk_desc) in self.options.disks:
1085
      results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id
1086
      if disk_desc.get("size"):
1087
        try:
1088
          disk_size = utils.ParseUnit(disk_desc["size"])
1089
        except ValueError:
1090
          raise errors.OpPrereqError("Invalid disk size for disk %s: %s" %
1091
                                     (disk_id, disk_desc["size"]))
1092
        new_path = utils.PathJoin(self.output_dir, str(disk_id))
1093
        args = [
1094
          "qemu-img",
1095
          "create",
1096
          "-f",
1097
          "raw",
1098
          new_path,
1099
          disk_size,
1100
        ]
1101
        run_result = utils.RunCmd(args)
1102
        if run_result.failed:
1103
          raise errors.OpPrereqError("Creation of disk %s failed, output was:"
1104
                                     " %s" % (new_path, run_result.stderr))
1105
        results["disk%s_size" % disk_id] = str(disk_size)
1106
        results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id
1107
      else:
1108
        raise errors.OpPrereqError("Disks created for import must have their"
1109
                                   " size specified")
1110
    results["disk_count"] = str(len(self.options.disks))
1111
    return results
1112

    
1113
  def _GetDiskInfo(self):
1114
    """Gathers information about disks used by instance, perfomes conversion.
1115

1116
    @rtype: dict
1117
    @return: dictionary of disk-related options
1118

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

1121
    """
1122
    results = {}
1123
    disks_list = self.ovf_reader.GetDisksNames()
1124
    for (counter, (disk_name, disk_compression)) in enumerate(disks_list):
1125
      if os.path.dirname(disk_name):
1126
        raise errors.OpPrereqError("Disks are not allowed to have absolute"
1127
                                   " paths or paths outside main OVF directory")
1128
      disk, _ = os.path.splitext(disk_name)
1129
      disk_path = utils.PathJoin(self.input_dir, disk_name)
1130
      if disk_compression:
1131
        _, disk_path = self._CompressDisk(disk_path, disk_compression,
1132
          DECOMPRESS)
1133
        disk, _ = os.path.splitext(disk)
1134
      if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw":
1135
        logging.info("Conversion to raw format is required")
1136
      ext, new_disk_path = self._ConvertDisk("raw", disk_path)
1137

    
1138
      final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext,
1139
        directory=self.output_dir)
1140
      final_name = os.path.basename(final_disk_path)
1141
      disk_size = os.path.getsize(final_disk_path) / (1024 * 1024)
1142
      results["disk%s_dump" % counter] = final_name
1143
      results["disk%s_size" % counter] = str(disk_size)
1144
      results["disk%s_ivname" % counter] = "disk/%s" % str(counter)
1145
    if disks_list:
1146
      results["disk_count"] = str(len(disks_list))
1147
    return results
1148

    
1149
  def Save(self):
1150
    """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
1151

1152
    @raise errors.OpPrereqError: when saving to config file failed
1153

1154
    """
1155
    logging.info("Conversion was succesfull, saving %s in %s directory",
1156
                 constants.EXPORT_CONF_FILE, self.output_dir)
1157
    results = {
1158
      constants.INISECT_INS: {},
1159
      constants.INISECT_BEP: {},
1160
      constants.INISECT_EXP: {},
1161
      constants.INISECT_OSP: {},
1162
      constants.INISECT_HYP: {},
1163
    }
1164

    
1165
    results[constants.INISECT_INS].update(self.results_disk)
1166
    results[constants.INISECT_INS].update(self.results_network)
1167
    results[constants.INISECT_INS]["hypervisor"] = \
1168
      self.results_hypervisor["hypervisor_name"]
1169
    results[constants.INISECT_INS]["name"] = self.results_name
1170
    if self.results_template:
1171
      results[constants.INISECT_INS]["disk_template"] = self.results_template
1172
    if self.results_tags:
1173
      results[constants.INISECT_INS]["tags"] = self.results_tags
1174

    
1175
    results[constants.INISECT_BEP].update(self.results_backend)
1176

    
1177
    results[constants.INISECT_EXP]["os"] = self.results_os["os_name"]
1178
    results[constants.INISECT_EXP]["version"] = self.results_version
1179

    
1180
    del self.results_os["os_name"]
1181
    results[constants.INISECT_OSP].update(self.results_os)
1182

    
1183
    del self.results_hypervisor["hypervisor_name"]
1184
    results[constants.INISECT_HYP].update(self.results_hypervisor)
1185

    
1186
    output_file_name = utils.PathJoin(self.output_dir,
1187
      constants.EXPORT_CONF_FILE)
1188

    
1189
    output = []
1190
    for section, options in results.iteritems():
1191
      output.append("[%s]" % section)
1192
      for name, value in options.iteritems():
1193
        if value is None:
1194
          value = ""
1195
        output.append("%s = %s" % (name, value))
1196
      output.append("")
1197
    output_contents = "\n".join(output)
1198

    
1199
    try:
1200
      utils.WriteFile(output_file_name, data=output_contents)
1201
    except errors.ProgrammerError, err:
1202
      raise errors.OpPrereqError("Saving the config file failed: %s" % err)
1203

    
1204
    self.Cleanup()
1205

    
1206

    
1207
class ConfigParserWithDefaults(ConfigParser.SafeConfigParser):
1208
  """This is just a wrapper on SafeConfigParser, that uses default values
1209

1210
  """
1211
  def get(self, section, options, raw=None, vars=None): # pylint: disable=W0622
1212
    try:
1213
      result = ConfigParser.SafeConfigParser.get(self, section, options, \
1214
        raw=raw, vars=vars)
1215
    except ConfigParser.NoOptionError:
1216
      result = None
1217
    return result
1218

    
1219
  def getint(self, section, options):
1220
    try:
1221
      result = ConfigParser.SafeConfigParser.get(self, section, options)
1222
    except ConfigParser.NoOptionError:
1223
      result = 0
1224
    return int(result)
1225

    
1226

    
1227
class OVFExporter(Converter):
1228
  """Converter from Ganeti config file to OVF
1229

1230
  @type input_dir: string
1231
  @ivar input_dir: directory in which the config.ini file resides
1232
  @type output_dir: string
1233
  @ivar output_dir: directory to which the results of conversion shall be
1234
    written
1235
  @type packed_dir: string
1236
  @ivar packed_dir: if we want OVA package, this points to the real (i.e. not
1237
    temp) output directory
1238
  @type input_path: string
1239
  @ivar input_path: complete path to the config.ini file
1240
  @type output_path: string
1241
  @ivar output_path: complete path to .ovf file
1242
  @type config_parser: L{ConfigParserWithDefaults}
1243
  @ivar config_parser: parser for the config.ini file
1244
  @type results_disk: list
1245
  @ivar results_disk: list of dictionaries of disk options from config.ini
1246
  @type results_network: list
1247
  @ivar results_network: list of dictionaries of network options form config.ini
1248
  @type results_name: string
1249
  @ivar results_name: name of the instance
1250
  @type results_vcpus: string
1251
  @ivar results_vcpus: number of VCPUs
1252
  @type results_memory: string
1253
  @ivar results_memory: RAM memory in MB
1254
  @type results_ganeti: dict
1255
  @ivar results_ganeti: dictionary of Ganeti-specific options from config.ini
1256

1257
  """
1258
  def _ReadInputData(self, input_path):
1259
    """Reads the data on which the conversion will take place.
1260

1261
    @type input_path: string
1262
    @param input_path: absolute path to the config.ini input file
1263

1264
    @raise errors.OpPrereqError: error when reading the config file
1265

1266
    """
1267
    input_dir = os.path.dirname(input_path)
1268
    self.input_path = input_path
1269
    self.input_dir = input_dir
1270
    if self.options.output_dir:
1271
      self.output_dir = os.path.abspath(self.options.output_dir)
1272
    else:
1273
      self.output_dir = input_dir
1274
    self.config_parser = ConfigParserWithDefaults()
1275
    logging.info("Reading configuration from %s file", input_path)
1276
    try:
1277
      self.config_parser.read(input_path)
1278
    except ConfigParser.MissingSectionHeaderError, err:
1279
      raise errors.OpPrereqError("Error when trying to read %s: %s" %
1280
                                 (input_path, err))
1281
    if self.options.ova_package:
1282
      self.temp_dir = tempfile.mkdtemp()
1283
      self.packed_dir = self.output_dir
1284
      self.output_dir = self.temp_dir
1285

    
1286
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1287

    
1288
  def _ParseName(self):
1289
    """Parses name from command line options or config file.
1290

1291
    @rtype: string
1292
    @return: name of Ganeti instance
1293

1294
    @raise errors.OpPrereqError: if name of the instance is not provided
1295

1296
    """
1297
    if self.options.name:
1298
      name = self.options.name
1299
    else:
1300
      name = self.config_parser.get(constants.INISECT_INS, NAME)
1301
    if name is None:
1302
      raise errors.OpPrereqError("No instance name found")
1303
    return name
1304

    
1305
  def _ParseVCPUs(self):
1306
    """Parses vcpus number from config file.
1307

1308
    @rtype: int
1309
    @return: number of virtual CPUs
1310

1311
    @raise errors.OpPrereqError: if number of VCPUs equals 0
1312

1313
    """
1314
    vcpus = self.config_parser.getint(constants.INISECT_BEP, VCPUS)
1315
    if vcpus == 0:
1316
      raise errors.OpPrereqError("No CPU information found")
1317
    return vcpus
1318

    
1319
  def _ParseMemory(self):
1320
    """Parses vcpus number from config file.
1321

1322
    @rtype: int
1323
    @return: amount of memory in MB
1324

1325
    @raise errors.OpPrereqError: if amount of memory equals 0
1326

1327
    """
1328
    memory = self.config_parser.getint(constants.INISECT_BEP, MEMORY)
1329
    if memory == 0:
1330
      raise errors.OpPrereqError("No memory information found")
1331
    return memory
1332

    
1333
  def _ParseGaneti(self):
1334
    """Parses Ganeti data from config file.
1335

1336
    @rtype: dictionary
1337
    @return: dictionary of Ganeti-specific options
1338

1339
    """
1340
    results = {}
1341
    # hypervisor
1342
    results["hypervisor"] = {}
1343
    hyp_name = self.config_parser.get(constants.INISECT_INS, HYPERV)
1344
    if hyp_name is None:
1345
      raise errors.OpPrereqError("No hypervisor information found")
1346
    results["hypervisor"]["name"] = hyp_name
1347
    pairs = self.config_parser.items(constants.INISECT_HYP)
1348
    for (name, value) in pairs:
1349
      results["hypervisor"][name] = value
1350
    # os
1351
    results["os"] = {}
1352
    os_name = self.config_parser.get(constants.INISECT_EXP, OS)
1353
    if os_name is None:
1354
      raise errors.OpPrereqError("No operating system information found")
1355
    results["os"]["name"] = os_name
1356
    pairs = self.config_parser.items(constants.INISECT_OSP)
1357
    for (name, value) in pairs:
1358
      results["os"][name] = value
1359
    # other
1360
    others = [
1361
      (constants.INISECT_INS, DISK_TEMPLATE, "disk_template"),
1362
      (constants.INISECT_BEP, AUTO_BALANCE, "auto_balance"),
1363
      (constants.INISECT_INS, TAGS, "tags"),
1364
      (constants.INISECT_EXP, VERSION, "version"),
1365
    ]
1366
    for (section, element, name) in others:
1367
      results[name] = self.config_parser.get(section, element)
1368
    return results
1369

    
1370
  def _ParseNetworks(self):
1371
    """Parses network data from config file.
1372

1373
    @rtype: list
1374
    @return: list of dictionaries of network options
1375

1376
    @raise errors.OpPrereqError: then network mode is not recognized
1377

1378
    """
1379
    results = []
1380
    counter = 0
1381
    while True:
1382
      data_link = \
1383
        self.config_parser.get(constants.INISECT_INS, "nic%s_link" % counter)
1384
      if data_link is None:
1385
        break
1386
      results.append({
1387
        "mode": self.config_parser.get(constants.INISECT_INS,
1388
           "nic%s_mode" % counter),
1389
        "mac": self.config_parser.get(constants.INISECT_INS,
1390
           "nic%s_mac" % counter),
1391
        "ip": self.config_parser.get(constants.INISECT_INS,
1392
           "nic%s_ip" % counter),
1393
        "link": data_link,
1394
      })
1395
      if results[counter]["mode"] not in constants.NIC_VALID_MODES:
1396
        raise errors.OpPrereqError("Network mode %s not recognized"
1397
                                   % results[counter]["mode"])
1398
      counter += 1
1399
    return results
1400

    
1401
  def _GetDiskOptions(self, disk_file, compression):
1402
    """Convert the disk and gather disk info for .ovf file.
1403

1404
    @type disk_file: string
1405
    @param disk_file: name of the disk (without the full path)
1406
    @type compression: bool
1407
    @param compression: whether the disk should be compressed or not
1408

1409
    @raise errors.OpPrereqError: when disk image does not exist
1410

1411
    """
1412
    disk_path = utils.PathJoin(self.input_dir, disk_file)
1413
    results = {}
1414
    if not os.path.isfile(disk_path):
1415
      raise errors.OpPrereqError("Disk image does not exist: %s" % disk_path)
1416
    if os.path.dirname(disk_file):
1417
      raise errors.OpPrereqError("Path for the disk: %s contains a directory"
1418
                                 " name" % disk_path)
1419
    disk_name, _ = os.path.splitext(disk_file)
1420
    ext, new_disk_path = self._ConvertDisk(self.options.disk_format, disk_path)
1421
    results["format"] = self.options.disk_format
1422
    results["virt-size"] = self._GetDiskQemuInfo(new_disk_path,
1423
      "virtual size: \S+ \((\d+) bytes\)")
1424
    if compression:
1425
      ext2, new_disk_path = self._CompressDisk(new_disk_path, "gzip",
1426
        COMPRESS)
1427
      disk_name, _ = os.path.splitext(disk_name)
1428
      results["compression"] = "gzip"
1429
      ext += ext2
1430
    final_disk_path = LinkFile(new_disk_path, prefix=disk_name, suffix=ext,
1431
      directory=self.output_dir)
1432
    final_disk_name = os.path.basename(final_disk_path)
1433
    results["real-size"] = os.path.getsize(final_disk_path)
1434
    results["path"] = final_disk_name
1435
    self.references_files.append(final_disk_path)
1436
    return results
1437

    
1438
  def _ParseDisks(self):
1439
    """Parses disk data from config file.
1440

1441
    @rtype: list
1442
    @return: list of dictionaries of disk options
1443

1444
    """
1445
    results = []
1446
    counter = 0
1447
    while True:
1448
      disk_file = \
1449
        self.config_parser.get(constants.INISECT_INS, "disk%s_dump" % counter)
1450
      if disk_file is None:
1451
        break
1452
      results.append(self._GetDiskOptions(disk_file, self.options.compression))
1453
      counter += 1
1454
    return results
1455

    
1456
  def Parse(self):
1457
    """Parses the data and creates a structure containing all required info.
1458

1459
    """
1460
    try:
1461
      utils.Makedirs(self.output_dir)
1462
    except OSError, err:
1463
      raise errors.OpPrereqError("Failed to create directory %s: %s" %
1464
                                 (self.output_dir, err))
1465

    
1466
    self.references_files = []
1467
    self.results_name = self._ParseName()
1468
    self.results_vcpus = self._ParseVCPUs()
1469
    self.results_memory = self._ParseMemory()
1470
    if not self.options.ext_usage:
1471
      self.results_ganeti = self._ParseGaneti()
1472
    self.results_network = self._ParseNetworks()
1473
    self.results_disk = self._ParseDisks()
1474

    
1475
  def _PrepareManifest(self, path):
1476
    """Creates manifest for all the files in OVF package.
1477

1478
    @type path: string
1479
    @param path: path to manifesto file
1480

1481
    @raise errors.OpPrereqError: if error occurs when writing file
1482

1483
    """
1484
    logging.info("Preparing manifest for the OVF package")
1485
    lines = []
1486
    files_list = [self.output_path]
1487
    files_list.extend(self.references_files)
1488
    logging.warning("Calculating SHA1 checksums, this may take a while")
1489
    sha1_sums = utils.FingerprintFiles(files_list)
1490
    for file_path, value in sha1_sums.iteritems():
1491
      file_name = os.path.basename(file_path)
1492
      lines.append("SHA1(%s)= %s" % (file_name, value))
1493
    lines.append("")
1494
    data = "\n".join(lines)
1495
    try:
1496
      utils.WriteFile(path, data=data)
1497
    except errors.ProgrammerError, err:
1498
      raise errors.OpPrereqError("Saving the manifest file failed: %s" % err)
1499

    
1500
  @staticmethod
1501
  def _PrepareTarFile(tar_path, files_list):
1502
    """Creates tarfile from the files in OVF package.
1503

1504
    @type tar_path: string
1505
    @param tar_path: path to the resulting file
1506
    @type files_list: list
1507
    @param files_list: list of files in the OVF package
1508

1509
    """
1510
    logging.info("Preparing tarball for the OVF package")
1511
    open(tar_path, mode="w").close()
1512
    ova_package = tarfile.open(name=tar_path, mode="w")
1513
    for file_name in files_list:
1514
      ova_package.add(file_name)
1515
    ova_package.close()
1516

    
1517
  def Save(self):
1518
    """Saves the gathered configuration in an apropriate format.
1519

1520
    @raise errors.OpPrereqError: if unable to create output directory
1521

1522
    """
1523
    output_file = "%s%s" % (self.results_name, OVF_EXT)
1524
    output_path = utils.PathJoin(self.output_dir, output_file)
1525
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1526
    logging.info("Saving read data to %s", output_path)
1527

    
1528
    self.output_path = utils.PathJoin(self.output_dir, output_file)
1529
    files_list = [self.output_path]
1530

    
1531
    data = self.ovf_writer.PrettyXmlDump()
1532
    utils.WriteFile(self.output_path, data=data)
1533

    
1534
    manifest_file = "%s%s" % (self.results_name, MF_EXT)
1535
    manifest_path = utils.PathJoin(self.output_dir, manifest_file)
1536
    self._PrepareManifest(manifest_path)
1537
    files_list.append(manifest_path)
1538

    
1539
    files_list.extend(self.references_files)
1540

    
1541
    if self.options.ova_package:
1542
      ova_file = "%s%s" % (self.results_name, OVA_EXT)
1543
      packed_path = utils.PathJoin(self.packed_dir, ova_file)
1544
      try:
1545
        utils.Makedirs(self.packed_dir)
1546
      except OSError, err:
1547
        raise errors.OpPrereqError("Failed to create directory %s: %s" %
1548
                                   (self.packed_dir, err))
1549
      self._PrepareTarFile(packed_path, files_list)
1550
    logging.info("Creation of the OVF package was successfull")
1551
    self.Cleanup()