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
