Signed-off-by: Agata Murawska <[email protected]>
---
 lib/ovf.py |  231 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 228 insertions(+), 3 deletions(-)

diff --git a/lib/ovf.py b/lib/ovf.py
index 32c360b..0b80b46 100644
--- a/lib/ovf.py
+++ b/lib/ovf.py
@@ -23,9 +23,140 @@
 
 """
 
+# pylint: disable=W0622, F0401
+
+# W0622 since we need to use the ganeti.utils library
+
+# F0401 because ElementTree is not default for python 2.4
+
+
+import logging
 import os.path
+import tarfile
+import tempfile
+import xml.parsers.expat
+try:
+  from xml.etree.ElementTree import ElementTree
+except ImportError:
+  from elementtree.ElementTree import ElementTree
 
 from ganeti import errors
+from ganeti import constants
+from ganeti.utils import hash
+
+
+OVF_SCHEMA = "http://schemas.dmtf.org/ovf/envelope/1";
+
+
+class OVFReader:
+  """Reader class for OVF files.
+
+  @type files_list: list
+  @ivar files_list: list of files in the OVF package
+  @type tree: L{ElementTree}
+  @ivar tree: XML tree of the .ovf file
+  @type schema_name: string
+  @ivar schema_name: name of the .ovf file
+  @type input_dir: string
+  @ivar input_dir: directory in which the .ovf file resides
+
+  """
+  def __init__(self, input_path):
+    """Initialiaze the reader - load the .ovf file to XML parser.
+
+    It is assumed that names of manifesto (.mf), certificate (.cert) and ovf
+    files are the same. In order to account any other files as part of the ovf
+    package, they have to be explicitly mentioned in the Resources section
+    of the .ovf file.
+
+    @type input_path: string
+    @param input_path: absolute path to the .ovf file
+
+    @raise errors.OpPrereqError: when .ovf file is not a proper XML file or 
some
+      of the files mentioned in Resources section do not exist
+
+    """
+    self.tree = ElementTree()
+    try:
+      self.tree.parse(input_path)
+    except xml.parsers.expat.ExpatError:
+      raise errors.OpPrereqError("Error while reading .ovf file - possibly"
+                                 " incorrect XML structure")
+
+    # Create a list of all files in the OVF package
+    (input_dir, input_file) = os.path.split(input_path)
+    (input_name, _) = os.path.splitext(input_file)
+    extensions = [".ovf", ".mf", ".cert"]
+    files_directory = os.listdir(input_dir)
+    files_list = []
+    for file_name in files_directory:
+      (name, extension) = os.path.splitext(file_name)
+      if extension in extensions and name == input_name:
+        files_list.append(file_name)
+    files_list += self._GetAttributes("{%s}References/{%s}File" %
+                                      (OVF_SCHEMA, OVF_SCHEMA),
+                                      "{%s}href" % OVF_SCHEMA)
+    for file_name in files_list:
+      if not os.path.exists("%s/%s" % (input_dir, file_name)):
+        raise errors.OpPrereqError("File %s does not exist" % file_name)
+    logging.info("Files in the OVF package: %s", " ".join(files_list))
+    self.files_list = files_list
+    self.input_dir = input_dir
+    self.schema_name = input_name
+
+  def _GetAttributes(self, path, attribute):
+    """Get specified attribute from all nodes accessible using given path.
+
+    Function follows the path from root node to the desired tags using path,
+    then reads the apropriate attribute values.
+
+    @type path: string
+    @param path: path of nodes to visit
+    @type attribute: string
+    @param attribute: attribute for which we gather the information
+    @rtype: list
+    @return: for each accessible tag with the attribute value set, value of the
+      attribute
+
+    """
+    current_list = self.tree.findall(path)
+    results = [x.get(attribute) for x in current_list]
+    return filter(None, results)
+
+  def VerifyManifest(self):
+    """Verifies manifest for the OVF package, if one is given.
+
+    @raise errors.OpPrereqError: if SHA1 checksums do not match
+
+    """
+    if "%s.mf" % self.schema_name in self.files_list:
+      logging.warning("Verifying SHA1 checksums, this may take a while")
+      manifest = open("%s/%s.mf" % (self.input_dir, self.schema_name))
+      manifest_content = manifest.readlines()
+      manifest.close()
+      manifest_files = {}
+      for line in manifest_content:
+        beg = line.find("SHA1(") + len("SHA1(")
+        end = line.find(")= ")
+        filename = line[beg:end]
+        sha1sum = line[end+len(")= "):-1]
+        manifest_files[filename] = sha1sum
+      files_with_paths = ["%s/%s" % (self.input_dir, file_name)
+        for file_name in self.files_list]
+      sha1_sums = hash.FingerprintFiles(files_with_paths)
+      for file_name, value in manifest_files.iteritems():
+        if sha1_sums.get("%s/%s" % (self.input_dir, file_name)) != value:
+          raise errors.OpPrereqError("SHA1 checksum of %s does not match the"
+                                     " value in manifest file" % file_name)
+      logging.info("SHA1 checksums verified")
+
+  def VerifyCerificate(self):
+    """Verifies signature and validates the certificate for the OVF package.
+
+    """
+    if self.schema_name + ".cert" in self.files_list:
+      raise NotImplementedError()
+
 
 class Converter(object):
   """Converter class for OVF packages.
