core: update qvm-block code for HAL API

Use QubesDB to get list of devices, call libvirt methods to
attach/detach devices.
This commit is contained in:
Marek Marczykowski-Górecki 2014-12-12 03:58:33 +01:00
parent b4e0833cb7
commit d4ab70ae9d
2 changed files with 179 additions and 193 deletions

View File

@ -21,6 +21,9 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
#
import string
from lxml import etree
from lxml.etree import ElementTree, SubElement, Element
from qubes import QubesException
from qubes import vmm
@ -37,6 +40,11 @@ import xen.lowlevel.xs
BLKSIZE = 512
# all frontends, prefer xvdi
# TODO: get this from libvirt driver?
AVAILABLE_FRONTENDS = ['xvd'+c for c in
string.lowercase[8:]+string.lowercase[:8]]
def mbytes_to_kmg(size):
if size > 1024:
return "%d GiB" % (size/1024)
@ -207,127 +215,144 @@ def block_find_unused_frontend(vm = None):
assert vm is not None
assert vm.is_running()
vbd_list = vmm.xs.ls('', '/local/domain/%d/device/vbd' % vm.xid)
# xvd* devices
major = 202
# prefer xvdi
for minor in range(8*16,254,16)+range(0,8*16,16):
if vbd_list is None or str(major << 8 | minor) not in vbd_list:
return block_devid_to_name(major << 8 | minor)
xml = vm.libvirt_domain.XMLDesc()
parsed_xml = etree.fromstring(xml)
used = [target.get('dev', None) for target in
parsed_xml.xpath("//domain/devices/disk/target")]
for dev in AVAILABLE_FRONTENDS:
if dev not in used:
return dev
return None
def block_list(vm = None, system_disks = False):
device_re = re.compile(r"^[a-z0-9-]{1,12}$")
def block_list_vm(vm, system_disks = False):
name_re = re.compile(r"^[a-z0-9-]{1,12}$")
device_re = re.compile(r"^[a-z0-9/-]{1,64}$")
# FIXME: any better idea of desc_re?
desc_re = re.compile(r"^.{1,255}$")
mode_re = re.compile(r"^[rw]$")
xs_trans = vmm.xs.transaction_start()
assert vm is not None
vm_list = []
if vm is not None:
if not vm.is_running():
vmm.xs.transaction_end(xs_trans)
return []
else:
vm_list = [ str(vm.xid) ]
else:
vm_list = vmm.xs.ls(xs_trans, '/local/domain')
if not vm.is_running():
return []
devices_list = {}
for xid in vm_list:
vm_name = vmm.xs.read(xs_trans, '/local/domain/%s/name' % xid)
vm_devices = vmm.xs.ls(xs_trans, '/local/domain/%s/qubes-block-devices' % xid)
if vm_devices is None:
continue
for device in vm_devices:
# Sanitize device name
if not device_re.match(device):
print >> sys.stderr, "Invalid device name in VM '%s'" % vm_name
continue
device_size = vmm.xs.read(xs_trans, '/local/domain/%s/qubes-block-devices/%s/size' % (xid, device))
device_desc = vmm.xs.read(xs_trans, '/local/domain/%s/qubes-block-devices/%s/desc' % (xid, device))
device_mode = vmm.xs.read(xs_trans, '/local/domain/%s/qubes-block-devices/%s/mode' % (xid, device))
untrusted_devices = vm.qdb.multiread('/qubes-block-devices/')
if device_size is None or device_desc is None or device_mode is None:
print >> sys.stderr, "Missing field in %s device parameters" % device
def get_dev_item(dev, item):
return untrusted_devices.get(
'/qubes-block-devices/%s/%s' % (dev, item),
None)
untrusted_devices_names = list(set(map(lambda x: x.split("/")[2],
untrusted_devices.keys())))
for untrusted_dev_name in untrusted_devices_names:
if name_re.match(untrusted_dev_name):
dev_name = untrusted_dev_name
untrusted_device_size = get_dev_item(dev_name, 'size')
untrusted_device_desc = get_dev_item(dev_name, 'desc')
untrusted_device_mode = get_dev_item(dev_name, 'mode')
untrusted_device_device = get_dev_item(dev_name, 'device')
if untrusted_device_desc is None or untrusted_device_mode is None\
or untrusted_device_size is None:
print >>sys.stderr, "Missing field in %s device parameters" %\
dev_name
continue
if not device_size.isdigit():
print >> sys.stderr, "Invalid %s device size in VM '%s'" % (device, vm_name)
if untrusted_device_device is None:
untrusted_device_device = '/dev/' + dev_name
if not device_re.match(untrusted_device_device):
print >> sys.stderr, "Invalid %s device path in VM '%s'" % (
dev_name, vm.name)
continue
if not desc_re.match(device_desc):
print >> sys.stderr, "Invalid %s device desc in VM '%s'" % (device, vm_name)
device_device = untrusted_device_device
if not untrusted_device_size.isdigit():
print >> sys.stderr, "Invalid %s device size in VM '%s'" % (
dev_name, vm.name)
continue
if not mode_re.match(device_mode):
print >> sys.stderr, "Invalid %s device mode in VM '%s'" % (device, vm_name)
device_size = int(untrusted_device_size)
if not desc_re.match(untrusted_device_desc):
print >> sys.stderr, "Invalid %s device desc in VM '%s'" % (
dev_name, vm.name)
continue
# Check if we know major number for this device; attach will work without this, but detach and check_attached don't
if block_name_to_majorminor(device) == (0, 0):
print >> sys.stderr, "Unsupported device %s:%s" % (vm_name, device)
device_desc = untrusted_device_desc
if not mode_re.match(untrusted_device_mode):
print >> sys.stderr, "Invalid %s device mode in VM '%s'" % (
dev_name, vm.name)
continue
device_mode = untrusted_device_mode
if not system_disks:
if xid == '0' and device_desc.startswith(system_path["qubes_base_dir"]):
if vm.qid == 0 and device_desc.startswith(system_path[
"qubes_base_dir"]):
continue
visible_name = "%s:%s" % (vm_name, device)
devices_list[visible_name] = {"name": visible_name, "xid":int(xid),
"vm": vm_name, "device":device, "size":int(device_size),
"desc":device_desc, "mode":device_mode}
visible_name = "%s:%s" % (vm.name, dev_name)
devices_list[visible_name] = {
"name": visible_name,
"vm": vm.name,
"device": device_device,
"size": device_size,
"desc": device_desc,
"mode": device_mode
}
vmm.xs.transaction_end(xs_trans)
return devices_list
def block_check_attached(backend_vm, device, backend_xid = None):
if backend_xid is None:
backend_xid = backend_vm.xid
xs_trans = vmm.xs.transaction_start()
vm_list = vmm.xs.ls(xs_trans, '/local/domain/%d/backend/vbd' % backend_xid)
if vm_list is None:
vmm.xs.transaction_end(xs_trans)
return None
device_majorminor = None
try:
device_majorminor = block_name_to_majorminor(device)
except:
# Unknown devices will be compared directly - perhaps it is a filename?
pass
for vm_xid in vm_list:
for devid in vmm.xs.ls(xs_trans, '/local/domain/%d/backend/vbd/%s' % (backend_xid, vm_xid)):
(tmp_major, tmp_minor) = (0, 0)
phys_device = vmm.xs.read(xs_trans, '/local/domain/%d/backend/vbd/%s/%s/physical-device' % (backend_xid, vm_xid, devid))
dev_params = vmm.xs.read(xs_trans, '/local/domain/%d/backend/vbd/%s/%s/params' % (backend_xid, vm_xid, devid))
if phys_device and phys_device.find(':'):
(tmp_major, tmp_minor) = phys_device.split(":")
tmp_major = int(tmp_major, 16)
tmp_minor = int(tmp_minor, 16)
else:
# perhaps not ready yet - check params
if not dev_params:
# Skip not-phy devices
continue
elif not dev_params.startswith('/dev/'):
# will compare params directly
pass
else:
(tmp_major, tmp_minor) = block_name_to_majorminor(dev_params.lstrip('/dev/'))
def block_list(qvmc = None, vm = None, system_disks = False):
if vm is not None:
if not vm.is_running():
return []
else:
vm_list = [ vm ]
else:
if qvmc is None:
raise QubesException("You must pass either qvm or vm argument")
vm_list = qvmc.values()
if (device_majorminor and (tmp_major, tmp_minor) == device_majorminor) or \
(device_majorminor is None and dev_params == device):
#TODO
vm_name = xl_ctx.domid_to_name(int(vm_xid))
frontend = block_devid_to_name(int(devid))
vmm.xs.transaction_end(xs_trans)
return {"xid":int(vm_xid), "frontend": frontend, "devid": int(devid), "vm": vm_name}
vmm.xs.transaction_end(xs_trans)
devices_list = {}
for vm in vm_list:
devices_list.update(block_list_vm(vm, system_disks))
return devices_list
def block_check_attached(qvmc, device):
"""
@type qvmc: QubesVmCollection
"""
if qvmc is None:
# TODO: ValueError
raise QubesException("You need to pass qvmc argument")
for vm in qvmc.values():
libvirt_domain = vm.libvirt_domain
if libvirt_domain:
xml = vm.libvirt_domain.XMLDesc()
parsed_xml = etree.fromstring(xml)
disks = parsed_xml.xpath("//domain/devices/disk")
for disk in disks:
backend_name = 'dom0'
# FIXME: move <domain/> into <source/>
if disk.find('domain') is not None:
backend_name = disk.find('domain').get('name')
source = disk.find('source')
if disk.get('type') == 'file':
path = source.get('file')
elif disk.get('type') == 'block':
path = source.get('dev')
else:
# TODO: logger
print >>sys.stderr, "Unknown disk type '%s' attached to " \
"VM '%s'" % (source.get('type'),
vm.name)
continue
if backend_name == device['vm'] and path == device['device']:
return {
"frontend": disk.find('target').get('dev'),
"vm": vm}
return None
def block_attach(vm, backend_vm, device, frontend=None, mode="w", auto_detach=False, wait=True):
device_attach_check(vm, backend_vm, device, frontend)
do_block_attach(vm, backend_vm, device, frontend, mode, auto_detach, wait)
def device_attach_check(vm, backend_vm, device, frontend):
def device_attach_check(vm, backend_vm, device, frontend, mode):
""" Checks all the parameters, dies on errors """
if not vm.is_running():
raise QubesException("VM %s not running" % vm.name)
@ -335,105 +360,67 @@ def device_attach_check(vm, backend_vm, device, frontend):
if not backend_vm.is_running():
raise QubesException("VM %s not running" % backend_vm.name)
def do_block_attach(vm, backend_vm, device, frontend, mode, auto_detach, wait):
if device['mode'] == 'r' and mode == 'w':
raise QubesException("Cannot attach read-only device in read-write "
"mode")
def block_attach(qvmc, vm, device, frontend=None, mode="w", auto_detach=False, wait=True):
backend_vm = qvmc.get_vm_by_name(device['vm'])
device_attach_check(vm, backend_vm, device, frontend, mode)
if frontend is None:
frontend = block_find_unused_frontend(vm)
if frontend is None:
raise QubesException("No unused frontend found")
else:
# Check if any device attached at this frontend
if vmm.xs.read('', '/local/domain/%d/device/vbd/%d/state' % (vm.xid, block_name_to_devid(frontend))) == '4':
xml = vm.libvirt_domain.XMLDesc()
parsed_xml = etree.fromstring(xml)
disks = parsed_xml.xpath("//domain/devices/disk/target[@dev='%s']" %
frontend)
if len(disks):
raise QubesException("Frontend %s busy in VM %s, detach it first" % (frontend, vm.name))
# Check if this device is attached to some domain
attached_vm = block_check_attached(backend_vm, device)
attached_vm = block_check_attached(qvmc, device)
if attached_vm:
if auto_detach:
block_detach(None, attached_vm['devid'], vm_xid=attached_vm['xid'])
block_detach(attached_vm['vm'], attached_vm['frontend'])
else:
raise QubesException("Device %s from %s already connected to VM %s as %s" % (device, backend_vm.name, attached_vm['vm'], attached_vm['frontend']))
raise QubesException("Device %s from %s already connected to VM "
"%s as %s" % (device['device'],
backend_vm.name, attached_vm['vm'], attached_vm['frontend']))
if device.startswith('/'):
backend_dev = 'script:file:' + device
else:
backend_dev = 'phy:/dev/' + device
disk = Element("disk")
disk.set('type', 'block')
disk.set('device', 'disk')
SubElement(disk, 'driver').set('name', 'phy')
SubElement(disk, 'source').set('dev', device['device'])
SubElement(disk, 'target').set('dev', frontend)
if backend_vm.qid != 0:
SubElement(disk, 'domain').set('name', device['vm'])
vm.libvirt_domain.attachDevice(etree.tostring(disk, encoding='utf-8'))
xl_cmd = [ '/usr/sbin/xl', 'block-attach', vm.name, backend_dev, frontend, mode, str(backend_vm.xid) ]
subprocess.check_call(xl_cmd)
if wait:
be_path = '/local/domain/%d/backend/vbd/%d/%d' % (backend_vm.xid, vm.xid, block_name_to_devid(frontend))
# There is no way to use xenstore watch with a timeout, so must check in a loop
interval = 0.100
# 5sec timeout
timeout = 5/interval
while timeout > 0:
be_state = vmm.xs.read('', be_path + '/state')
hotplug_state = vmm.xs.read('', be_path + '/hotplug-status')
if be_state is None:
raise QubesException("Backend device disappeared, something weird happened")
elif int(be_state) == 4:
# Ok
return
elif int(be_state) > 4:
# Error
error = vmm.xs.read('', '/local/domain/%d/error/backend/vbd/%d/%d/error' % (backend_vm.xid, vm.xid, block_name_to_devid(frontend)))
if error is not None:
raise QubesException("Error while connecting block device: " + error)
else:
raise QubesException("Unknown error while connecting block device")
elif hotplug_state == 'error':
hotplug_error = vmm.xs.read('', be_path + '/hotplug-error')
if hotplug_error:
raise QubesException("Error while connecting block device: " + hotplug_error)
else:
raise QubesException("Unknown hotplug error while connecting block device")
time.sleep(interval)
timeout -= interval
raise QubesException("Timeout while waiting for block defice connection")
def block_detach(vm, frontend = "xvdi"):
def block_detach(vm, frontend = "xvdi", vm_xid = None):
# Get XID if not provided already
if vm_xid is None:
if not vm.is_running():
raise QubesException("VM %s not running" % vm.name)
# FIXME: potential race
vm_xid = vm.xid
# Check if this device is really connected
if not vmm.xs.read('', '/local/domain/%d/device/vbd/%d/state' % (vm_xid, block_name_to_devid(frontend))) == '4':
# Do nothing - device already detached
return
xl_cmd = [ '/usr/sbin/xl', 'block-detach', str(vm_xid), str(frontend)]
subprocess.check_call(xl_cmd)
def block_detach_all(vm, vm_xid = None):
""" Detach all non-system devices"""
# Get XID if not provided already
if vm_xid is None:
if not vm.is_running():
raise QubesException("VM %s not running" % vm.name)
# FIXME: potential race
vm_xid = vm.xid
xs_trans = vmm.xs.transaction_start()
devices = vmm.xs.ls(xs_trans, '/local/domain/%d/device/vbd' % vm_xid)
if devices is None:
return
devices_to_detach = []
for devid in devices:
# check if this is system disk
be_path = vmm.xs.read(xs_trans, '/local/domain/%d/device/vbd/%s/backend' % (vm_xid, devid))
assert be_path is not None
be_params = vmm.xs.read(xs_trans, be_path + '/params')
if be_path.startswith('/local/domain/0/') and be_params is not None and be_params.startswith(system_path["qubes_base_dir"]):
# system disk
xml = vm.libvirt_domain.XMLDesc()
parsed_xml = etree.fromstring(xml)
attached = parsed_xml.xpath("//domain/devices/disk")
for disk in attached:
if frontend is not None and disk.find('target').get('dev') != frontend:
# Not the device we are looking for
continue
devices_to_detach.append(devid)
vmm.xs.transaction_end(xs_trans)
for devid in devices_to_detach:
xl_cmd = [ '/usr/sbin/xl', 'block-detach', str(vm_xid), devid]
subprocess.check_call(xl_cmd)
if frontend is None:
# ignore system disks
if disk.find('domain') == None and \
disk.find('source').get('dev').startswith(system_path[
"qubes_base_dir"]):
continue
vm.libvirt_domain.detachDevice(etree.tostring(disk, encoding='utf-8'))
def block_detach_all(vm):
""" Detach all non-system devices"""
block_detach(vm, None)
####### USB devices ######

