From 0963b26a58182da5c2d80b34b4043df85318518e Mon Sep 17 00:00:00 2001
From: Agata Murawska <agatamurawska@google.com>
Date: Mon, 12 Sep 2011 13:17:02 +0200
Subject: [PATCH] Export: initial commit - manifest, ova creation etc

Signed-off-by: Agata Murawska <agatamurawska@google.com>
Reviewed-by: Michael Hanselmann <hansmi@google.com>
---
 lib/ovf.py | 223 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 220 insertions(+), 3 deletions(-)

diff --git a/lib/ovf.py b/lib/ovf.py
index f9a789e9a..9c1651238 100644
--- a/lib/ovf.py
+++ b/lib/ovf.py
@@ -29,6 +29,7 @@
 # E1101 makes no sense - pylint assumes that ElementTree object is a tuple
 
 
+import ConfigParser
 import errno
 import logging
 import os
@@ -37,6 +38,7 @@ import re
 import shutil
 import tarfile
 import tempfile
+import xml.dom.minidom
 import xml.parsers.expat
 try:
   import xml.etree.ElementTree as ET
@@ -53,6 +55,9 @@ GANETI_SCHEMA = "http://ganeti"
 OVF_SCHEMA = "http://schemas.dmtf.org/ovf/envelope/1"
 RASD_SCHEMA = ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/"
                "CIM_ResourceAllocationSettingData")
+VSSD_SCHEMA = ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/"
+               "CIM_VirtualSystemSettingData")
+XML_SCHEMA = "http://www.w3.org/2001/XMLSchema-instance"
 
 # File extensions in OVF package
 OVA_EXT = ".ova"
@@ -91,6 +96,9 @@ CONVERT_UNITS_TO_MB = {
   'gb': lambda x: x * 1024,
 }
 
+# Names of the config fields
+NAME = "name"
+
 
 def LinkFile(old_path, prefix=None, suffix=None, directory=None):
   """Create link with a given prefix and suffix.
@@ -523,6 +531,47 @@ class OVFReader(object):
     return results
 
 
+class OVFWriter(object):
+  """Writer class for OVF files.
+
+  @type tree: ET.ElementTree
+  @ivar tree: XML tree that we are constructing
+
+  """
+  def __init__(self, has_gnt_section):
+    """Initialize the writer - set the top element.
+
+    @type has_gnt_section: bool
+    @param has_gnt_section: if the Ganeti schema should be added - i.e. this
+      means that Ganeti section will be present
+
+    """
+    env_attribs = {
+      "xmlns:xsi": XML_SCHEMA,
+      "xmlns:vssd": VSSD_SCHEMA,
+      "xmlns:rasd": RASD_SCHEMA,
+      "xmlns:ovf": OVF_SCHEMA,
+      "xmlns": OVF_SCHEMA,
+      "xml:lang": "en-US",
+    }
+    if has_gnt_section:
+      env_attribs["xmlns:gnt"] = GANETI_SCHEMA
+    self.tree = ET.Element("Envelope", attrib=env_attribs)
+
+  def PrettyXmlDump(self):
+    """Formatter of the XML file.
+
+    @rtype: string
+    @return: XML tree in the form of nicely-formatted string
+
+    """
+    raw_string = ET.tostring(self.tree)
+    parsed_xml = xml.dom.minidom.parseString(raw_string)
+    xml_string = parsed_xml.toprettyxml(indent="  ")
+    text_re = re.compile(">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL)
+    return text_re.sub(">\g<1></", xml_string)
+
+
 class Converter(object):
   """Converter class for OVF packages.
 
@@ -1147,12 +1196,180 @@ class OVFImporter(Converter):
     self.Cleanup()
 
 
+class ConfigParserWithDefaults(ConfigParser.SafeConfigParser):
+  """This is just a wrapper on SafeConfigParser, that uses default values
+
+  """
+  def get(self, section, options, raw=None, vars=None): # pylint: disable=W0622
+    try:
+      result = ConfigParser.SafeConfigParser.get(self, section, options, \
+        raw=raw, vars=vars)
+    except ConfigParser.NoOptionError:
+      result = None
+    return result
+
+  def getint(self, section, options):
+    try:
+      result = ConfigParser.SafeConfigParser.get(self, section, options)
+    except ConfigParser.NoOptionError:
+      result = 0
+    return int(result)
+
+
 class OVFExporter(Converter):
+  """Converter from Ganeti config file to OVF
+
+  @type input_dir: string
+  @ivar input_dir: directory in which the config.ini file resides
+  @type output_dir: string
+  @ivar output_dir: directory to which the results of conversion shall be
+    written
+  @type packed_dir: string
+  @ivar packed_dir: if we want OVA package, this points to the real (i.e. not
+    temp) output directory
+  @type input_path: string
+  @ivar input_path: complete path to the config.ini file
+  @type output_path: string
+  @ivar output_path: complete path to .ovf file
+  @type config_parser: L{ConfigParserWithDefaults}
+  @ivar config_parser: parser for the config.ini file
+  @type results_name: string
+  @ivar results_name: name of the instance
+
+  """
   def _ReadInputData(self, input_path):
