Revision 0963b26a

b/lib/ovf.py
29 29
# E1101 makes no sense - pylint assumes that ElementTree object is a tuple
30 30

  
31 31

  
32
import ConfigParser
32 33
import errno
33 34
import logging
34 35
import os
......
37 38
import shutil
38 39
import tarfile
39 40
import tempfile
41
import xml.dom.minidom
40 42
import xml.parsers.expat
41 43
try:
42 44
  import xml.etree.ElementTree as ET
......
53 55
OVF_SCHEMA = "http://schemas.dmtf.org/ovf/envelope/1"
54 56
RASD_SCHEMA = ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/"
55 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"
56 61

  
57 62
# File extensions in OVF package
58 63
OVA_EXT = ".ova"
......
91 96
  'gb': lambda x: x * 1024,
92 97
}
93 98

  
99
# Names of the config fields
100
NAME = "name"
101

  
94 102

  
95 103
def LinkFile(old_path, prefix=None, suffix=None, directory=None):
96 104
  """Create link with a given prefix and suffix.
......
523 531
    return results
524 532

  
525 533

  
534
class OVFWriter(object):
535
  """Writer class for OVF files.
536

  
537
  @type tree: ET.ElementTree
538
  @ivar tree: XML tree that we are constructing
539

  
540
  """
541
  def __init__(self, has_gnt_section):
542
    """Initialize the writer - set the top element.
543

  
544
    @type has_gnt_section: bool
545
    @param has_gnt_section: if the Ganeti schema should be added - i.e. this
546
      means that Ganeti section will be present
547

  
548
    """
549
    env_attribs = {
550
      "xmlns:xsi": XML_SCHEMA,
551
      "xmlns:vssd": VSSD_SCHEMA,
552
      "xmlns:rasd": RASD_SCHEMA,
553
      "xmlns:ovf": OVF_SCHEMA,
554
      "xmlns": OVF_SCHEMA,
555
      "xml:lang": "en-US",
556
    }
557
    if has_gnt_section:
558
      env_attribs["xmlns:gnt"] = GANETI_SCHEMA
559
    self.tree = ET.Element("Envelope", attrib=env_attribs)
560

  
561
  def PrettyXmlDump(self):
562
    """Formatter of the XML file.
563

  
564
    @rtype: string
565
    @return: XML tree in the form of nicely-formatted string
566

  
567
    """
568
    raw_string = ET.tostring(self.tree)
569
    parsed_xml = xml.dom.minidom.parseString(raw_string)
570
    xml_string = parsed_xml.toprettyxml(indent="  ")
571
    text_re = re.compile(">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL)
572
    return text_re.sub(">\g<1></", xml_string)
573

  
574

  
526 575
class Converter(object):
527 576
  """Converter class for OVF packages.
528 577

  
......
1147 1196
    self.Cleanup()
1148 1197

  
1149 1198

  
1199
class ConfigParserWithDefaults(ConfigParser.SafeConfigParser):
1200
  """This is just a wrapper on SafeConfigParser, that uses default values
1201

  
1202
  """
1203
  def get(self, section, options, raw=None, vars=None): # pylint: disable=W0622
1204
    try:
1205
      result = ConfigParser.SafeConfigParser.get(self, section, options, \
1206
        raw=raw, vars=vars)
1207
    except ConfigParser.NoOptionError:
1208
      result = None
1209
    return result
1210

  
1211
  def getint(self, section, options):
1212
    try:
1213
      result = ConfigParser.SafeConfigParser.get(self, section, options)
1214
    except ConfigParser.NoOptionError:
1215
      result = 0
1216
    return int(result)
1217

  
1218

  
1150 1219
class OVFExporter(Converter):
1220
  """Converter from Ganeti config file to OVF
1221

  
1222
  @type input_dir: string
1223
  @ivar input_dir: directory in which the config.ini file resides
1224
  @type output_dir: string
1225
  @ivar output_dir: directory to which the results of conversion shall be
1226
    written
1227
  @type packed_dir: string
1228
  @ivar packed_dir: if we want OVA package, this points to the real (i.e. not
1229
    temp) output directory
1230
  @type input_path: string
1231
  @ivar input_path: complete path to the config.ini file
1232
  @type output_path: string
1233
  @ivar output_path: complete path to .ovf file
1234
  @type config_parser: L{ConfigParserWithDefaults}
1235
  @ivar config_parser: parser for the config.ini file
1236
  @type results_name: string
1237
  @ivar results_name: name of the instance
1238

  
1239
  """
1151 1240
  def _ReadInputData(self, input_path):
1152
    pass
1241
    """Reads the data on which the conversion will take place.
1242

  
1243
    @type input_path: string
1244
    @param input_path: absolute path to the config.ini input file
1245

  
1246
    @raise errors.OpPrereqError: error when reading the config file
1247

  
1248
    """
1249
    input_dir = os.path.dirname(input_path)
1250
    self.input_path = input_path
1251
    self.input_dir = input_dir
1252
    if self.options.output_dir:
1253
      self.output_dir = os.path.abspath(self.options.output_dir)
1254
    else:
