285 lines
8.0 KiB
Python
285 lines
8.0 KiB
Python
#
|
|
# Copyright 2009, 2013 Red Hat, Inc.
|
|
#
|
|
# This work is licensed under the GNU GPLv2 or later.
|
|
# See the COPYING file in the top-level directory.
|
|
#
|
|
|
|
import os
|
|
import xml.etree.ElementTree
|
|
|
|
import virtinst
|
|
from virtinst import log
|
|
|
|
from .formats import parser_class
|
|
|
|
|
|
# Mapping of ResourceType value to device type
|
|
# http://konkretcmpi.org/cim218/CIM_ResourceAllocationSettingData.html
|
|
#
|
|
# "Other" [1]
|
|
# "Computer System" [2]
|
|
# "Processor" [3]
|
|
# "Memory" [4]
|
|
# "IDE Controller" [5]
|
|
# "Parallel SCSI HBA" [6]
|
|
# "FC HBA" [7]
|
|
# "iSCSI HBA" [8]
|
|
# "IB HCA" [9]
|
|
# "Ethernet Adapter" [10]
|
|
# "Other Network Adapter" [11]
|
|
# "I/O Slot" [12]
|
|
# "I/O Device" [13]
|
|
# "Floppy Drive" [14]
|
|
# "CD Drive" [15]
|
|
# "DVD drive" [16]
|
|
# "Disk Drive" [17]
|
|
# "Tape Drive" [18]
|
|
# "Storage Extent" [19]
|
|
# "Other storage device" [20]
|
|
# "Serial port" [21]
|
|
# "Parallel port" [22]
|
|
# "USB Controller" [23]
|
|
# "Graphics controller" [24]
|
|
# "IEEE 1394 Controller" [25]
|
|
# "Partitionable Unit" [26]
|
|
# "Base Partitionable Unit" [27]
|
|
# "Power" [28]
|
|
# "Cooling Capacity" [29]
|
|
# "Ethernet Switch Port" [30]
|
|
|
|
|
|
DEVICE_CPU = "3"
|
|
DEVICE_MEMORY = "4"
|
|
DEVICE_IDE_BUS = "5"
|
|
DEVICE_SCSI_BUS = "6"
|
|
DEVICE_ETHERNET = "10"
|
|
DEVICE_DISK = "17"
|
|
DEVICE_GRAPHICS = "24"
|
|
|
|
# AllocationUnits mapping can be found in Appendix C here:
|
|
# https://www.dmtf.org/standards/documents/CIM/DSP0004.pdf
|
|
|
|
|
|
OVF_NAMESPACES = {
|
|
"ovf": "http://schemas.dmtf.org/ovf/envelope/1",
|
|
"ovfenv": "http://schemas.dmtf.org/ovf/environment/1",
|
|
"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",
|
|
"vmw": "http://www.vmware.com/schema/ovf",
|
|
}
|
|
|
|
|
|
def _convert_alloc_val(ignore, val):
|
|
# This is a hack, but should we really have to decode
|
|
# allocation units = "bytes * 2^20"?
|
|
val = float(val)
|
|
|
|
if val > 100000000:
|
|
# Assume bytes
|
|
return int(round(val / 1024.0 / 1024.0))
|
|
|
|
elif val > 100000:
|
|
# Assume kilobytes
|
|
return int(round(val / 1024.0))
|
|
|
|
elif val < 32:
|
|
# Assume GiB
|
|
return int(val * 1024)
|
|
|
|
return int(val)
|
|
|
|
|
|
def _convert_bool_val(val):
|
|
if str(val).lower() == "false":
|
|
return False
|
|
elif str(val).lower() == "true":
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
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
|
|
|
|
if _path_has_prefix("/disk/"):
|
|
disk_ref = _path_has_prefix("/disk/")
|
|
xpath = "./ovf:DiskSection/ovf:Disk[@ovf:diskId='%s']" % disk_ref
|
|
dnode = _find(root, xpath)
|
|
|
|
if dnode is None:
|
|
raise ValueError(_("Unknown disk reference id '%s' "
|
|
"for path %s.") % (path, disk_ref))
|
|
|
|
ref = dnode.attrib["{%s}fileRef" % OVF_NAMESPACES["ovf"]]
|
|
elif _path_has_prefix("/file/"):
|
|
ref = _path_has_prefix("/file/")
|
|
|
|
else:
|
|
raise ValueError(_("Unknown storage path type %s.") % path)
|
|
|
|
xpath = "./ovf:References/ovf:File[@ovf:id='%s']" % ref
|
|
refnode = _find(root, xpath)
|
|
if refnode is None:
|
|
raise ValueError(_("Unknown reference id '%s' "
|
|
"for path %s.") % (ref, path))
|
|
|
|
return refnode.attrib["{%s}href" % OVF_NAMESPACES["ovf"]]
|
|
|
|
|
|
def _import_file(conn, input_file):
|
|
"""
|
|
Parse the OVF file and generate a virtinst.Guest object from it
|
|
"""
|
|
root = xml.etree.ElementTree.parse(input_file).getroot()
|
|
vsnode = _find(root, "./ovf:VirtualSystem")
|
|
vhnode = _find(vsnode, "./ovf:VirtualHardwareSection")
|
|
|
|
# General info
|
|
name = _text(vsnode.find("./ovf:Name", OVF_NAMESPACES))
|
|
desc = _text(vsnode.find("./ovf:AnnotationSection/ovf:Annotation",
|
|
OVF_NAMESPACES))
|
|
if not desc:
|
|
desc = _text(vsnode.find("./ovf:Description", OVF_NAMESPACES))
|
|
|
|
vhxpath = "./ovf:Item[rasd:ResourceType='%s']"
|
|
vcpus = _text(_find(vhnode,
|
|
(vhxpath % DEVICE_CPU) + "/rasd:VirtualQuantity"))
|
|
mem = _text(_find(vhnode,
|
|
(vhxpath % DEVICE_MEMORY) + "/rasd:VirtualQuantity"))
|
|
alloc_mem = _text(_find(vhnode,
|
|
(vhxpath % DEVICE_MEMORY) + "/rasd:AllocationUnits"))
|
|
|
|
# Sections that we handle
|
|
# NetworkSection is ignored, since I don't have an example of
|
|
# a valid section in the wild.
|
|
parsed_sections = ["References", "DiskSection", "NetworkSection",
|
|
"VirtualSystem"]
|
|
|
|
# Check for unhandled 'required' sections
|
|
for env_node in root.findall("./"):
|
|
if any([p for p in parsed_sections if p in env_node.tag]):
|
|
continue
|
|
|
|
log.debug("Unhandled XML section '%s'",
|
|
env_node.tag)
|
|
|
|
if not _convert_bool_val(env_node.attrib.get("required")):
|
|
continue
|
|
raise Exception(_("OVF section '%s' is listed as "
|
|
"required, but parser doesn't know "
|
|
"how to handle it.") % env_node.name)
|
|
|
|
disk_buses = {}
|
|
for node in _findall(vhnode, vhxpath % DEVICE_IDE_BUS):
|
|
instance_id = _text(_find(node, "rasd:InstanceID"))
|
|
disk_buses[instance_id] = "ide"
|
|
for node in _findall(vhnode, vhxpath % DEVICE_SCSI_BUS):
|
|
instance_id = _text(_find(node, "rasd:InstanceID"))
|
|
disk_buses[instance_id] = "scsi"
|
|
|
|
ifaces = []
|
|
for node in _findall(vhnode, vhxpath % DEVICE_ETHERNET):
|
|
iface = virtinst.DeviceInterface(conn)
|
|
# Just ignore 'source' info for now and choose the default
|
|
net_model = _text(_find(node, "rasd:ResourceSubType"))
|
|
if net_model and not net_model.isdigit():
|
|
iface.model = net_model.lower()
|
|
iface.set_default_source()
|
|
ifaces.append(iface)
|
|
|
|
disks = []
|
|
for node in _findall(vhnode, vhxpath % DEVICE_DISK):
|
|
bus_id = _text(_find(node, "rasd:Parent"))
|
|
path = _text(_find(node, "rasd:HostResource"))
|
|
|
|
bus = disk_buses.get(bus_id, "ide")
|
|
fmt = "raw"
|
|
|
|
if path:
|
|
path = _lookup_disk_path(root, path)
|
|
fmt = "vmdk"
|
|
|
|
disk = virtinst.DeviceDisk(conn)
|
|
disk.path = path
|
|
disk.driver_type = fmt
|
|
disk.bus = bus
|
|
disk.device = "disk"
|
|
disks.append(disk)
|
|
|
|
|
|
# Generate the Guest
|
|
guest = virtinst.Guest(conn)
|
|
if not name:
|
|
name = os.path.basename(input_file)
|
|
|
|
guest.name = name.replace(" ", "_")
|
|
guest.description = desc or None
|
|
if vcpus:
|
|
guest.vcpus = int(vcpus)
|
|
|
|
if mem:
|
|
guest.currentMemory = _convert_alloc_val(alloc_mem, mem) * 1024
|
|
|
|
for dev in ifaces + disks:
|
|
guest.add_device(dev)
|
|
|
|
return guest
|
|
|
|
|
|
class ovf_parser(parser_class):
|
|
"""
|
|
Support for OVF appliance configurations.
|
|
|
|
Whitepaper: https://www.vmware.com/pdf/ovf_whitepaper_specification.pdf
|
|
Spec: https://www.dmtf.org/standards/published_documents/DSP0243_1.0.0.pdf
|
|
"""
|
|
name = "ovf"
|
|
suffix = ".ovf"
|
|
|
|
@staticmethod
|
|
def identify_file(input_file):
|
|
"""
|
|
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):
|
|
return
|
|
|
|
try:
|
|
root = xml.etree.ElementTree.parse(input_file).getroot()
|
|
return root.tag == ("{%s}Envelope" % OVF_NAMESPACES["ovf"])
|
|
except Exception:
|
|
log.debug("Error parsing OVF XML", exc_info=True)
|
|
|
|
return False
|
|
|
|
@staticmethod
|
|
def export_libvirt(conn, input_file):
|
|
log.debug("Importing OVF XML:\n%s", open(input_file).read())
|
|
return _import_file(conn, input_file)
|