mirror of
https://github.com/virt-manager/virt-manager.git
synced 2025-03-10 12:58:27 +03:00
We can't predict when it's expected or not, so just never raise the error and let libvirt error if the user is trying do things at create time.
1032 lines
33 KiB
Python
1032 lines
33 KiB
Python
#
|
|
# Classes for building disk device xml
|
|
#
|
|
# Copyright 2006-2008, 2012-2014 Red Hat, Inc.
|
|
# Jeremy Katz <katzj@redhat.com>
|
|
#
|
|
# 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
|
|
import stat
|
|
import pwd
|
|
import subprocess
|
|
import logging
|
|
import re
|
|
|
|
import urlgrabber.progress as progress
|
|
|
|
from . import diskbackend
|
|
from . import util
|
|
from .device import VirtualDevice
|
|
from .uri import URISplit
|
|
from .xmlbuilder import XMLProperty
|
|
|
|
|
|
def _qemu_sanitize_drvtype(phystype, fmt, manual_format=False):
|
|
"""
|
|
Sanitize libvirt storage volume format to a valid qemu driver type
|
|
"""
|
|
raw_list = ["iso"]
|
|
|
|
if phystype == VirtualDisk.TYPE_BLOCK:
|
|
if not fmt:
|
|
return VirtualDisk.DRIVER_QEMU_RAW
|
|
if fmt and not manual_format:
|
|
return VirtualDisk.DRIVER_QEMU_RAW
|
|
|
|
if fmt in raw_list:
|
|
return VirtualDisk.DRIVER_QEMU_RAW
|
|
|
|
return fmt
|
|
|
|
|
|
def _name_uid(user):
|
|
"""
|
|
Return UID for string username
|
|
"""
|
|
pwdinfo = pwd.getpwnam(user)
|
|
return pwdinfo[2]
|
|
|
|
|
|
def _is_dir_searchable(uid, username, path):
|
|
"""
|
|
Check if passed directory is searchable by uid
|
|
"""
|
|
try:
|
|
statinfo = os.stat(path)
|
|
except OSError:
|
|
return False
|
|
|
|
if uid == statinfo.st_uid:
|
|
flag = stat.S_IXUSR
|
|
elif uid == statinfo.st_gid:
|
|
flag = stat.S_IXGRP
|
|
else:
|
|
flag = stat.S_IXOTH
|
|
|
|
if bool(statinfo.st_mode & flag):
|
|
return True
|
|
|
|
# Check POSIX ACL (since that is what we use to 'fix' access)
|
|
cmd = ["getfacl", path]
|
|
try:
|
|
proc = subprocess.Popen(cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
out, err = proc.communicate()
|
|
except OSError:
|
|
logging.debug("Didn't find the getfacl command.")
|
|
return False
|
|
|
|
if proc.returncode != 0:
|
|
logging.debug("Cmd '%s' failed: %s", cmd, err)
|
|
return False
|
|
|
|
return bool(re.search("user:%s:..x" % username, out))
|
|
|
|
|
|
class VirtualDisk(VirtualDevice):
|
|
virtual_device_type = VirtualDevice.VIRTUAL_DEV_DISK
|
|
|
|
DRIVER_FILE = "file"
|
|
DRIVER_PHY = "phy"
|
|
DRIVER_TAP = "tap"
|
|
DRIVER_QEMU = "qemu"
|
|
driver_names = [DRIVER_FILE, DRIVER_PHY, DRIVER_TAP, DRIVER_QEMU]
|
|
|
|
DRIVER_QEMU_RAW = "raw"
|
|
# No list here, since there are many other valid values
|
|
|
|
DRIVER_TAP_RAW = "aio"
|
|
DRIVER_TAP_QCOW = "qcow"
|
|
DRIVER_TAP_VMDK = "vmdk"
|
|
DRIVER_TAP_VDISK = "vdisk"
|
|
DRIVER_TAP_QED = "qed"
|
|
driver_types = [DRIVER_TAP_RAW, DRIVER_TAP_QCOW,
|
|
DRIVER_TAP_VMDK, DRIVER_TAP_VDISK, DRIVER_TAP_QED]
|
|
|
|
CACHE_MODE_NONE = "none"
|
|
CACHE_MODE_WRITETHROUGH = "writethrough"
|
|
CACHE_MODE_WRITEBACK = "writeback"
|
|
CACHE_MODE_DIRECTSYNC = "directsync"
|
|
CACHE_MODE_UNSAFE = "unsafe"
|
|
cache_types = [CACHE_MODE_NONE, CACHE_MODE_WRITETHROUGH,
|
|
CACHE_MODE_WRITEBACK, CACHE_MODE_DIRECTSYNC, CACHE_MODE_UNSAFE]
|
|
|
|
DISCARD_MODE_IGNORE = "ignore"
|
|
DISCARD_MODE_UNMAP = "unmap"
|
|
discard_types = [DISCARD_MODE_IGNORE, DISCARD_MODE_UNMAP]
|
|
|
|
DEVICE_DISK = "disk"
|
|
DEVICE_LUN = "lun"
|
|
DEVICE_CDROM = "cdrom"
|
|
DEVICE_FLOPPY = "floppy"
|
|
devices = [DEVICE_DISK, DEVICE_LUN, DEVICE_CDROM, DEVICE_FLOPPY]
|
|
|
|
TYPE_FILE = "file"
|
|
TYPE_BLOCK = "block"
|
|
TYPE_DIR = "dir"
|
|
TYPE_VOLUME = "volume"
|
|
TYPE_NETWORK = "network"
|
|
types = [TYPE_FILE, TYPE_BLOCK, TYPE_DIR, TYPE_NETWORK]
|
|
|
|
IO_MODE_NATIVE = "native"
|
|
IO_MODE_THREADS = "threads"
|
|
io_modes = [IO_MODE_NATIVE, IO_MODE_THREADS]
|
|
|
|
error_policies = ["ignore", "stop", "enospace", "report"]
|
|
|
|
@staticmethod
|
|
def disk_type_to_xen_driver_name(disk_type):
|
|
"""
|
|
Convert a value of VirtualDisk.type to it's associated Xen
|
|
<driver name=/> property
|
|
"""
|
|
if disk_type == VirtualDisk.TYPE_BLOCK:
|
|
return "phy"
|
|
elif disk_type == VirtualDisk.TYPE_FILE:
|
|
return "file"
|
|
return "file"
|
|
|
|
@staticmethod
|
|
def pretty_disk_bus(bus):
|
|
if bus in ["ide", "sata", "scsi", "usb", "sd"]:
|
|
return bus.upper()
|
|
if bus in ["xen"]:
|
|
return bus.capitalize()
|
|
if bus == "virtio":
|
|
return "VirtIO"
|
|
if bus == "spapr-vscsi":
|
|
return "vSCSI"
|
|
return bus
|
|
|
|
@staticmethod
|
|
def path_definitely_exists(conn, path):
|
|
"""
|
|
Check if path exists.
|
|
|
|
return True if we are certain, False otherwise. Path may in fact
|
|
exist if we return False, but we can't exhaustively know in all
|
|
cases.
|
|
|
|
(In fact if cached storage volume data is out of date, the volume
|
|
may have disappeared behind out back, but that shouldn't have bad
|
|
effects in practice.)
|
|
"""
|
|
if path is None:
|
|
return False
|
|
|
|
try:
|
|
(vol, pool) = diskbackend.check_if_path_managed(conn, path)
|
|
ignore = pool
|
|
|
|
if vol:
|
|
return True
|
|
|
|
if not conn.is_remote():
|
|
return os.path.exists(path)
|
|
except:
|
|
pass
|
|
|
|
return False
|
|
|
|
@staticmethod
|
|
def check_path_search_for_user(conn, path, username):
|
|
"""
|
|
Check if the passed user has search permissions for all the
|
|
directories in the disk path.
|
|
|
|
@return: List of the directories the user cannot search, or empty list
|
|
@rtype : C{list}
|
|
"""
|
|
if path is None:
|
|
return []
|
|
if conn.is_remote():
|
|
return []
|
|
if username == "root":
|
|
return []
|
|
if diskbackend.path_is_url(path):
|
|
return []
|
|
|
|
try:
|
|
uid = _name_uid(username)
|
|
except Exception, e:
|
|
logging.debug("Error looking up username: %s", str(e))
|
|
return []
|
|
|
|
fixlist = []
|
|
|
|
if os.path.isdir(path):
|
|
dirname = path
|
|
base = "-"
|
|
else:
|
|
dirname, base = os.path.split(path)
|
|
|
|
while base:
|
|
if not _is_dir_searchable(uid, username, dirname):
|
|
fixlist.append(dirname)
|
|
|
|
dirname, base = os.path.split(dirname)
|
|
|
|
return fixlist
|
|
|
|
@staticmethod
|
|
def check_path_search(conn, path):
|
|
# Only works for qemu and DAC
|
|
if conn.is_remote() or not conn.is_qemu_system():
|
|
return None, []
|
|
|
|
from virtcli import cliconfig
|
|
user = cliconfig.default_qemu_user
|
|
try:
|
|
for i in conn.caps.host.secmodels:
|
|
if i.model != "dac":
|
|
continue
|
|
|
|
label = (i.baselabels.get("kvm") or
|
|
i.baselabels.get("qemu"))
|
|
if not label:
|
|
continue
|
|
|
|
pwuid = pwd.getpwuid(
|
|
int(label.split(":")[0].replace("+", "")))
|
|
if pwuid:
|
|
user = pwuid[0]
|
|
except:
|
|
logging.debug("Exception grabbing qemu DAC user", exc_info=True)
|
|
return None, []
|
|
|
|
return user, VirtualDisk.check_path_search_for_user(conn, path, user)
|
|
|
|
@staticmethod
|
|
def fix_path_search_for_user(conn, path, username):
|
|
"""
|
|
Try to fix any permission problems found by check_path_search_for_user
|
|
|
|
@return: Return a dictionary of entries {broken path : error msg}
|
|
@rtype : C{dict}
|
|
"""
|
|
def fix_perms(dirname, useacl=True):
|
|
if useacl:
|
|
cmd = ["setfacl", "--modify", "user:%s:x" % username, dirname]
|
|
proc = subprocess.Popen(cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
out, err = proc.communicate()
|
|
|
|
logging.debug("Ran command '%s'", cmd)
|
|
if out or err:
|
|
logging.debug("out=%s\nerr=%s", out, err)
|
|
|
|
if proc.returncode != 0:
|
|
raise ValueError(err)
|
|
else:
|
|
logging.debug("Setting +x on %s", dirname)
|
|
mode = os.stat(dirname).st_mode
|
|
newmode = mode | stat.S_IXOTH
|
|
os.chmod(dirname, newmode)
|
|
if os.stat(dirname).st_mode != newmode:
|
|
# Trying to change perms on vfat at least doesn't work
|
|
# but also doesn't seem to error. Try and detect that
|
|
raise ValueError(_("Permissions on '%s' did not stick") %
|
|
dirname)
|
|
|
|
fixlist = VirtualDisk.check_path_search_for_user(conn, path, username)
|
|
if not fixlist:
|
|
return []
|
|
|
|
fixlist.reverse()
|
|
errdict = {}
|
|
|
|
useacl = True
|
|
for dirname in fixlist:
|
|
try:
|
|
try:
|
|
fix_perms(dirname, useacl)
|
|
except:
|
|
# If acl fails, fall back to chmod and retry
|
|
if not useacl:
|
|
raise
|
|
useacl = False
|
|
|
|
logging.debug("setfacl failed, trying old fashioned way")
|
|
fix_perms(dirname, useacl)
|
|
except Exception, e:
|
|
errdict[dirname] = str(e)
|
|
|
|
return errdict
|
|
|
|
@staticmethod
|
|
def path_in_use_by(conn, path, shareable=False, read_only=False):
|
|
"""
|
|
Return a list of VM names that are using the passed path.
|
|
|
|
@param conn: virConnect to check VMs
|
|
@param path: Path to check for
|
|
@param shareable: Path we are checking is marked shareable, so
|
|
don't warn if it conflicts with another shareable source.
|
|
@param read_only: Path we are checking is marked read_only, so
|
|
don't warn if it conflicts with another read_only source.
|
|
"""
|
|
if not path:
|
|
return []
|
|
|
|
# Find all volumes that have 'path' somewhere in their backing chain
|
|
vols = []
|
|
volmap = dict((vol.backing_store, vol)
|
|
for vol in conn.fetch_all_vols() if vol.backing_store)
|
|
backpath = path
|
|
while backpath in volmap:
|
|
vol = volmap[backpath]
|
|
if vol in vols:
|
|
break
|
|
backpath = vol.target_path
|
|
vols.append(backpath)
|
|
|
|
ret = []
|
|
vms = conn.fetch_all_guests()
|
|
for vm in vms:
|
|
if not read_only:
|
|
if path in [vm.os.kernel, vm.os.initrd, vm.os.dtb]:
|
|
ret.append(vm.name)
|
|
continue
|
|
|
|
for disk in vm.get_devices("disk"):
|
|
if disk.path in vols and vm.name not in ret:
|
|
# VM uses the path indirectly via backing store
|
|
ret.append(vm.name)
|
|
break
|
|
|
|
if disk.path != path:
|
|
continue
|
|
|
|
if shareable and disk.shareable:
|
|
continue
|
|
if read_only and disk.read_only:
|
|
continue
|
|
|
|
ret.append(vm.name)
|
|
break
|
|
|
|
return ret
|
|
|
|
@staticmethod
|
|
def stat_local_path(path):
|
|
"""
|
|
Return tuple (storage type, storage size) for the passed path on
|
|
the local machine. This is a best effort attempt.
|
|
|
|
@return: tuple of
|
|
(True if regular file, False otherwise, default is True,
|
|
max size of storage, default is 0)
|
|
"""
|
|
try:
|
|
return util.stat_disk(path)
|
|
except:
|
|
return (True, 0)
|
|
|
|
@staticmethod
|
|
def build_vol_install(conn, volname, poolobj, size, sparse,
|
|
fmt=None, backing_store=None):
|
|
"""
|
|
Helper for building a StorageVolume instance to pass to VirtualDisk
|
|
for eventual storage creation.
|
|
|
|
:param volname: name of the volume to be created
|
|
:param size: size in bytes
|
|
"""
|
|
from .storage import StorageVolume
|
|
|
|
if size is None:
|
|
raise ValueError(_("Size must be specified for non "
|
|
"existent volume '%s'" % volname))
|
|
|
|
# This catches --disk /dev/idontexist,size=1 if /dev is unmanaged
|
|
if not poolobj:
|
|
raise RuntimeError(_("Don't know how to create storage for "
|
|
"path '%s'. Use libvirt APIs to manage the parent directory "
|
|
"as a pool first.") % volname)
|
|
|
|
logging.debug("Creating volume '%s' on pool '%s'",
|
|
volname, poolobj.name())
|
|
|
|
cap = (size * 1024 * 1024 * 1024)
|
|
if sparse:
|
|
alloc = 0
|
|
else:
|
|
alloc = cap
|
|
|
|
volinst = StorageVolume(conn)
|
|
volinst.pool = poolobj
|
|
volinst.name = volname
|
|
volinst.capacity = cap
|
|
volinst.allocation = alloc
|
|
volinst.backing_store = backing_store
|
|
|
|
if fmt:
|
|
if not volinst.supports_property("format"):
|
|
raise ValueError(_("Format attribute not supported for this "
|
|
"volume type"))
|
|
volinst.format = fmt
|
|
|
|
return volinst
|
|
|
|
@staticmethod
|
|
def num_to_target(num):
|
|
"""
|
|
Convert an index in range (1, 1024) to a disk /dev number
|
|
(like hda, hdb, hdaa, etc.)
|
|
"""
|
|
digits = []
|
|
for factor in range(0, 3):
|
|
amt = (num % (26 ** (factor + 1))) / (26 ** factor)
|
|
if amt == 0 and num >= (26 ** (factor + 1)):
|
|
amt = 26
|
|
num -= amt
|
|
digits.insert(0, amt)
|
|
|
|
seen_valid = False
|
|
gen_t = ""
|
|
for digit in digits:
|
|
if digit == 0:
|
|
if not seen_valid:
|
|
continue
|
|
digit = 1
|
|
|
|
seen_valid = True
|
|
gen_t += "%c" % (ord('a') + digit - 1)
|
|
|
|
return gen_t
|
|
|
|
|
|
@staticmethod
|
|
def target_to_num(tgt):
|
|
"""
|
|
Convert disk /dev number (like hda, hdb, hdaa, etc.) to an index
|
|
"""
|
|
num = 0
|
|
k = 0
|
|
if tgt[0] == 'x':
|
|
# This case is here for 'xvda'
|
|
tgt = tgt[1:]
|
|
for i, c in enumerate(reversed(tgt[2:])):
|
|
if i != 0:
|
|
k = 1
|
|
num += (ord(c) - ord('a') + k) * (26 ** i)
|
|
return num
|
|
|
|
|
|
_XML_PROP_ORDER = [
|
|
"type", "device",
|
|
"driver_name", "driver_type",
|
|
"driver_cache", "driver_discard", "driver_io", "error_policy",
|
|
"_source_file", "_source_dev", "_source_dir",
|
|
"source_volume", "source_pool", "source_protocol", "source_name",
|
|
"source_host_name", "source_host_port",
|
|
"source_host_transport", "source_host_socket",
|
|
"target", "bus",
|
|
]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
VirtualDevice.__init__(self, *args, **kwargs)
|
|
|
|
self._storage_backend = None
|
|
|
|
|
|
#############################
|
|
# Public property-esque API #
|
|
#############################
|
|
|
|
def _get_path(self):
|
|
if not self._storage_backend:
|
|
xmlpath = self._get_xmlpath()
|
|
if xmlpath:
|
|
return xmlpath
|
|
|
|
self._set_default_storage_backend()
|
|
return self._storage_backend.get_path()
|
|
def _set_path(self, newpath):
|
|
if (self._storage_backend and
|
|
self._storage_backend.will_create_storage()):
|
|
raise ValueError("Can't change disk path if storage creation info "
|
|
"has been set.")
|
|
|
|
# User explicitly changed 'path', so try to lookup its storage
|
|
# object since we may need it
|
|
(vol_object, parent_pool) = diskbackend.manage_path(self.conn, newpath)
|
|
|
|
self._change_backend(newpath, vol_object, parent_pool)
|
|
self._set_xmlpath(self.path)
|
|
path = property(_get_path, _set_path)
|
|
|
|
def set_vol_object(self, vol_object, parent_pool):
|
|
self._change_backend(None, vol_object, parent_pool)
|
|
self._set_xmlpath(self.path)
|
|
|
|
def set_vol_install(self, vol_install):
|
|
self._storage_backend = diskbackend.ManagedStorageCreator(
|
|
self.conn, vol_install)
|
|
self._set_xmlpath(self.path)
|
|
|
|
def get_vol_object(self):
|
|
return self._storage_backend.get_vol_object()
|
|
def get_vol_install(self):
|
|
return self._storage_backend.get_vol_install()
|
|
def get_parent_pool(self):
|
|
if self.get_vol_install():
|
|
return self.get_vol_install().pool
|
|
return self._storage_backend.get_parent_pool()
|
|
|
|
def get_size(self):
|
|
return self._storage_backend.get_size()
|
|
|
|
|
|
#############################
|
|
# Internal defaults helpers #
|
|
#############################
|
|
|
|
def _get_default_driver_name(self):
|
|
if not self.path:
|
|
return None
|
|
if self.conn.is_qemu():
|
|
return self.DRIVER_QEMU
|
|
return None
|
|
|
|
def _get_default_driver_type(self):
|
|
"""
|
|
Set driver type from passed parameters
|
|
|
|
Where possible, we want to force /driver/@type = "raw" if installing
|
|
a QEMU VM. Without telling QEMU to expect a raw file, the emulator
|
|
is forced to autodetect, which has security implications:
|
|
|
|
http://lists.gnu.org/archive/html/qemu-devel/2008-04/msg00675.html
|
|
"""
|
|
if self.driver_name != self.DRIVER_QEMU:
|
|
return None
|
|
|
|
drvtype = self._storage_backend.get_driver_type()
|
|
return _qemu_sanitize_drvtype(self.type, drvtype)
|
|
|
|
|
|
#############################
|
|
# XML source media handling #
|
|
#############################
|
|
|
|
_source_file = XMLProperty("./source/@file")
|
|
_source_dev = XMLProperty("./source/@dev")
|
|
_source_dir = XMLProperty("./source/@dir")
|
|
|
|
source_pool = XMLProperty("./source/@pool")
|
|
source_volume = XMLProperty("./source/@volume")
|
|
|
|
source_name = XMLProperty("./source/@name")
|
|
source_protocol = XMLProperty("./source/@protocol")
|
|
# Technically multiple host lines can be listed
|
|
source_host_name = XMLProperty("./source/host/@name")
|
|
source_host_port = XMLProperty("./source/host/@port", is_int=True)
|
|
source_host_transport = XMLProperty("./source/host/@transport")
|
|
source_host_socket = XMLProperty("./source/host/@socket")
|
|
|
|
def _set_source_network_from_url(self, uri):
|
|
uriinfo = URISplit(uri)
|
|
if uriinfo.scheme:
|
|
self.source_protocol = uriinfo.scheme
|
|
if uriinfo.transport:
|
|
self.source_host_transport = uriinfo.transport
|
|
if uriinfo.hostname:
|
|
self.source_host_name = uriinfo.hostname
|
|
if uriinfo.port:
|
|
self.source_host_port = uriinfo.port
|
|
if uriinfo.path:
|
|
if self.source_host_transport:
|
|
self.source_host_socket = uriinfo.path
|
|
else:
|
|
self.source_name = uriinfo.path
|
|
if self.source_name.startswith("/"):
|
|
self.source_name = self.source_name[1:]
|
|
|
|
def _set_source_network_from_storage(self, volxml, poolxml):
|
|
self.source_protocol = poolxml.type
|
|
if poolxml.hosts:
|
|
self.source_host_name = poolxml.hosts[0].name
|
|
self.source_host_port = poolxml.hosts[0].port
|
|
|
|
path = ""
|
|
if poolxml.source_name:
|
|
path += poolxml.source_name + "/"
|
|
path += volxml.name
|
|
self.source_name = path
|
|
self.type = "network"
|
|
|
|
def _set_network_source_from_backend(self):
|
|
if (self._storage_backend.get_vol_object() or
|
|
self._storage_backend.get_vol_install()):
|
|
volxml = self._storage_backend.get_vol_xml()
|
|
poolxml = self._storage_backend.get_parent_pool_xml()
|
|
self._set_source_network_from_storage(volxml, poolxml)
|
|
elif self._storage_backend.get_path():
|
|
self._set_source_network_from_url(self._storage_backend.get_path())
|
|
|
|
def _build_url_from_network_source(self):
|
|
ret = self.source_protocol
|
|
if self.source_host_transport:
|
|
ret += "+%s" % self.source_host_transport
|
|
ret += "://"
|
|
if self.source_host_name:
|
|
ret += self.source_host_name
|
|
if self.source_host_port:
|
|
ret += ":" + str(self.source_host_port)
|
|
if self.source_name:
|
|
if not self.source_name.startswith("/"):
|
|
ret += "/"
|
|
ret += self.source_name
|
|
elif self.source_host_socket:
|
|
if not self.source_host_socket.startswith("/"):
|
|
ret += "/"
|
|
ret += self.source_host_socket
|
|
return ret
|
|
|
|
def _get_default_type(self):
|
|
if self.source_pool or self.source_volume:
|
|
return VirtualDisk.TYPE_VOLUME
|
|
if self._storage_backend:
|
|
return self._storage_backend.get_dev_type()
|
|
if self.source_protocol:
|
|
return VirtualDisk.TYPE_NETWORK
|
|
return self.TYPE_FILE
|
|
type = XMLProperty("./@type", default_cb=_get_default_type)
|
|
|
|
def _clear_source_xml(self):
|
|
"""
|
|
Unset all XML properties that describe the actual source media
|
|
"""
|
|
self._source_file = None
|
|
self._source_dev = None
|
|
self._source_dir = None
|
|
self.source_volume = None
|
|
self.source_pool = None
|
|
self.source_name = None
|
|
self.source_protocol = None
|
|
self.source_host_name = None
|
|
self.source_host_port = None
|
|
self.source_host_transport = None
|
|
self.source_host_socket = None
|
|
|
|
def _disk_type_to_object_prop_name(self):
|
|
disk_type = self.type
|
|
if disk_type == VirtualDisk.TYPE_BLOCK:
|
|
return "_source_dev"
|
|
elif disk_type == VirtualDisk.TYPE_DIR:
|
|
return "_source_dir"
|
|
elif disk_type == VirtualDisk.TYPE_FILE:
|
|
return "_source_file"
|
|
return None
|
|
|
|
|
|
# _xmlpath is an abstraction for source file/block/dir paths, since
|
|
# they don't have any special properties aside from needing to match
|
|
# 'type' value with the source property used.
|
|
def _get_xmlpath(self):
|
|
if self._source_file:
|
|
return self._source_file
|
|
if self._source_dev:
|
|
return self._source_dev
|
|
if self._source_dir:
|
|
return self._source_dir
|
|
return None
|
|
def _set_xmlpath(self, val):
|
|
self._clear_source_xml()
|
|
|
|
if self._storage_backend.get_dev_type() == "network":
|
|
self._set_network_source_from_backend()
|
|
return
|
|
|
|
propname = self._disk_type_to_object_prop_name()
|
|
if not propname:
|
|
return
|
|
return setattr(self, propname, val)
|
|
|
|
|
|
##################
|
|
# XML properties #
|
|
##################
|
|
|
|
device = XMLProperty("./@device",
|
|
default_cb=lambda s: s.DEVICE_DISK)
|
|
driver_name = XMLProperty("./driver/@name",
|
|
default_cb=_get_default_driver_name)
|
|
driver_type = XMLProperty("./driver/@type",
|
|
default_cb=_get_default_driver_type)
|
|
|
|
|
|
bus = XMLProperty("./target/@bus")
|
|
target = XMLProperty("./target/@dev")
|
|
removable = XMLProperty("./target/@removable", is_onoff=True)
|
|
|
|
read_only = XMLProperty("./readonly", is_bool=True)
|
|
shareable = XMLProperty("./shareable", is_bool=True)
|
|
driver_cache = XMLProperty("./driver/@cache")
|
|
driver_discard = XMLProperty("./driver/@discard")
|
|
driver_io = XMLProperty("./driver/@io")
|
|
|
|
error_policy = XMLProperty("./driver/@error_policy")
|
|
serial = XMLProperty("./serial")
|
|
startup_policy = XMLProperty("./source/@startupPolicy")
|
|
|
|
iotune_rbs = XMLProperty("./iotune/read_bytes_sec", is_int=True)
|
|
iotune_ris = XMLProperty("./iotune/read_iops_sec", is_int=True)
|
|
iotune_tbs = XMLProperty("./iotune/total_bytes_sec", is_int=True)
|
|
iotune_tis = XMLProperty("./iotune/total_iops_sec", is_int=True)
|
|
iotune_wbs = XMLProperty("./iotune/write_bytes_sec", is_int=True)
|
|
iotune_wis = XMLProperty("./iotune/write_iops_sec", is_int=True)
|
|
|
|
|
|
#################################
|
|
# Validation assistance methods #
|
|
#################################
|
|
|
|
def _set_default_storage_backend(self):
|
|
path = None
|
|
vol_object = None
|
|
parent_pool = None
|
|
typ = self._get_default_type()
|
|
|
|
if self.type == VirtualDisk.TYPE_NETWORK:
|
|
# Fill in a completed URL for virt-manager UI, path comparison, etc
|
|
path = self._build_url_from_network_source()
|
|
|
|
if typ == VirtualDisk.TYPE_VOLUME:
|
|
conn = self.conn
|
|
if "weakref" in str(type(conn)):
|
|
conn = conn()
|
|
|
|
try:
|
|
parent_pool = conn.storagePoolLookupByName(self.source_pool)
|
|
vol_object = parent_pool.storageVolLookupByName(
|
|
self.source_volume)
|
|
except:
|
|
logging.debug("Error fetching source pool=%s vol=%s",
|
|
self.source_pool, self.source_volume, exc_info=True)
|
|
|
|
if vol_object is None and path is None:
|
|
path = self._get_xmlpath()
|
|
|
|
self._change_backend(path, vol_object, parent_pool)
|
|
|
|
def set_local_disk_to_clone(self, disk, sparse):
|
|
"""
|
|
Set a path to manually clone (as in, not through libvirt)
|
|
"""
|
|
self._storage_backend = diskbackend.CloneStorageCreator(self.conn,
|
|
self.path, disk.path, disk.get_size(), sparse)
|
|
|
|
def is_cdrom(self):
|
|
return self.device == self.DEVICE_CDROM
|
|
def is_floppy(self):
|
|
return self.device == self.DEVICE_FLOPPY
|
|
def is_disk(self):
|
|
return self.device == self.DEVICE_DISK
|
|
|
|
def can_be_empty(self):
|
|
return self.is_floppy() or self.is_cdrom()
|
|
|
|
def _change_backend(self, path, vol_object, parent_pool):
|
|
backend = diskbackend.StorageBackend(self.conn, path,
|
|
vol_object, parent_pool)
|
|
self._storage_backend = backend
|
|
|
|
def sync_path_props(self):
|
|
"""
|
|
Fills in the values of type, driver_type, and driver_name for
|
|
the associated backing storage. This needs to be manually called
|
|
if changing an existing disk's media.
|
|
"""
|
|
path = self._get_xmlpath()
|
|
|
|
self.type = self._get_default_type()
|
|
self.driver_name = self._get_default_driver_name()
|
|
self.driver_type = self._get_default_driver_type()
|
|
|
|
# Need to retrigger this if self.type changed
|
|
if path:
|
|
self._set_xmlpath(path)
|
|
|
|
def wants_storage_creation(self):
|
|
"""
|
|
If true, this disk needs storage creation parameters or things
|
|
will error.
|
|
"""
|
|
return (self._storage_backend and
|
|
not self._storage_backend.exists())
|
|
|
|
def validate(self):
|
|
if self.path is None:
|
|
if not self.can_be_empty():
|
|
raise ValueError(_("Device type '%s' requires a path") %
|
|
self.device)
|
|
|
|
return
|
|
|
|
if (self.type == VirtualDisk.TYPE_DIR and
|
|
not self.is_floppy()):
|
|
raise ValueError(_("The path '%s' must be a file or a "
|
|
"device, not a directory") % self.path)
|
|
|
|
if not self._storage_backend:
|
|
return
|
|
|
|
if (not self._storage_backend.will_create_storage() and
|
|
not self._storage_backend.exists()):
|
|
raise ValueError(
|
|
_("Must specify storage creation parameters for "
|
|
"non-existent path '%s'.") % self.path)
|
|
|
|
self._storage_backend.validate(self)
|
|
|
|
def setup(self, meter=None):
|
|
"""
|
|
Build storage (if required)
|
|
|
|
If storage doesn't exist (a non-existent file 'path', or 'vol_install'
|
|
was specified), we create it.
|
|
|
|
@param meter: Progress meter to report file creation on
|
|
@type meter: instanceof urlgrabber.BaseMeter
|
|
"""
|
|
if not meter:
|
|
meter = progress.BaseMeter()
|
|
if not self._storage_backend.will_create_storage():
|
|
return
|
|
|
|
vol_object = self._storage_backend.create(meter)
|
|
if not vol_object:
|
|
return
|
|
|
|
parent_pool = self.get_vol_install().pool
|
|
self._change_backend(None, vol_object, parent_pool)
|
|
|
|
def set_defaults(self, guest):
|
|
if self.is_cdrom():
|
|
self.read_only = True
|
|
|
|
if (guest.os.is_xenpv() and
|
|
self.type == VirtualDisk.TYPE_FILE and
|
|
self.driver_name is None and
|
|
util.is_blktap_capable(self.conn)):
|
|
self.driver_name = VirtualDisk.DRIVER_TAP
|
|
|
|
if not self.conn.is_qemu():
|
|
return
|
|
if not self.is_disk():
|
|
return
|
|
if not self.type == self.TYPE_BLOCK:
|
|
return
|
|
|
|
# Enable cache=none and io=native for block devices. Would
|
|
# be nice if qemu did this for us but that time has long passed.
|
|
if not self.driver_cache:
|
|
self.driver_cache = self.CACHE_MODE_NONE
|
|
if not self.driver_io:
|
|
self.driver_io = self.IO_MODE_NATIVE
|
|
|
|
|
|
def is_size_conflict(self):
|
|
"""
|
|
reports if disk size conflicts with available space
|
|
|
|
returns a two element tuple:
|
|
1. first element is True if fatal conflict occurs
|
|
2. 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")
|
|
"""
|
|
return self._storage_backend.is_size_conflict()
|
|
|
|
def is_conflict_disk(self, conn=None):
|
|
"""
|
|
check if specified storage is in use by any other VMs on passed
|
|
connection.
|
|
|
|
@return: list of colliding VM names
|
|
@rtype: C{list}
|
|
"""
|
|
if not self.path:
|
|
return False
|
|
if not conn:
|
|
conn = self.conn
|
|
|
|
ret = self.path_in_use_by(conn, self.path,
|
|
shareable=self.shareable,
|
|
read_only=self.read_only)
|
|
return ret
|
|
|
|
|
|
def get_target_prefix(self, used_targets=None):
|
|
"""
|
|
Returns the suggested disk target prefix (hd, xvd, sd ...) for the
|
|
disk.
|
|
@returns: str prefix, or None if no reasonable guess can be made
|
|
"""
|
|
# The upper limits here aren't necessarilly 1024, but let the HV
|
|
# error as appropriate.
|
|
def _return(prefix):
|
|
nummap = {
|
|
"vd": 1024,
|
|
"xvd": 1024,
|
|
"fd": 2,
|
|
"hd": 4,
|
|
"sd": 1024,
|
|
}
|
|
return prefix, nummap[prefix]
|
|
|
|
if self.bus == "virtio":
|
|
return _return("vd")
|
|
elif self.bus == "xen":
|
|
return _return("xvd")
|
|
elif self.bus == "fdc" or self.is_floppy():
|
|
return _return("fd")
|
|
elif self.bus == "ide":
|
|
return _return("hd")
|
|
elif self.bus or not used_targets:
|
|
# sata, scsi, usb, sd
|
|
return _return("sd")
|
|
|
|
# If guest already has some disks defined
|
|
preforder = ["vd", "xvd", "sd", "hd"]
|
|
for pref in preforder:
|
|
for target in used_targets:
|
|
if target.startswith(pref):
|
|
return _return(pref)
|
|
return _return("sd")
|
|
|
|
def generate_target(self, skip_targets, pref_ctrl=None):
|
|
"""
|
|
Generate target device ('hda', 'sdb', etc..) for disk, excluding
|
|
any targets in 'skip_targets'. If given the 'pref_ctrl'
|
|
parameter, it tries to select the target so that the disk is
|
|
mapped onto that controller.
|
|
Sets self.target, and returns the generated value.
|
|
|
|
@param skip_targets: list of targets to exclude
|
|
@type skip_targets: C{list}
|
|
@param pref_ctrl: preferred controller to connect the disk to
|
|
@type pref_ctrl: C{int}
|
|
@raise ValueError: can't determine target type, no targets available
|
|
@returns generated target
|
|
@rtype C{str}
|
|
"""
|
|
prefix, maxnode = self.get_target_prefix(skip_targets)
|
|
skip_targets = [t for t in skip_targets if t and t.startswith(prefix)]
|
|
skip_targets.sort()
|
|
|
|
def get_target():
|
|
first_found = None
|
|
|
|
ran = range(maxnode)
|
|
if pref_ctrl is not None:
|
|
# We assume narrow SCSI bus and libvirt assigning 7
|
|
# (1-7, 8-14, etc.) devices per controller
|
|
ran = range(pref_ctrl * 7, (pref_ctrl + 1) * 7)
|
|
|
|
for i in ran:
|
|
gen_t = prefix + self.num_to_target(i + 1)
|
|
if gen_t in skip_targets:
|
|
skip_targets.remove(gen_t)
|
|
continue
|
|
if not skip_targets:
|
|
return gen_t
|
|
elif not first_found:
|
|
first_found = gen_t
|
|
if first_found:
|
|
return first_found
|
|
|
|
ret = get_target()
|
|
if ret:
|
|
self.target = ret
|
|
return ret
|
|
|
|
if pref_ctrl is not None:
|
|
# This basically means that we either chose full
|
|
# controller or didn't add any
|
|
raise ValueError(_("Controller number %d for disk of type %s has "
|
|
"no empty slot to use" % (pref_ctrl, prefix)))
|
|
else:
|
|
raise ValueError(_("Only %s disks of type '%s' are supported"
|
|
% (maxnode, prefix)))
|
|
|
|
VirtualDisk.register_type()
|