@@ -48,6 +179,7 @@ class Converter(object):
     @raise errors.OpPrereqError: if file does not exist
 
     """
+    input_path = os.path.abspath(input_path)
     if not (os.path.exists(input_path) and os.path.isfile(input_path)):
       raise errors.OpPrereqError("File %s does not exist" % input_path)
     self.options = options
@@ -57,7 +189,7 @@ class Converter(object):
     """Reads the data on which the conversion will take place.
 
     @type input_path: string
-    @param input_path: path to the Converter input file
+    @param input_path: absolute path to the Converter input file
 
     """
     raise NotImplementedError()
@@ -82,8 +214,96 @@ class Converter(object):
 
 
 class OVFImporter(Converter):
+  """Converter from OVF to Ganeti config file.
+
+  @type input_dir: string
+  @ivar input_dir: directory in which the .ovf file resides
+  @type output_dir: string
+  @ivar output_dir: directory to which the results of conversion shall be
+    written
+  @type temp_dir string or None
+  @ivar temp_dir: if the input was .ova archive, this variable points to the
+    temporary directory to which the OVA package was unpacked
+  @type input_path: string
+  @ivar input_path: complete path to the .ovf file
+  @type ovf_reader: L{OVFReader}
+  @ivar ovf_reader: OVF reader instance collects data from .ovf file
+
+  """
   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 .ovf or .ova input file
+
+    @raises errors.OpPrereqError: if input file is neither .ovf nor .ova
+
+    """
+    (input_dir, input_file) = os.path.split(input_path)
+    (_, input_extension) = os.path.splitext(input_file)
+
+    if input_extension == ".ovf":
+      logging.info(".ovf file extension found, no unpacking necessary")
+      self.input_dir = input_dir
+      self.input_path = input_path
+      self.temp_dir = None
+    elif input_extension == ".ova":
+      logging.info(".ova file extension found, proceeding to unpacking")
+      self._UnpackOVA(input_path)
+    else:
+      raise errors.OpPrereqError("Unknown file extension; expected '.ovf' or"
+                                 " '.ova' file")
+    assert ((input_extension == ".ova" and self.temp_dir) or
+            (input_extension == ".ovf" and not self.temp_dir))
+    assert self.input_dir in self.input_path
+
+    if self.options.output_dir:
+      self.output_dir = os.path.abspath(self.options.output_dir)
+      if (os.path.commonprefix([constants.EXPORT_DIR, self.output_dir]) !=
+          constants.EXPORT_DIR):
+        logging.warning("Export path is not under %s directory, import to"
+                        " Ganeti using gnt-backup may fail",
+                        constants.EXPORT_DIR)
+    else:
+      self.output_dir = constants.EXPORT_DIR
+
+    self.ovf_reader = OVFReader(self.input_path)
+    self.ovf_reader.VerifyManifest()
+    self.ovf_reader.VerifyCerificate()
+
+  def _UnpackOVA(self, input_path):
+    """Unpacks the .ova package into temporary directory.
+
+    @type input_path: string
+    @param input_path: path to the .ova package file
+
+    @raises errors.OpPrereqError: if file is not a proper tarball, one of the
+        files in the archive seem malicious (e.g. path starts with '../') or
+        .ova package does not contain .ovf file
+
+    """
+    input_name = None
+    if not tarfile.is_tarfile(input_path):
+      raise errors.OpPrereqError("The provided '.ova' file is not a proper tar"
+                                 " archive")
+    ova_content = tarfile.open(input_path)
+    for file_name in ova_content.getnames():
+      if ".." in file_name or file_name.startswith("/"):
+        raise errors.OpPrereqError("File %s inside .ova package has untrusted"
+                                   " path starting with '/' or containing 
'..'",
+                                   file_name)
+      if file_name.endswith(".ovf"):
+        input_name = file_name
+    if not input_name:
+      raise errors.OpPrereqError("No .ovf file in .ova package found")
+    temp_dir = tempfile.mkdtemp()
+    logging.warning("Unpacking the %s archive, this may take a while",
+      input_path)
+    ova_content.extractall(temp_dir)
+    self.temp_dir = temp_dir
+    self.input_dir = temp_dir
+    self.input_path = self.temp_dir + "/" + input_name
+    logging.info("OVA file extracted to %s directory", self.temp_dir)
 
   def Parse(self):
     pass
@@ -92,7 +312,12 @@ class OVFImporter(Converter):
     pass
 
   def Cleanup(self):
-    pass
+    """Cleanes the temporary directory, if one was created.
+
+    """
+    if self.temp_dir:
+      shutil.rmtree(self.temp_dir)
+      self.temp_dir = None
 
 
 class OVFExporter(Converter):
-- 
1.7.3.1

Reply via email to