1255
      self.output_dir = input_dir
1256
    self.config_parser = ConfigParserWithDefaults()
1257
    logging.info("Reading configuration from %s file", input_path)
1258
    try:
1259
      self.config_parser.read(input_path)
1260
    except ConfigParser.MissingSectionHeaderError, err:
1261
      raise errors.OpPrereqError("Error when trying to read %s: %s" %
1262
                                 (input_path, err))
1263
    if self.options.ova_package:
1264
      self.temp_dir = tempfile.mkdtemp()
1265
      self.packed_dir = self.output_dir
1266
      self.output_dir = self.temp_dir
1267

  
1268
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1269

  
1270
  def _ParseName(self):
1271
    """Parses name from command line options or config file.
1272

  
1273
    @rtype: string
1274
    @return: name of Ganeti instance
1275

  
1276
    @raise errors.OpPrereqError: if name of the instance is not provided
1277

  
1278
    """
1279
    if self.options.name:
1280
      name = self.options.name
1281
    else:
1282
      name = self.config_parser.get(constants.INISECT_INS, NAME)
1283
    if name is None:
1284
      raise errors.OpPrereqError("No instance name found")
1285
    return name
1153 1286

  
1154 1287
  def Parse(self):
1155
    pass
1288
    """Parses the data and creates a structure containing all required info.
1289

  
1290
    """
1291
    try:
1292
      utils.Makedirs(self.output_dir)
1293
    except OSError, err:
1294
      raise errors.OpPrereqError("Failed to create directory %s: %s" %
1295
                                 (self.output_dir, err))
1296

  
1297
    self.results_name = self._ParseName()
1298

  
1299
  def _PrepareManifest(self, path):
1300
    """Creates manifest for all the files in OVF package.
1301

  
1302
    @type path: string
1303
    @param path: path to manifesto file
1304

  
1305
    @raise errors.OpPrereqError: if error occurs when writing file
1306

  
1307
    """
1308
    logging.info("Preparing manifest for the OVF package")
1309
    lines = []
1310
    files_list = [self.output_path]
1311
    files_list.extend(self.references_files)
1312
    logging.warning("Calculating SHA1 checksums, this may take a while")
1313
    sha1_sums = utils.FingerprintFiles(files_list)
1314
    for file_path, value in sha1_sums.iteritems():
1315
      file_name = os.path.basename(file_path)
1316
      lines.append("SHA1(%s)= %s" % (file_name, value))
1317
    lines.append("")
1318
    data = "\n".join(lines)
1319
    try:
1320
      utils.WriteFile(path, data=data)
1321
    except errors.ProgrammerError, err:
1322
      raise errors.OpPrereqError("Saving the manifest file failed: %s" % err)
1323

  
1324
  @staticmethod
1325
  def _PrepareTarFile(tar_path, files_list):
1326
    """Creates tarfile from the files in OVF package.
1327

  
1328
    @type tar_path: string
1329
    @param tar_path: path to the resulting file
1330
    @type files_list: list
1331
    @param files_list: list of files in the OVF package
1332

  
1333
    """
1334
    logging.info("Preparing tarball for the OVF package")
1335
    open(tar_path, mode="w").close()
1336
    ova_package = tarfile.open(name=tar_path, mode="w")
1337
    for file_name in files_list:
1338
      ova_package.add(file_name)
1339
    ova_package.close()
1156 1340

  
1157 1341
  def Save(self):
1158
    pass
1342
    """Saves the gathered configuration in an apropriate format.
1343

  
1344
    @raise errors.OpPrereqError: if unable to create output directory
1345

  
1346
    """
1347
    output_file = "%s%s" % (self.results_name, OVF_EXT)
1348
    output_path = utils.PathJoin(self.output_dir, output_file)
1349
    self.ovf_writer = OVFWriter(not self.options.ext_usage)
1350
    logging.info("Saving read data to %s", output_path)
1351

  
1352
    self.output_path = utils.PathJoin(self.output_dir, output_file)
1353
    files_list = [self.output_path]
1354

  
1355
    data = self.ovf_writer.PrettyXmlDump()
1356
    utils.WriteFile(self.output_path, data=data)
1357

  
1358
    manifest_file = "%s%s" % (self.results_name, MF_EXT)
1359
    manifest_path = utils.PathJoin(self.output_dir, manifest_file)
1360
    self._PrepareManifest(manifest_path)
1361
    files_list.append(manifest_path)
1362

  
1363
    files_list.extend(self.references_files)
1364

  
1365
    if self.options.ova_package:
1366
      ova_file = "%s%s" % (self.results_name, OVA_EXT)
1367
      packed_path = utils.PathJoin(self.packed_dir, ova_file)
1368
      try:
1369
        utils.Makedirs(self.packed_dir)
1370
      except OSError, err:
1371
        raise errors.OpPrereqError("Failed to create directory %s: %s" %
1372
                                   (self.packed_dir, err))
1373
      self._PrepareTarFile(packed_path, files_list)
1374
    logging.info("Creation of the OVF package was successfull")
1375
    self.Cleanup()

Also available in: Unified diff