This is merged with current tip and now installs properly. thanks john Create config parser plugin system Start a plugin system for config formats parsing, and implement enough of it to get to where we were with virt-convert. Signed-off-by: John Levon <john.levon@xxxxxxx> diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -9,7 +9,8 @@ import os, sys import os, sys import tests.coverage as coverage -pkgs = ['virtinst'] +pkgs = ['virtinst', 'virtconv', 'virtconv.parsers' ] + datafiles = [('share/man/man1', ['man/en/virt-install.1', 'man/en/virt-clone.1', 'man/en/virt-image.1', diff --git a/virt-convert b/virt-convert --- a/virt-convert +++ b/virt-convert @@ -21,56 +21,58 @@ # MA 02110-1301 USA. import sys -from string import ascii_letters -import virtinst.cli as cli import os import logging import errno from optparse import OptionParser +import virtinst.cli as cli +import virtconv +import virtconv.vmconfig as vmconfig + def parse_args(): - parser = OptionParser() - parser.set_usage("%prog [options] inputdir|input.vmx " + opts = OptionParser() + opts.set_usage("%prog [options] inputdir|input.vmx " "[outputdir|output.xml]") - parser.add_option("-a", "--arch", type="string", dest="arch", - help=("Machine Architecture Type (i686/x86_64/ppc)")) - parser.add_option("-t", "--type", type="string", dest="type", - help=("Output virtualization type (hvm, paravirt")) - parser.add_option("-d", "--debug", action="store_true", dest="debug", - help=("Print debugging information")) - parser.add_option("-i", "--input-format", action="store", - dest="inputformat", default="vmx", - help=("Input format, e.g. 'vmx'")) - parser.add_option("-o", "--output-format", action="store", - dest="outputformat", default="virt-image", - help=("Output format, e.g. 'virt-image'")) - parser.add_option("-v", "--hvm", action="store_true", dest="fullvirt", - help=("This guest should be a fully virtualized guest")) - parser.add_option("-p", "--paravirt", action="store_true", dest="paravirt", - help=("This guest should be a paravirtualized guest")) + opts.add_option("-a", "--arch", type="string", dest="arch", + help=("Machine Architecture Type (i686/x86_64/ppc)")) + opts.add_option("-t", "--type", type="string", dest="type", + help=("Output virtualization type (hvm, paravirt")) + opts.add_option("-d", "--debug", action="store_true", dest="debug", + help=("Print debugging information")) + opts.add_option("-i", "--input-format", action="store", + dest="inputformat", default="vmx", + help=("Input format, e.g. 'vmx'")) + opts.add_option("-o", "--output-format", action="store", + dest="outputformat", default="virt-image", + help=("Output format, e.g. 'virt-image'")) + opts.add_option("-v", "--hvm", action="store_true", dest="fullvirt", + help=("This guest should be a fully virtualized guest")) + opts.add_option("-p", "--paravirt", action="store_true", dest="paravirt", + help=("This guest should be a paravirtualized guest")) - (options, args) = parser.parse_args() + (options, args) = opts.parse_args() if len(args) < 1: - parser.error(("You need to provide an input VM definition")) + opts.error(("You need to provide an input VM definition")) if len(args) > 2: - parser.error(("Too many arguments provided")) + opts.error(("Too many arguments provided")) if (options.arch is None): - parser.error(("Missing option value \n\nArchitecture: " + + opts.error(("Missing option value \n\nArchitecture: " + str(options.arch))) # hard-code for now if options.inputformat != "vmx": - parser.error(("Unsupported input format \"%s\"" % options.inputformat)) + opts.error(("Unsupported input format \"%s\"" % options.inputformat)) if options.outputformat != "virt-image": - parser.error(("Unsupported output format \"%s\"" + opts.error(("Unsupported output format \"%s\"" % options.outputformat)) if os.path.isdir(args[0]): vmx_files = [x for x in os.listdir(args[0]) if x.endswith(".vmx") ] if (len(vmx_files)) == 0: - parser.error(("No VM definition file was found in %s" % args[0])) + opts.error(("No VM definition file was found in %s" % args[0])) if (len(vmx_files)) > 1: - parser.error(("Too many .vmx definitions found in %s" % args[0])) + opts.error(("Too many .vmx definitions found in %s" % args[0])) options.input_file = os.path.join(args[0], vmx_files[0]) options.input_dir = args[0] else: @@ -91,197 +93,78 @@ def parse_args(): return options -# Begin creation of xml template from parsed vmx config file -def vmx_to_image_xml(disks_list, record, options, hvm): - pv_disk_list = [] - fv_disk_list = [] - storage_disk_list = [] - - infile = options.input_file - - # validate required values for conversion are in the input vmx file - if record.has_key("displayName"): - name = record["displayName"] - else: - logging.error("displayName key not parsed from %s" % infile) - sys.exit(1) - - if record.has_key("memsize"): - memory = int(record["memsize"]) * 1024 - else: - logging.error("memsize key not parsed from %s" % infile) - sys.exit(1) - - if record.has_key("annotation"): - annotation = record["annotation"] - else: - annotation = "" - - if record.has_key("numvcpus"): - vcpus = record["numvcpus"] - else: - vcpus = "1" - - -# create disk filename lists for xml template - for (number, dfile) in enumerate(disks_list): - dfile = str(dfile.replace(".vmdk","")).strip() - pv_disk_list.append("""<drive disk="%s.img" target="xvd%s"/>""" % \ - (dfile, ascii_letters[number % 26])) - fv_disk_list.append("""<drive disk="%s.img" target="hd%s"/>""" % \ - (dfile, ascii_letters[number % 26])) - storage_disk_list.append("""<disk file="%s.img" use="system" format="raw"/>""" % (dfile)) - -# determine virtualization type for image.boot section - if hvm is False: - virt_boot_template = """<boot type="xen"> - <guest> - <arch>%(vm_arch)s</arch> - <features> - <pae/> - </features> - </guest> - <os> - <loader>pygrub</loader> - </os> - %(vm_pv_disks)s - </boot>""" - elif hvm is True: - virt_boot_template = """<boot type="hvm"> - <guest> - <arch>%(vm_arch)s</arch> - </guest> - <os> - <loader dev="hd"/> - </os> - %(vm_fv_disks)s - </boot>""" - - -# xml replacements dictionaries - virt_boot_xml_dict = { - "vm_pv_disks" : "".join(pv_disk_list), - "vm_fv_disks" : "".join(fv_disk_list), - "vm_arch" : options.arch, - } - virt_boot_template = virt_boot_template % virt_boot_xml_dict - virt_image_xml_dict = { - "virt_boot_template" : virt_boot_template, - "vm_displayName": name.replace(" ","_"), - "vm_annotation" : annotation, - "vm_vcpus" : vcpus, - "vm_mem" : memory, - "vm_storage" : "".join(storage_disk_list), - } - - virt_image_xml_template = """<image> - <name>%(vm_displayName)s</name> - <label>%(vm_displayName)s</label> - <description> - %(vm_annotation)s - </description> - <domain> - %(virt_boot_template)s - <devices> - <vcpu>%(vm_vcpus)s</vcpu> - <memory>%(vm_mem)s</memory> - <interface/> - <graphics/> - </devices> - </domain> - <storage> - %(vm_storage)s - </storage> -</image> -""" - - virtimage_xml_template = virt_image_xml_template % virt_image_xml_dict - return virtimage_xml_template - -# parse input vmware configuration -def parse_vmware_config(options): - if not os.access(options.input_file, os.R_OK): - raise ValueError, "Could not read file: %s" % options.input_file - infile = open(options.input_file, "r") - contents = infile.readlines() - infile.close() - record = {} - vm_config = [] - disks_list = [] - - # strip out comment and blank lines for easy splitting of values - for line in contents: - if not line.strip() or line.startswith("#"): - continue - else: - vm_config.append(line) - - for line in vm_config: - before_eq, after_eq = line.split("=", 1) - key = before_eq.replace(" ","") - value = after_eq.replace('"',"") - value = value.strip() - record[key] = value - logging.debug("Key: %s Value: \"%s\"" % (key, value)) - if value.endswith("vmdk"): # separate disks from config - disks_list.append(value) - return record, disks_list - - -def convert_disks(disks_list, options): - for disk in disks_list: - infile = disk.strip() - if not os.path.isabs(infile): - infile = os.path.join(options.input_dir, infile) - - outfile = disk.replace(".vmdk","").strip() - outfile += ".img" - if not os.path.isabs(outfile): - outfile = os.path.join(options.output_dir, outfile) - convert_cmd = "qemu-img convert %s -O raw %s" % (infile, outfile) - ret = os.system(convert_cmd) - print ret - - def main(): options = parse_args() cli.setupLogging("virt-convert", options.debug) - vm_config = parse_vmware_config(options) - record, disks_list = vm_config + try: + inp = virtconv.vmconfig.find_parser_by_name(options.inputformat) + except: + logging.error("No parser of format \"%s\" was found." % + options.inputformat) + sys.exit(1) + + try: + outp = virtconv.vmconfig.find_parser_by_name(options.outputformat) + except: + logging.error("No parser of format \"%s\" was found." % + options.outputformat) + sys.exit(1) + + vmdef = None + + try: + vmdef = inp.import_file(options.input_file) + except IOError, e: + logging.error("Couldn't import file \"%s\": %s" % + (options.input_file, e.strerror)) + sys.exit(1) + except Exception, e: + logging.error("Couldn't import file \"%s\": %s" % + (options.input_file, e.message)) + sys.exit(1) if options.paravirt: - hvm = False + vmdef.type = vmconfig.VM_TYPE_PV else: - hvm = True - out_contents = vmx_to_image_xml(disks_list, record, options, hvm) + vmdef.type = vmconfig.VM_TYPE_HVM - name = record["displayName"].replace(" ","-") + vmdef.arch = options.arch + + unixname = vmdef.name.replace(" ", "-") if not options.output_dir: - options.output_dir = name + options.output_dir = unixname try: logging.debug("Creating directory %s" % options.output_dir) os.mkdir(options.output_dir) except OSError, e: if (e.errno != errno.EEXIST): logging.error("Could not create directory %s: %s" % - (options.output_dir, str(e))) + (options.output_dir, e.strerror)) sys.exit(1) if not options.output_file: options.output_file = os.path.join(options.output_dir, - "%s.virt-image.xml" % name) + "%s%s" % (unixname, outp.suffix)) logging.debug("input_file: %s" % options.input_file) logging.debug("input_dir: %s" % options.input_dir) logging.debug("output_file: %s" % options.output_file) logging.debug("output_dir: %s" % options.input_dir) - # configuration completed, ready to write config file and convert disks - out = open(options.output_file, "w") - out.writelines(out_contents) - out.close() - convert_disks(disks_list, options) + try: + for d in vmdef.disks: + d.convert(options.input_dir, options.output_dir, + vmconfig.DISK_TYPE_RAW) + except Exception, e: + logging.error(e) + sys.exit(1) + + try: + outp.export_file(vmdef, options.output_file) + except Exception, e: + logging.error(e) + sys.exit(1) print "\n\nConversion completed and placed in: %s" % options.output_dir diff --git a/virtconv/__init__.py b/virtconv/__init__.py new file mode 100644 --- /dev/null +++ b/virtconv/__init__.py @@ -0,0 +1,29 @@ +# +# Copyright 2008 Sun Microsystems, Inc. All rights reserved. +# Use is subject to license terms. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA. +# + +import pkgutil +import imp +import os + +parsers_path = [os.path.join(__path__[0], "parsers/")] + +for loader, name, ispkg in pkgutil.iter_modules(parsers_path): + filename, pathname, desc = imp.find_module(name, parsers_path) + imp.load_module(name, filename, pathname, desc) diff --git a/virtconv/parsers/virtimage.py b/virtconv/parsers/virtimage.py new file mode 100644 --- /dev/null +++ b/virtconv/parsers/virtimage.py @@ -0,0 +1,152 @@ +# +# Copyright 2008 Sun Microsystems, Inc. All rights reserved. +# Use is subject to license terms. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA. +# + +from string import ascii_letters +import virtconv.vmconfig as vmconfig + +pv_boot_template = """ + <boot type="xen"> + <guest> + <arch>%(arch)s</arch> + <features> + <pae/> + </features> + </guest> + <os> + <loader>pygrub</loader> + </os> + %(pv_disks)s + </boot> +""" + +hvm_boot_template = """ + <boot type="hvm"> + <guest> + <arch>%(arch)s</arch> + </guest> + <os> + <loader dev="hd"/> + </os> + %(hvm_disks)s + </boot> +""" + +image_template = """ +<image> + <name>%(name)s</name> + <label>%(name)s</label> + <description> + %(description)s + </description> + <domain> + %(boot_template)s + <devices> + <vcpu>%(nr_vcpus)s</vcpu> + <memory>%(memory)s</memory> + <interface/> + <graphics/> + </devices> + </domain> + <storage> + %(storage)s + </storage> +</image> +""" + +class virtimage_parser(vmconfig.parser): + """ + Support for virt-install's image format (see virt-image man page). + """ + name = "virt-image" + suffix = ".virt-image.xml" + + @staticmethod + def identify_file(input_file): + """ + Return True if the given file is of this format. + """ + raise NotImplementedError + + @staticmethod + def import_file(input_file): + """ + Import a configuration file. Raises if the file couldn't be + opened, or parsing otherwise failed. + """ + raise NotImplementedError + + @staticmethod + def export_file(vm, output_file): + """ + Export a configuration file. + @vm vm configuration instance + @file Output file + + Raises ValueError if configuration is not suitable, or another + exception on failure to write the output file. + """ + + if not vm.memory: + raise ValueError("VM must have a memory setting") + + pv_disks = [] + hvm_disks = [] + storage_disks = [] + + # create disk filename lists for xml template + for disk in vm.disks: + number = disk.number + path = disk.path + + # FIXME: needs updating for later Xen enhancements; need to + # implement capabilities checking for max disks etc. + pv_disks.append("""<drive disk="%s" target="xvd%s" />""" % + (path, ascii_letters[number % 26])) + hvm_disks.append("""<drive disk="%s" target="hd%s" />""" % + (path, ascii_letters[number % 26])) + storage_disks.append( + """<disk file="%s" use="system" format="raw"/>""" % path) + + if vm.type == vmconfig.VM_TYPE_PV: + boot_template = pv_boot_template + else: + boot_template = hvm_boot_template + + boot_xml = boot_template % { + "pv_disks" : "".join(pv_disks), + "hvm_disks" : "".join(hvm_disks), + "arch" : vm.arch, + } + + out = image_template % { + "boot_template": boot_xml, + "name" : vm.name, + "description" : vm.description, + "nr_vcpus" : vm.nr_vcpus, + # Mb to Kb + "memory" : int(vm.memory) * 1024, + "storage" : "".join(storage_disks), + } + + outfile = open(output_file, "w") + outfile.writelines(out) + outfile.close() + +vmconfig.register_parser(virtimage_parser) diff --git a/virtconv/parsers/vmx.py b/virtconv/parsers/vmx.py new file mode 100644 --- /dev/null +++ b/virtconv/parsers/vmx.py @@ -0,0 +1,107 @@ +# +# Copyright 2008 Sun Microsystems, Inc. All rights reserved. +# Use is subject to license terms. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA. +# + +import virtconv.vmconfig as vmconfig + +class vmx_parser(vmconfig.parser): + """ + Support for VMWare .vmx files. Note that documentation is + particularly sparse on this format, with pretty much the best + resource being http://sanbarrow.com/vmx.html + """ + + name = "vmx" + suffix = ".vmx" + + @staticmethod + def identify_file(input_file): + """ + Return True if the given file is of this format. + """ + raise NotImplementedError + + @staticmethod + def import_file(input_file): + """ + Import a configuration file. Raises if the file couldn't be + opened, or parsing otherwise failed. + """ + + vm = vmconfig.vm() + + infile = open(input_file, "r") + contents = infile.readlines() + infile.close() + + lines = [] + + # strip out comment and blank lines for easy splitting of values + for line in contents: + if not line.strip() or line.startswith("#"): + continue + else: + lines.append(line) + + config = {} + + # split out all remaining entries of key = value form + for (line_nr, line) in enumerate(lines): + try: + before_eq, after_eq = line.split("=", 1) + key = before_eq.replace(" ","") + value = after_eq.replace('"',"") + value = value.strip() + config[key] = value + except: + raise Exception("Syntax error at line %d: %s" % + (line_nr + 1, line.strip())) + + if not config.get("displayName"): + raise ValueError("No displayName defined in \"%s\"" % input_file) + vm.name = config.get("displayName") + + vm.memory = config.get("memsize") + vm.description = config.get("annotation") + vm.nr_vcpus = config.get("numvcpus") + + # FIXME: this should probably be a lot smarter. I don't think + # this even gets disk numbering right. + disks = [ d for d in config.values() if d.endswith(".vmdk") ] + + for (number, path) in enumerate(disks): + vm.disks += [ vmconfig.disk(path, number, vmconfig.DISK_TYPE_VMDK) ] + + vm.validate() + return vm + + @staticmethod + def export_file(vm, output_file): + """ + Export a configuration file. + @vm vm configuration instance + @file Output file + + Raises ValueError if configuration is not suitable, or another + exception on failure to write the output file. + """ + + raise NotImplementedError + +vmconfig.register_parser(vmx_parser) diff --git a/virtconv/vmconfig.py b/virtconv/vmconfig.py new file mode 100644 --- /dev/null +++ b/virtconv/vmconfig.py @@ -0,0 +1,193 @@ +# +# Copyright 2008 Sun Microsystems, Inc. All rights reserved. +# Use is subject to license terms. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA. +# + +import os + +_parsers = [ ] + +VM_TYPE_PV = 0 +VM_TYPE_HVM = 1 + +DISK_TYPE_RAW = 0 +DISK_TYPE_VMDK = 1 + +disk_suffixes = { + DISK_TYPE_RAW: ".img", + DISK_TYPE_VMDK: ".vmdk", +} + +qemu_formats = { + DISK_TYPE_RAW: "raw", + DISK_TYPE_VMDK: "vmdk", +} + +class disk(object): + """Definition of an individual disk instance.""" + + def __init__(self, path = None, number = None, type = None): + self.path = path + self.number = number + self.type = type + + def convert(self, input_dir, output_dir, output_type): + """ + Convert a disk into the requested format if possible, in the + given output directory. Raises NotImplementedError or other + failures. + """ + + if self.type == output_type: + return + + if output_type != DISK_TYPE_RAW: + raise NotImplementedError("Cannot convert to disk type %d" % + output_type) + + infile = self.path + + if not os.path.isabs(infile): + infile = os.path.join(input_dir, infile) + + outfile = self.path + + if os.path.isabs(outfile): + outfile = os.path.basename(outfile) + + outfile = outfile.replace(disk_suffixes[self.type], + disk_suffixes[output_type]).strip() + + convert_cmd = ("qemu-img convert \"%s\" -O %s \"%s\"" % + (infile, qemu_formats[output_type], + os.path.join(output_dir, outfile))) + + os.system(convert_cmd) + + # Note: this is the *relative* path still + self.path = outfile + self.type = output_type + + +class vm(object): + """ + Generic configuration for a particular VM instance. + + At export, a plugin is guaranteed to have the at least the following + values set (any others needed should be checked for, raising + ValueError on failure): + + vm.name + vm.description (defaults to empty string) + vm.nr_vcpus (defaults to 1) + vm.type + vm.arch + + If vm.memory is set, it is in Mb units. + """ + + name = None + suffix = None + + def __init__(self): + self.name = None + self.description = None + self.memory = None + self.nr_vcpus = None + self.disks = [ ] + self.type = VM_TYPE_HVM + self.arch = "i686" # FIXME? + + def validate(self): + """ + Validate all parameters, and fix up any unset values to meet the + guarantees we make above. + """ + + if not self.name: + raise ValueError("VM name is not set") + if not self.description: + self.description = "" + if not self.nr_vcpus: + self.nr_vcpus = 1 + if not self.type: + raise ValueError("VM type is not set") + if not self.arch: + raise ValueError("VM arch is not set") + + +class parser(object): + """ + Base class for particular config file format definitions of + a VM instance. + + Warning: this interface is not (yet) considered stable and may + change at will. + """ + + @staticmethod + def identify_file(input_file): + """ + Return True if the given file is of this format. + """ + raise NotImplementedError + + @staticmethod + def import_file(input_file): + """ + Import a configuration file. Raises if the file couldn't be + opened, or parsing otherwise failed. + """ + raise NotImplementedError + + @staticmethod + def export_file(vm, output_file): + """ + Export a configuration file. + @vm vm configuration instance + @output_file Output file + + Raises ValueError if configuration is not suitable, or another + exception on failure to write the output file. + """ + raise NotImplementedError + + +def register_parser(parser): + """ + Register a particular config format parser. This should be called by each + config plugin on import. + """ + + global _parsers + _parsers += [ parser ] + +def find_parser_by_name(name): + """ + Return the parser of the given name + """ + return [p for p in _parsers if p.name == name][0] or None + +def find_parser_by_file(input_file): + """ + Return the parser that is capable of comprehending the given file. + """ + for p in _parsers: + if p.identify_file(input_file): + return p + return None _______________________________________________ et-mgmt-tools mailing list et-mgmt-tools@xxxxxxxxxx https://www.redhat.com/mailman/listinfo/et-mgmt-tools