# # Classes for building disk device xml # # Copyright 2006-2008 Red Hat, Inc. # Jeremy Katz # # 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, stat, statvfs import libxml2 import logging import libvirt import __builtin__ import util import Storage from VirtualDevice import VirtualDevice from virtinst import _virtinst as _ class VirtualDisk(VirtualDevice): DRIVER_FILE = "file" DRIVER_PHY = "phy" DRIVER_TAP = "tap" DRIVER_TAP_RAW = "aio" DRIVER_TAP_QCOW = "qcow" DRIVER_TAP_VMDK = "vmdk" DEVICE_DISK = "disk" DEVICE_CDROM = "cdrom" DEVICE_FLOPPY = "floppy" devices = [DEVICE_DISK, DEVICE_CDROM, DEVICE_FLOPPY] TYPE_FILE = "file" TYPE_BLOCK = "block" types = [TYPE_FILE, TYPE_BLOCK] def __init__(self, path=None, size=None, transient=False, type=None, device=DEVICE_DISK, driverName=None, driverType=None, readOnly=False, sparse=True, conn=None, volobj=None, volinstall=None): """@param path: is the path to the disk image. @type path: str @param size: size of local file to create @type size: long (in gigabytes) @param transient: test @param type: media type (file, block, ...) @type type: str @param device: none @param driverName: none @param driverType: none @param readOnly: none @param sparse: none @param conn: none @param volobj: none @param volinstall: none""" VirtualDevice.__init__(self, conn=conn) self.set_read_only(readOnly, validate=False) self.set_sparse(sparse, validate=False) self.set_type(type, validate=False) self.set_device(device, validate=False) self.set_path(path, validate=False) self.set_size(size, validate=False) self.transient = transient self._driverName = driverName self._driverType = driverType self.target = None self.volobj = volobj self.volinstall = volinstall self.__validate_params() def __repr__(self): return "%s:%s" %(self.type, self.path) def get_path(self): return self._path def set_path(self, val, validate=True): if val is not None: self._check_str(val, "path") val = os.path.abspath(val) self.__validate_wrapper("_path", val, validate) path = property(get_path, set_path) def get_size(self): return self._size def set_size(self, val, validate=True): if val is not None: if type(val) not in [int, float, long] or val < 0: raise ValueError, _("'size' must be a number greater than 0.") self.__validate_wrapper("_size", val, validate) size = property(get_size, set_size) def get_type(self): return self._type def set_type(self, val, validate=True): if val is not None: self._check_str(val, "type") if val not in self.types: raise ValueError, _("Unknown storage type '%s'" % val) self.__validate_wrapper("_type", val, validate) type = property(get_type, set_type) def get_device(self): return self._device def set_device(self, val, validate=True): self._check_str(val, "device") if val not in self.devices: raise ValueError, _("Unknown device type '%s'" % val) self.__validate_wrapper("_device", val, validate) device = property(get_device, set_device) def get_driver_name(self): return self._driverName driver_name = property(get_driver_name) def get_driver_type(self): return self._driverType driver_type = property(get_driver_type) def get_sparse(self): return self._sparse def set_sparse(self, val, validate=True): self._check_bool(val, "sparse") self.__validate_wrapper("_sparse", val, validate) sparse = property(get_sparse, set_sparse) def get_read_only(self): return self._readOnly def set_read_only(self, val, validate=True): self._check_bool(val, "read_only") self.__validate_wrapper("_readOnly", val, validate) read_only = property(get_read_only, set_read_only) # Validation assistance methods def __validate_wrapper(self, varname, newval, validate=True): try: orig = getattr(self, varname) except: orig = newval setattr(self, varname, newval) if validate: try: self.__validate_params() except: setattr(self, varname, orig) raise # Detect file or block type from passed storage parameters def __set_dev_type(self): dtype = None if self.volobj: # vol info is [ vol type (file or block), capacity, allocation ] t = self.volobj.info()[0] if t == libvirt.VIR_STORAGE_VOL_FILE: dtype = self.TYPE_FILE elif t == libvirt.VIR_STORAGE_VOL_BLOCK: dtype = self.TYPE_BLOCK else: raise ValueError, _("Unknown storage volume type.") elif self.volinstall: if isinstance(self.volinstall, Storage.FileVolume): dtype = self.TYPE_FILE else: raise ValueError, _("Unknown dev type for volinstall.") elif self.path: if stat.S_ISBLK(os.stat(self.path)[stat.ST_MODE]): dtype = self.TYPE_BLOCK else: dtype = self.TYPE_FILE logging.debug("Detected storage as type '%s'" % dtype) if self.type is not None and dtype != self.type: raise ValueError(_("Passed type '%s' does not match detected " "storage type '%s'" % (self.type, dtype))) self.set_type(dtype, validate=False) def __validate_params(self): if self._is_remote() and not (self.volobj or self.volinstall): raise ValueError, _("Must specify libvirt managed storage if on " "a remote connection") if self.device == self.DEVICE_CDROM: logging.debug("Forcing '%s' device as read only." % self.device) self.set_read_only(True, validate=False) # Only floppy or cdrom can be created w/o media if self.path is None and not self.volobj and not self.volinstall: if self.device != self.DEVICE_FLOPPY and \ self.device != self.DEVICE_CDROM: raise ValueError, _("Device type '%s' requires a path") % \ self.device # If no path, our work is done return True if self.volinstall: logging.debug("Overwriting 'size' with 'capacity' from " "passed StorageVolume") self.set_size(self.volinstall.capacity*1024*1024*1024, validate=False) if self.volobj or self.volinstall or self._is_remote(): logging.debug("Using storage api objects for VirtualDisk") using_path = False else: logging.debug("Using self.path for VirtualDisk.") using_path = True if ((using_path and os.path.exists(self.path)) or self.volobj): logging.debug("VirtualDisk storage exists.") if using_path and os.path.isdir(self.path): raise ValueError, _("The path must be a file or a device," " not a directory") self.__set_dev_type() return True logging.debug("VirtualDisk storage does not exist.") if self.device == self.DEVICE_FLOPPY or \ self.device == self.DEVICE_CDROM: raise ValueError, _("Cannot create storage for %s device.") % \ self.device if using_path: # Not true for api? if self.type is self.TYPE_BLOCK: raise ValueError, _("Local block device path must exist.") self.set_type(self.TYPE_FILE, validate=False) # Path doesn't exist: make sure we have write access to dir if not os.access(os.path.dirname(self.path), os.W_OK): raise ValueError, _("No write access to directory '%s'") % \ os.path.dirname(self.path) else: self.__set_dev_type() if not self.size: raise ValueError, _("'size' is required for non-existent disks") ret = self.is_size_conflict() if ret[0]: raise ValueError, ret[1] elif ret[1]: logging.warn(ret[1]) def setup(self, progresscb): """ Build storage media if required""" if self.volobj: return elif self.volinst: self.volinst.install(meter=progresscb) return elif self.type == VirtualDisk.TYPE_FILE and self.path is not None \ and not os.path.exists(self.path): size_bytes = long(self.size * 1024L * 1024L * 1024L) progresscb.start(filename=self.path,size=long(size_bytes), \ text=_("Creating storage file...")) fd = None try: try: fd = os.open(self.path, os.O_WRONLY | os.O_CREAT) if self.sparse: os.lseek(fd, size_bytes, 0) os.write(fd, '\x00') progresscb.update(self.size) else: buf = '\x00' * 1024 * 1024 # 1 meg of nulls for i in range(0, long(self.size * 1024L)): os.write(fd, buf) progresscb.update(long(i * 1024L * 1024L)) except OSError, e: raise RuntimeError, _("Error creating diskimage %s: %s" % \ (self.path, str(e))) finally: if fd is not None: os.close(fd) progresscb.end(size_bytes) # FIXME: set selinux context? def get_xml_config(self, disknode): typeattr = 'file' if self.type == VirtualDisk.TYPE_BLOCK: typeattr = 'dev' path = self.path if self.volobj: path = self.volobj.path() elif self.volinstall: path = self.volinstall.target_path if path: path = util.xml_escape(path) ret = " \n" % { "type": self.type, "device": self.device } if not(self.driver_name is None): if self.driver_type is None: ret += " \n" % { "name": self.driver_name } else: ret += " \n" % { "name": self.driver_name, "type": self.driver_type } if path is not None: ret += " \n" % { "typeattr": typeattr, "disk": path } if self.target is not None: disknode = self.target ret += " \n" % { "disknode": disknode } if self.read_only: ret += " \n" ret += " \n" return ret def is_size_conflict(self): """reports if disk size conflicts with available space returns a two element tuple: first element is True if fatal conflict occurs second element is a string description of the conflict or None Non fatal conflicts (sparse disk exceeds available space) will return (False, "description of collision")""" if self.volobj or self.size is None or not self.path \ or os.path.exists(self.path) or self.type != self.TYPE_FILE: return (False, None) if self.volinstall: return self.volinstall.is_size_conflict() ret = False msg = None vfs = os.statvfs(os.path.dirname(self.path)) avail = vfs[statvfs.F_FRSIZE] * vfs[statvfs.F_BAVAIL] need = long(self.size * 1024L * 1024L * 1024L) if need > avail: if self.sparse: msg = _("The filesystem will not have enough free space" " to fully allocate the sparse file when the guest" " is running.") else: ret = True msg = _("There is not enough free space to create the disk.") if msg: msg += _(" %d M requested > %d M available") % \ ((need / (1024*1024)), (avail / (1024*1024))) return (ret, msg) def is_conflict_disk(self, conn): vms = [] # get working domain's name ids = conn.listDomainsID(); for id in ids: try: vm = conn.lookupByID(id) vms.append(vm) except libvirt.libvirtError: # guest probably in process of dieing logging.warn("Failed to lookup domain id %d" % id) # get defined domain names = conn.listDefinedDomains() for name in names: try: vm = conn.lookupByName(name) vms.append(vm) except libvirt.libvirtError: # guest probably in process of dieing logging.warn("Failed to lookup domain name %s" % name) path = self.path if self.volobj: path = self.volobj.path() elif self.volinstall: path = self.volinstall.target_path if path: path = util.xml_escape(path) count = 0 for vm in vms: doc = None try: doc = libxml2.parseDoc(vm.XMLDesc(0)) except: continue ctx = doc.xpathNewContext() try: try: count += ctx.xpathEval("count(/domain/devices/disk/source[@dev='%s'])" % path) count += ctx.xpathEval("count(/domain/devices/disk/source[@file='%s'])" % path) except: continue finally: if ctx is not None: ctx.xpathFreeContext() if doc is not None: doc.freeDoc() if count > 0: return True else: return False # Back compat class to avoid ABI break class XenDisk(VirtualDisk): pass