-    pass
+    """Reads the data on which the conversion will take place.
+
+    @type input_path: string
+    @param input_path: absolute path to the config.ini input file
+
+    @raise errors.OpPrereqError: error when reading the config file
+
+    """
+    input_dir = os.path.dirname(input_path)
+    self.input_path = input_path
+    self.input_dir = input_dir
+    if self.options.output_dir:
+      self.output_dir = os.path.abspath(self.options.output_dir)
+    else:
+      self.output_dir = input_dir
+    self.config_parser = ConfigParserWithDefaults()
+    logging.info("Reading configuration from %s file", input_path)
+    try:
+      self.config_parser.read(input_path)
+    except ConfigParser.MissingSectionHeaderError, err:
+      raise errors.OpPrereqError("Error when trying to read %s: %s" %
+                                 (input_path, err))
+    if self.options.ova_package:
+      self.temp_dir = tempfile.mkdtemp()
+      self.packed_dir = self.output_dir
+      self.output_dir = self.temp_dir
+
+    self.ovf_writer = OVFWriter(not self.options.ext_usage)
+
+  def _ParseName(self):
+    """Parses name from command line options or config file.
+
+    @rtype: string
+    @return: name of Ganeti instance
+
+    @raise errors.OpPrereqError: if name of the instance is not provided
+
+    """
+    if self.options.name:
+      name = self.options.name
+    else:
+      name = self.config_parser.get(constants.INISECT_INS, NAME)
+    if name is None:
+      raise errors.OpPrereqError("No instance name found")
+    return name
 
   def Parse(self):
-    pass
+    """Parses the data and creates a structure containing all required info.
+
+    """
+    try:
+      utils.Makedirs(self.output_dir)
+    except OSError, err:
+      raise errors.OpPrereqError("Failed to create directory %s: %s" %
+                                 (self.output_dir, err))
+
+    self.results_name = self._ParseName()
+
+  def _PrepareManifest(self, path):
+    """Creates manifest for all the files in OVF package.
+
+    @type path: string
+    @param path: path to manifesto file
+
+    @raise errors.OpPrereqError: if error occurs when writing file
+
+    """
+    logging.info("Preparing manifest for the OVF package")
+    lines = []
+    files_list = [self.output_path]
+    files_list.extend(self.references_files)
+    logging.warning("Calculating SHA1 checksums, this may take a while")
+    sha1_sums = utils.FingerprintFiles(files_list)
+    for file_path, value in sha1_sums.iteritems():
+      file_name = os.path.basename(file_path)
+      lines.append("SHA1(%s)= %s" % (file_name, value))
+    lines.append("")
+    data = "\n".join(lines)
+    try:
+      utils.WriteFile(path, data=data)
+    except errors.ProgrammerError, err:
+      raise errors.OpPrereqError("Saving the manifest file failed: %s" % err)
+
+  @staticmethod
+  def _PrepareTarFile(tar_path, files_list):
+    """Creates tarfile from the files in OVF package.
+
+    @type tar_path: string
+    @param tar_path: path to the resulting file
+    @type files_list: list
+    @param files_list: list of files in the OVF package
+
+    """
+    logging.info("Preparing tarball for the OVF package")
+    open(tar_path, mode="w").close()
+    ova_package = tarfile.open(name=tar_path, mode="w")
+    for file_name in files_list:
+      ova_package.add(file_name)
+    ova_package.close()
 
   def Save(self):
-    pass
+    """Saves the gathered configuration in an apropriate format.
+
+    @raise errors.OpPrereqError: if unable to create output directory
+
+    """
+    output_file = "%s%s" % (self.results_name, OVF_EXT)
+    output_path = utils.PathJoin(self.output_dir, output_file)
+    self.ovf_writer = OVFWriter(not self.options.ext_usage)
+    logging.info("Saving read data to %s", output_path)
+
+    self.output_path = utils.PathJoin(self.output_dir, output_file)
+    files_list = [self.output_path]
+
+    data = self.ovf_writer.PrettyXmlDump()
+    utils.WriteFile(self.output_path, data=data)
+
+    manifest_file = "%s%s" % (self.results_name, MF_EXT)
+    manifest_path = utils.PathJoin(self.output_dir, manifest_file)
+    self._PrepareManifest(manifest_path)
+    files_list.append(manifest_path)
+
+    files_list.extend(self.references_files)
+
+    if self.options.ova_package:
+      ova_file = "%s%s" % (self.results_name, OVA_EXT)
+      packed_path = utils.PathJoin(self.packed_dir, ova_file)
+      try:
+        utils.Makedirs(self.packed_dir)
+      except OSError, err:
+        raise errors.OpPrereqError("Failed to create directory %s: %s" %
+                                   (self.packed_dir, err))
+      self._PrepareTarFile(packed_path, files_list)
+    logging.info("Creation of the OVF package was successfull")
+    self.Cleanup()
-- 
GitLab