View File

@ -70,14 +70,13 @@ def main():
print >> sys.stderr, "Only one of -l -a/-A -d is allowed!"
exit (1)
if options.do_attach or options.do_detach:
qvm_collection = QubesVmCollection()
qvm_collection.lock_db_for_reading()
qvm_collection.load()
qvm_collection.unlock_db()
qvm_collection = QubesVmCollection()
qvm_collection.lock_db_for_reading()
qvm_collection.load()
qvm_collection.unlock_db()
if options.do_attach:
if (len (args) != 2):
if len(args) != 2:
parser.error ("You must provide vm name and device!")
vm = qvm_collection.get_vm_by_name(args[0])
if vm is None:
@ -90,12 +89,10 @@ def main():
(dev['vm'], dev['device']) = args[1].split(":")
dev['mode'] = 'w'
else:
dev_list = block_list()
dev_list = block_list(qvm_collection)
if not args[1] in dev_list.keys():
parser.error ("Invalid device name: %s" % args[1])
dev = dev_list[args[1]]
backend_vm = qvm_collection.get_vm_by_name(dev['vm'])
assert backend_vm is not None
kwargs = {}
if options.frontend:
kwargs['frontend'] = options.frontend
@ -105,7 +102,7 @@ def main():
kwargs['mode'] = dev['mode']
kwargs['auto_detach'] = options.auto_detach
try:
block_attach(vm, backend_vm, dev['device'], **kwargs)
block_attach(qvm_collection, vm, dev, **kwargs)
except QubesException as e:
print >> sys.stderr, "ERROR: %s" % str(e)
sys.exit(1)
@ -125,26 +122,28 @@ def main():
block_detach_all(vm)
else:
# Maybe device?
dev_list = block_list()
dev_list = block_list(qvm_collection)
if not args[0] in dev_list.keys():
parser.error ("Invalid VM or device name: %s" % args[0])
dev = dev_list[args[0]]
attached_to = block_check_attached(None, dev['device'], backend_xid = dev['xid'])
attached_to = block_check_attached(qvm_collection, dev)
if attached_to is None:
print >> sys.stderr, "WARNING: Device not connected to any VM"
exit(0)
block_detach(None, attached_to['devid'], vm_xid=attached_to['xid'])
block_detach(attached_to['vm'], attached_to['frontend'])
else:
# do_list
if len(args) > 0:
parser.error ("Too many parameters")
kwargs = {}
kwargs['qvmc'] = qvm_collection
kwargs['system_disks'] = options.system_disks
for dev in block_list(**kwargs).values():
attached_to = block_check_attached(None, dev['device'], backend_xid = dev['xid'])
attached_to = block_check_attached(qvm_collection, dev)
attached_to_str = ""
if attached_to:
attached_to_str = " (attached to '%s' as '%s')" % (attached_to['vm'], attached_to['frontend'])
attached_to_str = " (attached to '%s' as '%s')" % (
attached_to['vm'].name, attached_to['frontend'])
size_str = bytes_to_kmg(dev['size'])
print "%s\t%s %s%s" % (dev['name'], dev['desc'], size_str, attached_to_str)
exit (0)