# Sample code to parse an image XML description and # spit out libvirt XML # # Copyright 2007, 2013 Red Hat, Inc. # David Lutterkort # # 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 logging import os import urlgrabber from virtinst import CapabilitiesParser from virtinst import util class Image(object): """The toplevel object representing a VM image""" def __init__(self, node=None, base=None, filename=None): self.storage = {} self.domain = None if filename is None: self.filename = None else: self.filename = os.path.abspath(filename) if base is None: if filename is not None: self.base = os.path.dirname(filename) if self.base == '': self.base = "." else: self.base = "." else: self.base = base self.name = None self.label = None self.descr = None self.version = None self.release = None if not node is None: self.parseXML(node) def abspath(self, p): """Turn P into an absolute path. Relative paths are taken relative to self.BASE""" return os.path.abspath(os.path.join(self.base, p)) def parseXML(self, node): self.name = xpathString(node, "name") self.label = xpathString(node, "label") self.descr = xpathString(node, "description") self.version = xpathString(node, "name/@version") self.release = xpathString(node, "name/@release") for d in node.xpathEval("storage/disk"): disk = Disk(d) if disk.file is None: disk.id = "disk%d.img" % len(self.storage) disk.file = "disk%d.img" % (len(self.storage) + 1) if disk.id in self.storage: raise RuntimeError("Disk file '%s' defined twice" % disk.file) self.storage[disk.id] = disk lm = node.xpathEval("domain") if len(lm) == 1: self.domain = Domain(lm[0]) else: raise RuntimeError(_("Expected exactly one 'domain' element")) # Connect the disk maps to the disk definitions for boot in self.domain.boots: for d in boot.drives: if d.disk_id not in self.storage: raise RuntimeError(_("Disk entry for '%s' not found") % d.disk_id) d.disk = self.storage[d.disk_id] class Domain(object): """The description of a virtual domain as part of an image""" def __init__(self, node=None): self.boots = [] self.vcpu = None self.memory = None self.interface = 0 self.graphics = None if not node is None: self.parseXML(node) def parseXML(self, node): self.boots = [Boot(b) for b in node.xpathEval("boot")] self.vcpu = xpathString(node, "devices/vcpu", 1) tmpmem = xpathString(node, "devices/memory") self.interface = int(node.xpathEval("count(devices/interface)")) self.graphics = node.xpathEval("count(devices/graphics)") > 0 if tmpmem is not None: try: self.memory = int(tmpmem) except ValueError: raise RuntimeError(_("Memory must be an integer, " "but is '%s'") % self.memory) else: tmpmem = 0 class ImageFeatures(CapabilitiesParser.Features): def __init__(self, node=None): CapabilitiesParser.Features.__init__(self, node) def _extractFeature(self, feature, d, n): state = xpathString(n, "@state", "on") if state == "on": d[feature] = CapabilitiesParser.FEATURE_ON elif state == "off": d[feature] = CapabilitiesParser.FEATURE_OFF else: raise RuntimeError("The state for feature %s must be " "either 'on' or 'off', but is '%s'" % (feature, state)) class Boot(object): """The overall description of how the image can be booted, including required capabilities of the host and mapping of disks into the VM""" def __init__(self, node=None): # 'xen' or 'hvm' self.type = None # Either 'pygrub' or nothing; might have others in the future # For HVM, figure outhte right loader based on the guest self.loader = None # Only used for hvm self.bootdev = None self.kernel = None self.initrd = None self.cmdline = None self.drives = [] self.arch = None self.features = ImageFeatures() if not node is None: self.parseXML(node) def parseXML(self, node): self.type = xpathString(node, "@type") self.loader = xpathString(node, "os/loader") self.bootdev = xpathString(node, "os/loader/@dev") self.kernel = xpathString(node, "os/kernel") self.initrd = xpathString(node, "os/initrd") self.cmdline = xpathString(node, "os/cmdline") self.arch = util.sanitize_arch(xpathString(node, "guest/arch")) fl = node.xpathEval("guest/features") if len(fl) > 1: raise RuntimeError("Expected at most one element " "in %s boot descriptor for %s" % (self.type, self.arch)) elif len(fl) == 1: self.features = ImageFeatures(fl[0]) for d in node.xpathEval("drive"): self.drives.append(Drive(d)) validate(self.type is not None, "The boot type must be provided") validate(self.type == "hvm" or self.type == "xen", "Boot type must be 'xen' or 'hvm', but is %s" % self.type) validate(self.arch is not None, "Missing guest arch") validate(self.loader is None or self.loader == "pygrub", "Invalid loader %s" % self.loader) validate([None, "hd", "cdrom"].count(self.bootdev) > 0, "Invalid bootdev %s" % self.bootdev) # We should make sure that kernel/initrd/cmdline are only used for pv # and without a loader class Drive(object): """The mapping of a disk from the storage section to a virtual drive in a guest""" def __init__(self, node=None): self.disk_id = None self.target = None self.disk = None # Will point to the underlying Disk object if node: self.parseXML(node) def parseXML(self, node): self.disk_id = xpathString(node, "@disk") self.target = xpathString(node, "@target") class Disk(object): FORMAT_RAW = "raw" FORMAT_ISO = "iso" FORMAT_QCOW = "qcow" FORMAT_QCOW2 = "qcow2" FORMAT_VMDK = "vmdk" FORMAT_VDI = "vdi" USE_SYSTEM = "system" USE_USER = "user" USE_SCRATCH = "scratch" def __init__(self, node=None): self.id = None self.file = None self.format = None self.size = None self.use = None self.csum = {} if not node is None: self.parseXML(node) def parseXML(self, node): self.file = xpathString(node, "@file") self.id = xpathString(node, "@id", self.file) self.format = xpathString(node, "@format", Disk.FORMAT_RAW) self.size = xpathString(node, "@size") self.use = xpathString(node, "@use", Disk.USE_SYSTEM) for d in node.xpathEval("checksum"): csumtype = xpathString(d, "@type") csumvalue = xpathString(d, "") self.csum[csumtype] = csumvalue formats = [Disk.FORMAT_RAW, Disk.FORMAT_QCOW, Disk.FORMAT_QCOW2, Disk.FORMAT_VMDK, Disk.FORMAT_ISO, Disk.FORMAT_VDI] validate(formats.count(self.format) > 0, _("The format for disk %s must be one of %s") % (self.file, ",".join(formats))) def check_disk_signature(self, meter=None): try: import hashlib sha = None except: import sha hashlib = None if meter is None: meter = urlgrabber.progress.BaseMeter() m = None if hashlib: if "sha256" in self.csum: csumvalue = self.csum["sha256"] m = hashlib.sha256() elif "sha1" in self.csum: csumvalue = self.csum["sha1"] m = hashlib.sha1() else: if "sha1" in self.csum: csumvalue = self.csum["sha1"] m = sha.new() if not m: return meter_ct = 0 disk_size = os.path.getsize(self.file) meter.start(size=disk_size, text=_("Checking disk signature for %s" % self.file)) f = file(self.file) while 1: chunk = f.read(65536) if not chunk: break meter.update(meter_ct) meter_ct = meter_ct + 65536 m.update(chunk) checksum = m.hexdigest() if checksum != csumvalue: logging.debug(_("Disk signature for %s does not match " "Expected: %s Received: %s" % (self.file, csumvalue, checksum))) raise ValueError(_("Disk signature for %s does not " "match" % self.file)) def validate(cond, msg): if not cond: raise RuntimeError(msg) def xpathString(node, path, default=None): result = node.xpathEval("string(%s)" % path) if len(result) == 0: result = default return result def parse(xml, filename): """Parse the XML description of a VM image into a data structure. Returns an object of class Image. BASE should be the directory where the disk image files for this image can be found""" def cb(x): return Image(x, filename=filename) return util.parse_node_helper(xml, "image", cb, RuntimeError) def parse_file(filename): f = open(filename, "r") xml = f.read() f.close() return parse(xml, filename=filename)