From 99381e3b6cb276b6de33427cddae93ab0e75e6cd Mon Sep 17 00:00:00 2001 From: Agata Murawska <agatamurawska@google.com> Date: Mon, 12 Sep 2011 10:34:06 +0200 Subject: [PATCH] Import: disk conversion Signed-off-by: Agata Murawska <agatamurawska@google.com> Reviewed-by: Michael Hanselmann <hansmi@google.com> --- lib/ovf.py | 401 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 400 insertions(+), 1 deletion(-) diff --git a/lib/ovf.py b/lib/ovf.py index 7ffc80c05..36e3e2de5 100644 --- a/lib/ovf.py +++ b/lib/ovf.py @@ -29,7 +29,9 @@ # E1101 makes no sense - pylint assumes that ElementTree object is a tuple +import errno import logging +import os import os.path import re import shutil @@ -57,12 +59,58 @@ OVA_EXT = ".ova" OVF_EXT = ".ovf" MF_EXT = ".mf" CERT_EXT = ".cert" +COMPRESSION_EXT = ".gz" FILE_EXTENSIONS = [ OVF_EXT, MF_EXT, CERT_EXT, ] +COMPRESSION_TYPE = "gzip" +COMPRESS = "compression" +DECOMPRESS = "decompression" +ALLOWED_ACTIONS = [COMPRESS, DECOMPRESS] + + +def LinkFile(old_path, prefix=None, suffix=None, directory=None): + """Create link with a given prefix and suffix. + + This is a wrapper over os.link. It tries to create a hard link for given file, + but instead of rising error when file exists, the function changes the name + a little bit. + + @type old_path:string + @param old_path: path to the file that is to be linked + @type prefix: string + @param prefix: prefix of filename for the link + @type suffix: string + @param suffix: suffix of the filename for the link + @type directory: string + @param directory: directory of the link + + @raise errors.OpPrereqError: when error on linking is different than + "File exists" + + """ + assert(prefix is not None or suffix is not None) + if directory is None: + directory = os.getcwd() + new_path = utils.PathJoin(directory, "%s%s" % (prefix, suffix)) + counter = 1 + while True: + try: + os.link(old_path, new_path) + break + except OSError, err: + if err.errno == errno.EEXIST: + new_path = utils.PathJoin(directory, + "%s_%s%s" % (prefix, counter, suffix)) + counter += 1 + else: + raise errors.OpPrereqError("Error moving the file %s to %s location:" + " %s" % (old_path, new_path, err)) + return new_path + class OVFReader(object): """Reader class for OVF files. @@ -139,6 +187,50 @@ class OVFReader(object): results = [x.get(attribute) for x in current_list] return filter(None, results) + def _GetElementMatchingAttr(self, path, match_attr): + """Searches for element on a path that matches certain attribute value. + + Function follows the path from root node to the desired tags using path, + then searches for the first one matching the attribute value. + + @type path: string + @param path: path of nodes to visit + @type match_attr: tuple + @param match_attr: pair (attribute, value) for which we search + @rtype: ET.ElementTree or None + @return: first element matching match_attr or None if nothing matches + + """ + potential_elements = self.tree.findall(path) + (attr, val) = match_attr + for elem in potential_elements: + if elem.get(attr) == val: + return elem + return None + + @staticmethod + def _GetDictParameters(root, schema): + """Reads text in all children and creates the dictionary from the contents. + + @type root: ET.ElementTree or None + @param root: father of the nodes we want to collect data about + @type schema: string + @param schema: schema name to be removed from the tag + @rtype: dict + @return: dictionary containing tags and their text contents, tags have their + schema fragment removed or empty dictionary, when root is None + + """ + if not root: + return {} + results = {} + for element in list(root): + pref_len = len("{%s}" % schema) + assert(schema in element.tag) + tag = element.tag[pref_len:] + results[tag] = element.text + return results + def VerifyManifest(self): """Verifies manifest for the OVF package, if one is given. @@ -167,6 +259,48 @@ class OVFReader(object): " value in manifest file" % file_name) logging.info("SHA1 checksums verified") + def GetInstanceName(self): + """Provides information about instance name. + + @rtype: string + @return: instance name string + + """ + find_name = "{%s}VirtualSystem/{%s}Name" % (OVF_SCHEMA, OVF_SCHEMA) + return self.tree.findtext(find_name) + + def GetDiskTemplate(self): + """Returns disk template from .ovf file + + @rtype: string or None + @return: name of the template + """ + find_template = ("{%s}GanetiSection/{%s}DiskTemplate" % + (GANETI_SCHEMA, GANETI_SCHEMA)) + return self.tree.findtext(find_template) + + def GetDisksNames(self): + """Provides list of file names for the disks used by the instance. + + @rtype: list + @return: list of file names, as referenced in .ovf file + + """ + results = [] + disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA) + disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA) + for disk in disk_ids: + disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA) + disk_match = ("{%s}id" % OVF_SCHEMA, disk) + disk_elem = self._GetElementMatchingAttr(disk_search, disk_match) + if disk_elem is None: + raise errors.OpPrereqError("%s file corrupted - disk %s not found in" + " references" % (OVF_EXT, disk)) + disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA) + disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA) + results.append((disk_name, disk_compression)) + return results + class Converter(object): """Converter class for OVF packages. @@ -215,6 +349,109 @@ class Converter(object): """ raise NotImplementedError() + def _CompressDisk(self, disk_path, compression, action): + """Performs (de)compression on the disk and returns the new path + + @type disk_path: string + @param disk_path: path to the disk + @type compression: string + @param compression: compression type + @type action: string + @param action: whether the action is compression or decompression + @rtype: string + @return: new disk path after (de)compression + + @raise errors.OpPrereqError: disk (de)compression failed or "compression" + is not supported + + """ + assert(action in ALLOWED_ACTIONS) + # For now we only support gzip, as it is used in ovftool + if compression != COMPRESSION_TYPE: + raise errors.OpPrereqError("Unsupported compression type: %s" + % compression) + disk_file = os.path.basename(disk_path) + if action == DECOMPRESS: + (disk_name, _) = os.path.splitext(disk_file) + prefix = disk_name + elif action == COMPRESS: + prefix = disk_file + new_path = utils.GetClosedTempfile(suffix=COMPRESSION_EXT, prefix=prefix, + dir=self.output_dir) + self.temp_file_manager.Add(new_path) + args = ["gzip", "-c", disk_path] + run_result = utils.RunCmd(args, output=new_path) + if run_result.failed: + raise errors.OpPrereqError("Disk %s failed with output: %s" + % (action, run_result.stderr)) + logging.info("The %s of the disk is completed", action) + return (COMPRESSION_EXT, new_path) + + def _ConvertDisk(self, disk_format, disk_path): + """Performes conversion to specified format. + + @type disk_format: string + @param disk_format: format to which the disk should be converted + @type disk_path: string + @param disk_path: path to the disk that should be converted + @rtype: string + @return path to the output disk + + @raise errors.OpPrereqError: convertion of the disk failed + + """ + disk_file = os.path.basename(disk_path) + (disk_name, disk_extension) = os.path.splitext(disk_file) + if disk_extension != disk_format: + logging.warning("Conversion of disk image to %s format, this may take" + " a while", disk_format) + + new_disk_path = utils.GetClosedTempfile(suffix=".%s" % disk_format, + prefix=disk_name, dir=self.output_dir) + self.temp_file_manager.Add(new_disk_path) + args = [ + "qemu-img", + "convert", + "-O", + disk_format, + disk_path, + new_disk_path, + ] + run_result = utils.RunCmd(args, cwd=os.getcwd()) + if run_result.failed: + raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was" + ": %s" % (disk_format, run_result.stderr)) + return (".%s" % disk_format, new_disk_path) + + @staticmethod + def _GetDiskQemuInfo(disk_path, regexp): + """Figures out some information of the disk using qemu-img. + + @type disk_path: string + @param disk_path: path to the disk we want to know the format of + @type regexp: string + @param regexp: string that has to be matched, it has to contain one group + @rtype: string + @return: disk format + + @raise errors.OpPrereqError: format information cannot be retrieved + + """ + args = ["qemu-img", "info", disk_path] + run_result = utils.RunCmd(args, cwd=os.getcwd()) + if run_result.failed: + raise errors.OpPrereqError("Gathering info about the disk using qemu-img" + " failed, output was: %s" % run_result.stderr) + result = run_result.output + regexp = r"%s" % regexp + match = re.search(regexp, result) + if match: + disk_format = match.group(1) + else: + raise errors.OpPrereqError("No file information matching %s found in:" + " %s" % (regexp, result)) + return disk_format + def Parse(self): """Parses the data and creates a structure containing all required info. @@ -249,6 +486,14 @@ class OVFImporter(Converter): @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 + @type results_name: string + @ivar results_name: name of imported instance + @type results_template: string + @ivar results_template: disk template read from .ovf file or command line + arguments + @type results_disk: dict + @ivar results_disk: disk information gathered from .ovf file or command line + arguments """ def _ReadInputData(self, input_path): @@ -340,7 +585,159 @@ class OVFImporter(Converter): logging.info("OVA package extracted to %s directory", self.temp_dir) def Parse(self): - pass + """Parses the data and creates a structure containing all required info. + + The method reads the information given either as a command line option or as + a part of the OVF description. + + @raise errors.OpPrereqError: if some required part of the description of + virtual instance is missing or unable to create output directory + + """ + self.results_name = self._GetInfo("instance name", self.options.name, + self._ParseNameOptions, self.ovf_reader.GetInstanceName) + if not self.results_name: + raise errors.OpPrereqError("Name of instance not provided") + + self.output_dir = utils.PathJoin(self.output_dir, self.results_name) + try: + utils.Makedirs(self.output_dir) + except OSError, err: + raise errors.OpPrereqError("Failed to create directory %s: %s" % + (self.output_dir, err)) + + self.results_template = self._GetInfo("disk template", + self.options.disk_template, self._ParseTemplateOptions, + self.ovf_reader.GetDiskTemplate) + if not self.results_template: + logging.info("Disk template not given") + + self.results_disk = self._GetInfo("disk", self.options.disks, + self._ParseDiskOptions, self._GetDiskInfo, + ignore_test=self.results_template == constants.DT_DISKLESS) + + @staticmethod + def _GetInfo(name, cmd_arg, cmd_function, nocmd_function, + ignore_test=False): + """Get information about some section - e.g. disk, network, hypervisor. + + @type name: string + @param name: name of the section + @type cmd_arg: dict + @param cmd_arg: command line argument specific for section 'name' + @type cmd_function: callable + @param cmd_function: function to call if 'cmd_args' exists + @type nocmd_function: callable + @param nocmd_function: function to call if 'cmd_args' is not there + + """ + if ignore_test: + logging.info("Information for %s will be ignored", name) + return {} + if cmd_arg: + logging.info("Information for %s will be parsed from command line", name) + results = cmd_function() + else: + logging.info("Information for %s will be parsed from %s file", + name, OVF_EXT) + results = nocmd_function() + logging.info("Options for %s were succesfully read", name) + return results + + def _ParseNameOptions(self): + """Returns name if one was given in command line. + + @rtype: string + @return: name of an instance + + """ + return self.options.name + + def _ParseTemplateOptions(self): + """Returns disk template if one was given in command line. + + @rtype: string + @return: disk template name + + """ + return self.options.disk_template + + def _ParseDiskOptions(self): + """Parses disk options given in a command line. + + @rtype: dict + @return: dictionary of disk-related options + + @raise errors.OpPrereqError: disk description does not contain size + information or size information is invalid or creation failed + + """ + assert self.options.disks + results = {} + for (disk_id, disk_desc) in self.options.disks: + results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id + if disk_desc.get("size"): + try: + disk_size = utils.ParseUnit(disk_desc["size"]) + except ValueError: + raise errors.OpPrereqError("Invalid disk size for disk %s: %s" % + (disk_id, disk_desc["size"])) + new_path = utils.PathJoin(self.output_dir, str(disk_id)) + args = [ + "qemu-img", + "create", + "-f", + "raw", + new_path, + disk_size, + ] + run_result = utils.RunCmd(args) + if run_result.failed: + raise errors.OpPrereqError("Creation of disk %s failed, output was:" + " %s" % (new_path, run_result.stderr)) + results["disk%s_size" % disk_id] = str(disk_size) + results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id + else: + raise errors.OpPrereqError("Disks created for import must have their" + " size specified") + results["disk_count"] = str(len(self.options.disks)) + return results + + def _GetDiskInfo(self): + """Gathers information about disks used by instance, perfomes conversion. + + @rtype: dict + @return: dictionary of disk-related options + + @raise errors.OpPrereqError: disk is not in the same directory as .ovf file + + """ + results = {} + disks_list = self.ovf_reader.GetDisksNames() + for (counter, (disk_name, disk_compression)) in enumerate(disks_list): + if os.path.dirname(disk_name): + raise errors.OpPrereqError("Disks are not allowed to have absolute" + " paths or paths outside main OVF directory") + disk, _ = os.path.splitext(disk_name) + disk_path = utils.PathJoin(self.input_dir, disk_name) + if disk_compression: + _, disk_path = self._CompressDisk(disk_path, disk_compression, + DECOMPRESS) + disk, _ = os.path.splitext(disk) + if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw": + logging.info("Conversion to raw format is required") + ext, new_disk_path = self._ConvertDisk("raw", disk_path) + + final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext, + directory=self.output_dir) + final_name = os.path.basename(final_disk_path) + disk_size = os.path.getsize(final_disk_path) / (1024 * 1024) + results["disk%s_dump" % counter] = final_name + results["disk%s_size" % counter] = str(disk_size) + results["disk%s_ivname" % counter] = "disk/%s" % str(counter) + if disks_list: + results["disk_count"] = str(len(disks_list)) + return results def Save(self): """Saves all the gathered information in a constant.EXPORT_CONF_FILE file. @@ -370,6 +767,8 @@ class OVFImporter(Converter): for section, options in results.iteritems(): output.append("[%s]" % section) for name, value in options.iteritems(): + if value is None: + value = "" output.append("%s = %s" % (name, value)) output.append("") output_contents = "\n".join(output) -- GitLab