Signed-off-by: Agata Murawska <[email protected]> --- lib/ovf.py | 251 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 files changed, 248 insertions(+), 3 deletions(-)
diff --git a/lib/ovf.py b/lib/ovf.py index 4a16da8..8b427c9 100644 --- a/lib/ovf.py +++ b/lib/ovf.py @@ -23,9 +23,154 @@ """ -import os.path +# pylint: disable=F0401, E1101, R0201 +# F0401 because ElementTree is not default for python 2.4 +# E1101 makes no sense - pylint assumes that ElementTree object is a tuple +# R0201 we do not want _GetInfo, _GetDiskFormat or _ConvertDiskToRaw to be +# static functions +import logging +import os.path +import re +import shutil +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 constants from ganeti import errors +from ganeti import utils + + +# Schemas used in OVF format +GANETI_SCHEMA = "http://ganeti" +OVF_SCHEMA = "http://schemas.dmtf.org/ovf/envelope/1" + +# File extensions in OVF package +OVA_EXT = ".ova" +OVF_EXT = ".ovf" +MF_EXT = ".mf" +CERT_EXT = ".cert" +FILE_EXTENSIONS = [ + OVF_EXT, + MF_EXT, + CERT_EXT, +] + + +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, err: + raise errors.OpPrereqError("Error while reading %s file: %s" % + (OVF_EXT, err)) + + # 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) + files_directory = utils.ListVisibleFiles(input_dir) + files_list = [] + for file_name in files_directory: + (name, extension) = os.path.splitext(file_name) + if extension in FILE_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: + file_path = utils.PathJoin(input_dir, file_name) + if not os.path.exists(file_path): + raise errors.OpPrereqError("File does not exist: %s" % file_path) + 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%s" % (self.schema_name, MF_EXT) in self.files_list: + logging.warning("Verifying SHA1 checksums, this may take a while") + manifest_filename = "%s%s" % (self.schema_name, MF_EXT) + manifest_path = utils.PathJoin(self.input_dir, manifest_filename) + manifest_content = utils.ReadFile(manifest_path).splitlines() + manifest_files = {} + regexp = r"SHA1\((\S+)\)= (\S+)" + for line in manifest_content: + match = re.match(regexp, line) + if match: + file_name = match.group(1) + sha1_sum = match.group(2) + manifest_files[file_name] = sha1_sum + files_with_paths = [utils.PathJoin(self.input_dir, file_name) + for file_name in self.files_list] + sha1_sums = utils.FingerprintFiles(files_with_paths) + for file_name, value in manifest_files.iteritems(): + if sha1_sums.get(utils.PathJoin(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 "%s%s" % (self.schema_name, CERT_EXT) in self.files_list: + raise NotImplementedError() class Converter(object): @@ -84,8 +229,103 @@ 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_EXT: + logging.info("%s file extension found, no unpacking necessary", OVF_EXT) + self.input_dir = input_dir + self.input_path = input_path + self.temp_dir = None + elif input_extension == OVA_EXT: + logging.info("%s file extension found, proceeding to unpacking", OVA_EXT) + self._UnpackOVA(input_path) + else: + raise errors.OpPrereqError("Unknown file extension; expected %s or %s" + " file" % (OVA_EXT, OVF_EXT)) + assert ((input_extension == OVA_EXT and self.temp_dir) or + (input_extension == OVF_EXT 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 %s file is not a proper tar" + " archive", OVA_EXT) + ova_content = tarfile.open(input_path) + for file_name in ova_content.getnames(): + file_normname = os.path.normpath(file_name) + if (os.path.commonprefix(["../", file_normname]) or + os.path.commonprefix(["/", file_normname])): + raise errors.OpPrereqError("File %s inside %s package has untrusted" + " path" % (file_name, OVA_EXT)) + if file_name.endswith(OVF_EXT): + input_name = file_name + if not input_name: + raise errors.OpPrereqError("No %s file in %s package found" % + (OVF_EXT, OVA_EXT)) + temp_dir = tempfile.mkdtemp() + logging.warning("Unpacking the %s archive, this may take a while", + input_path) + self.temp_dir = temp_dir + self.input_dir = temp_dir + self.input_path = utils.PathJoin(self.temp_dir, input_name) + try: + ova_content.extractall(temp_dir) + logging.info("OVA package extracted to %s directory", self.temp_dir) + except tarfile.TarError, err: + self.Cleanup() + raise errors.OpPrereqError("Error while extracting %s archive: %s" % + (OVA_EXT, err)) def Parse(self): pass @@ -94,7 +334,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
