virtconv: ovf: convert to ElementTree

This commit is contained in:
Cole Robinson 2018-02-13 15:53:07 -05:00
parent fe728f899d
commit ab884b620f

View File

@ -20,8 +20,7 @@
import logging import logging
import os import os
import xml.etree.ElementTree
import libxml2
import virtinst import virtinst
@ -75,54 +74,13 @@ DEVICE_GRAPHICS = "24"
# http://www.dmtf.org/standards/documents/CIM/DSP0004.pdf # http://www.dmtf.org/standards/documents/CIM/DSP0004.pdf
OVF_NAMESPACES = {
def _ovf_register_namespace(ctx): "ovf": "http://schemas.dmtf.org/ovf/envelope/1",
ctx.xpathRegisterNs("ovf", "http://schemas.dmtf.org/ovf/envelope/1") "ovfenv": "http://schemas.dmtf.org/ovf/environment/1",
ctx.xpathRegisterNs("ovfenv", "http://schemas.dmtf.org/ovf/environment/1") "rasd": "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData",
ctx.xpathRegisterNs("rasd", "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData") "vssd": "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData",
ctx.xpathRegisterNs("vssd", "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData") "vmw": "http://www.vmware.com/schema/ovf",
ctx.xpathRegisterNs("vmw", "http://www.vmware.com/schema/ovf") }
ctx.xpathRegisterNs("xsi", "http://www.w3.org/2001/XMLSchema-instance")
def node_list(node):
child_list = []
child = node.children
while child:
child_list.append(child)
child = child.next
return child_list
def _get_child_content(parent_node, child_name):
for node in node_list(parent_node):
if node.name == child_name:
return node.content
return None
def _xml_parse_wrapper(xml, parse_func, *args, **kwargs):
"""
Parse the passed xml string into an xpath context, which is passed
to parse_func, along with any extra arguments.
"""
doc = None
ctx = None
ret = None
try:
doc = libxml2.parseDoc(xml)
ctx = doc.xpathNewContext()
_ovf_register_namespace(ctx)
ret = parse_func(doc, ctx, *args, **kwargs)
finally:
if ctx is not None:
ctx.xpathFreeContext()
if doc is not None:
doc.freeDoc()
return ret
def _convert_alloc_val(ignore, val): def _convert_alloc_val(ignore, val):
@ -145,111 +103,88 @@ def _convert_alloc_val(ignore, val):
return int(val) return int(val)
def _import_file(doc, ctx, conn, input_file): def _convert_bool_val(val):
ignore = doc if str(val).lower() == "false":
def xpath_str(path): return False
ret = ctx.xpathEval(path) elif str(val).lower() == "true":
result = None return True
if ret is not None:
if isinstance(ret, list):
if len(ret) >= 1:
result = ret[0].content
else:
result = ret
return result
def bool_val(val): return False
if str(val).lower() == "false":
return False
elif str(val).lower() == "true":
return True
def _find(_node, _xpath):
return _node.find(_xpath, namespaces=OVF_NAMESPACES)
def _findall(_node, _xpath):
return _node.findall(_xpath, namespaces=OVF_NAMESPACES)
def _text(_node):
if _node is not None:
return _node.text
def _lookup_disk_path(root, path):
"""
Map the passed HostResource ID to the actual host disk path
"""
ref = None
def _path_has_prefix(prefix):
if path.startswith(prefix):
return path[len(prefix):]
if path.startswith("ovf:" + prefix):
return path[len("ovf:" + prefix):]
return False return False
def xpath_nodechildren(path): if _path_has_prefix("/disk/"):
# Return the children of the first node found by the xpath disk_ref = _path_has_prefix("/disk/")
nodes = ctx.xpathEval(path) xpath = "./ovf:DiskSection/ovf:Disk[@ovf:diskId='%s']" % disk_ref
if not nodes: dnode = _find(root, xpath)
return []
return node_list(nodes[0])
def _lookup_disk_path(path): if dnode is None:
fmt = "vmdk" raise ValueError(_("Unknown disk reference id '%s' "
ref = None "for path %s.") % (path, disk_ref))
def _path_has_prefix(prefix): ref = dnode.attrib["{%s}fileRef" % OVF_NAMESPACES["ovf"]]
if path.startswith(prefix): elif _path_has_prefix("/file/"):
return path[len(prefix):] ref = _path_has_prefix("/file/")
if path.startswith("ovf:" + prefix):
return path[len("ovf:" + prefix):]
return False
if _path_has_prefix("/disk/"): else:
disk_ref = _path_has_prefix("/disk/") raise ValueError(_("Unknown storage path type %s.") % path)
xpath = (_make_section_xpath(envbase, "DiskSection") +
"/ovf:Disk[@ovf:diskId='%s']" % disk_ref)
if not ctx.xpathEval(xpath): xpath = "./ovf:References/ovf:File[@ovf:id='%s']" % ref
raise ValueError(_("Unknown disk reference id '%s' " refnode = _find(root, xpath)
"for path %s.") % (path, disk_ref)) if refnode is None:
raise ValueError(_("Unknown reference id '%s' "
"for path %s.") % (ref, path))
ref = xpath_str(xpath + "/@ovf:fileRef") return refnode.attrib["{%s}href" % OVF_NAMESPACES["ovf"]]
elif _path_has_prefix("/file/"):
ref = _path_has_prefix("/file/")
else: def _import_file(conn, input_file):
raise ValueError(_("Unknown storage path type %s.") % path) """
Parse the OVF file and generate a virtinst.Guest object from it
xpath = (envbase + "/ovf:References/ovf:File[@ovf:id='%s']" % ref) """
root = xml.etree.ElementTree.parse(input_file).getroot()
if not ctx.xpathEval(xpath): vsnode = _find(root, "./ovf:VirtualSystem")
raise ValueError(_("Unknown reference id '%s' " vhnode = _find(vsnode, "./ovf:VirtualHardwareSection")
"for path %s.") % (ref, path))
return xpath_str(xpath + "/@ovf:href"), fmt
is_ovirt_format = False
envbase = "/ovf:Envelope[1]"
vsbase = envbase + "/ovf:VirtualSystem"
if not ctx.xpathEval(vsbase):
vsbase = envbase + "/ovf:Content[@xsi:type='ovf:VirtualSystem_Type']"
is_ovirt_format = True
def _make_section_xpath(base, section_name):
if is_ovirt_format:
return (base +
"/ovf:Section[@xsi:type='ovf:%s_Type']" % section_name)
return base + "/ovf:%s" % section_name
osbase = _make_section_xpath(vsbase, "OperatingSystemSection")
vhstub = _make_section_xpath(vsbase, "VirtualHardwareSection")
if not ctx.xpathEval(vsbase):
raise RuntimeError("Did not find any VirtualSystem section")
if not ctx.xpathEval(vhstub):
raise RuntimeError("Did not find any VirtualHardwareSection")
vhbase = vhstub + "/ovf:Item[rasd:ResourceType='%s']"
# General info # General info
name = xpath_str(vsbase + "/ovf:Name") name = _text(vsnode.find("./ovf:Name", OVF_NAMESPACES))
desc = xpath_str(vsbase + "/ovf:AnnotationSection/ovf:Annotation") desc = _text(vsnode.find("./ovf:AnnotationSection/ovf:Annotation",
OVF_NAMESPACES))
if not desc: if not desc:
desc = xpath_str(vsbase + "/ovf:Description") desc = _text(vsnode.find("./ovf:Description", OVF_NAMESPACES))
vcpus = xpath_str((vhbase % DEVICE_CPU) + "/rasd:VirtualQuantity")
sockets = xpath_str((vhbase % DEVICE_CPU) + "/rasd:num_of_sockets")
cores = xpath_str((vhbase % DEVICE_CPU) + "/rasd:num_of_cores")
mem = xpath_str((vhbase % DEVICE_MEMORY) + "/rasd:VirtualQuantity")
alloc_mem = xpath_str((vhbase % DEVICE_MEMORY) +
"/rasd:AllocationUnits")
os_id = xpath_str(osbase + "/@id") vhxpath = "./ovf:Item[rasd:ResourceType='%s']"
os_version = xpath_str(osbase + "/@version") vcpus = _text(_find(vhnode,
# This is the VMWare OS name (vhxpath % DEVICE_CPU) + "/rasd:VirtualQuantity"))
os_vmware = xpath_str(osbase + "/@osType") mem = _text(_find(vhnode,
(vhxpath % DEVICE_MEMORY) + "/rasd:VirtualQuantity"))
logging.debug("OS parsed as: id=%s version=%s vmware=%s", alloc_mem = _text(_find(vhnode,
os_id, os_version, os_vmware) (vhxpath % DEVICE_MEMORY) + "/rasd:AllocationUnits"))
# Sections that we handle # Sections that we handle
# NetworkSection is ignored, since I don't have an example of # NetworkSection is ignored, since I don't have an example of
@ -258,49 +193,48 @@ def _import_file(doc, ctx, conn, input_file):
"VirtualSystem"] "VirtualSystem"]
# Check for unhandled 'required' sections # Check for unhandled 'required' sections
for env_node in xpath_nodechildren(envbase): for env_node in root.findall("./"):
if env_node.name in parsed_sections: if any([p for p in parsed_sections if p in env_node.tag]):
continue
elif env_node.isText():
continue continue
logging.debug("Unhandled XML section '%s'", logging.debug("Unhandled XML section '%s'",
env_node.name) env_node.tag)
if not bool_val(env_node.prop("required")): if not _convert_bool_val(env_node.attrib.get("required")):
continue continue
raise Exception(_("OVF section '%s' is listed as " raise Exception(_("OVF section '%s' is listed as "
"required, but parser doesn't know " "required, but parser doesn't know "
"how to handle it.") % env_node.name) "how to handle it.") % env_node.name)
disk_buses = {} disk_buses = {}
for node in ctx.xpathEval(vhbase % DEVICE_IDE_BUS): for node in _findall(vhnode, vhxpath % DEVICE_IDE_BUS):
instance_id = _get_child_content(node, "InstanceID") instance_id = _text(_find(node, "rasd:InstanceID"))
disk_buses[instance_id] = "ide" disk_buses[instance_id] = "ide"
for node in ctx.xpathEval(vhbase % DEVICE_SCSI_BUS): for node in _findall(vhnode, vhxpath % DEVICE_SCSI_BUS):
instance_id = _get_child_content(node, "InstanceID") instance_id = _text(_find(node, "rasd:InstanceID"))
disk_buses[instance_id] = "scsi" disk_buses[instance_id] = "scsi"
ifaces = [] ifaces = []
for node in ctx.xpathEval(vhbase % DEVICE_ETHERNET): for node in _findall(vhnode, vhxpath % DEVICE_ETHERNET):
iface = virtinst.VirtualNetworkInterface(conn) iface = virtinst.VirtualNetworkInterface(conn)
# XXX: Just ignore 'source' info and choose the default # Just ignore 'source' info for now and choose the default
net_model = _get_child_content(node, "ResourceSubType") net_model = _text(_find(node, "rasd:ResourceSubType"))
if net_model and not net_model.isdigit(): if net_model and not net_model.isdigit():
iface.model = net_model.lower() iface.model = net_model.lower()
iface.set_default_source() iface.set_default_source()
ifaces.append(iface) ifaces.append(iface)
disks = [] disks = []
for node in ctx.xpathEval(vhbase % DEVICE_DISK): for node in _findall(vhnode, vhxpath % DEVICE_DISK):
bus_id = _get_child_content(node, "Parent") bus_id = _text(_find(node, "rasd:Parent"))
path = _get_child_content(node, "HostResource") path = _text(_find(node, "rasd:HostResource"))
bus = disk_buses.get(bus_id, "ide") bus = disk_buses.get(bus_id, "ide")
fmt = "raw" fmt = "raw"
if path: if path:
path, fmt = _lookup_disk_path(path) path = _lookup_disk_path(root, path)
fmt = "vmdk"
disk = virtinst.VirtualDisk(conn) disk = virtinst.VirtualDisk(conn)
disk.path = path disk.path = path
@ -310,11 +244,7 @@ def _import_file(doc, ctx, conn, input_file):
disks.append(disk) disks.append(disk)
# XXX: Convert these OS values to something useful # Generate the Guest
ignore = os_version
ignore = os_id
ignore = os_vmware
guest = conn.caps.lookup_virtinst_guest() guest = conn.caps.lookup_virtinst_guest()
guest.installer = virtinst.ImportInstaller(conn) guest.installer = virtinst.ImportInstaller(conn)
@ -325,12 +255,6 @@ def _import_file(doc, ctx, conn, input_file):
guest.description = desc or None guest.description = desc or None
if vcpus: if vcpus:
guest.vcpus = int(vcpus) guest.vcpus = int(vcpus)
elif sockets or cores:
if sockets:
guest.cpu.sockets = int(sockets)
if cores:
guest.cpu.cores = int(cores)
guest.cpu.vcpus_from_topology()
if mem: if mem:
guest.memory = _convert_alloc_val(alloc_mem, mem) * 1024 guest.memory = _convert_alloc_val(alloc_mem, mem) * 1024
@ -356,29 +280,20 @@ class ovf_parser(parser_class):
""" """
Return True if the given file is of this format. Return True if the given file is of this format.
""" """
# Small heuristic to ensure we aren't attempting to identify
# a large .zip archive or similar
if os.path.getsize(input_file) > (1024 * 1024 * 2): if os.path.getsize(input_file) > (1024 * 1024 * 2):
return return
infile = open(input_file, "r")
xml = infile.read()
infile.close()
def parse_cb(doc, ctx):
ignore = doc
return bool(ctx.xpathEval("/ovf:Envelope"))
try: try:
return _xml_parse_wrapper(xml, parse_cb) root = xml.etree.ElementTree.parse(input_file).getroot()
except Exception as e: return root.tag == ("{%s}Envelope" % OVF_NAMESPACES["ovf"])
logging.debug("Error parsing OVF XML: %s", str(e)) except Exception:
logging.debug("Error parsing OVF XML", exc_info=True)
return False return False
@staticmethod @staticmethod
def export_libvirt(conn, input_file): def export_libvirt(conn, input_file):
infile = open(input_file, "r") logging.debug("Importing OVF XML:\n%s", open(input_file).read())
xml = infile.read() return _import_file(conn, input_file)
infile.close()
logging.debug("Importing OVF XML:\n%s", xml)
return _xml_parse_wrapper(xml, _import_file, conn, input_file)