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. # 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 QubesException
from qubes import vmm from qubes import vmm
@ -37,6 +40,11 @@ import xen.lowlevel.xs
BLKSIZE = 512 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): def mbytes_to_kmg(size):
if size > 1024: if size > 1024:
return "%d GiB" % (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 not None
assert vm.is_running() assert vm.is_running()
vbd_list = vmm.xs.ls('', '/local/domain/%d/device/vbd' % vm.xid) xml = vm.libvirt_domain.XMLDesc()
# xvd* devices parsed_xml = etree.fromstring(xml)
major = 202 used = [target.get('dev', None) for target in
# prefer xvdi parsed_xml.xpath("//domain/devices/disk/target")]
for minor in range(8*16,254,16)+range(0,8*16,16): for dev in AVAILABLE_FRONTENDS:
if vbd_list is None or str(major << 8 | minor) not in vbd_list: if dev not in used:
return block_devid_to_name(major << 8 | minor) return dev
return None return None
def block_list(vm = None, system_disks = False): def block_list_vm(vm, system_disks = False):
device_re = re.compile(r"^[a-z0-9-]{1,12}$") 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? # FIXME: any better idea of desc_re?
desc_re = re.compile(r"^.{1,255}$") desc_re = re.compile(r"^.{1,255}$")
mode_re = re.compile(r"^[rw]$") mode_re = re.compile(r"^[rw]$")
xs_trans = vmm.xs.transaction_start() assert vm is not None
vm_list = [] if not vm.is_running():
if vm is not None: return []
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')
devices_list = {} 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)) untrusted_devices = vm.qdb.multiread('/qubes-block-devices/')
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))
if device_size is None or device_desc is None or device_mode is None: def get_dev_item(dev, item):
print >> sys.stderr, "Missing field in %s device parameters" % device 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 continue
if not device_size.isdigit(): if untrusted_device_device is None:
print >> sys.stderr, "Invalid %s device size in VM '%s'" % (device, vm_name) 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 continue
if not desc_re.match(device_desc): device_device = untrusted_device_device
print >> sys.stderr, "Invalid %s device desc in VM '%s'" % (device, vm_name) if not untrusted_device_size.isdigit():
print >> sys.stderr, "Invalid %s device size in VM '%s'" % (
dev_name, vm.name)
continue continue
if not mode_re.match(device_mode): device_size = int(untrusted_device_size)
print >> sys.stderr, "Invalid %s device mode in VM '%s'" % (device, vm_name) if not desc_re.match(untrusted_device_desc):
print >> sys.stderr, "Invalid %s device desc in VM '%s'" % (
dev_name, vm.name)
continue continue
# Check if we know major number for this device; attach will work without this, but detach and check_attached don't device_desc = untrusted_device_desc
if block_name_to_majorminor(device) == (0, 0): if not mode_re.match(untrusted_device_mode):
print >> sys.stderr, "Unsupported device %s:%s" % (vm_name, device) print >> sys.stderr, "Invalid %s device mode in VM '%s'" % (
dev_name, vm.name)
continue continue
device_mode = untrusted_device_mode
if not system_disks: 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 continue
visible_name = "%s:%s" % (vm_name, device) visible_name = "%s:%s" % (vm.name, dev_name)
devices_list[visible_name] = {"name": visible_name, "xid":int(xid), devices_list[visible_name] = {
"vm": vm_name, "device":device, "size":int(device_size), "name": visible_name,
"desc":device_desc, "mode":device_mode} "vm": vm.name,
"device": device_device,
"size": device_size,
"desc": device_desc,
"mode": device_mode
}
vmm.xs.transaction_end(xs_trans)
return devices_list return devices_list
def block_check_attached(backend_vm, device, backend_xid = None): def block_list(qvmc = None, vm = None, system_disks = False):
if backend_xid is None: if vm is not None:
backend_xid = backend_vm.xid if not vm.is_running():
xs_trans = vmm.xs.transaction_start() return []
vm_list = vmm.xs.ls(xs_trans, '/local/domain/%d/backend/vbd' % backend_xid) else:
if vm_list is None: vm_list = [ vm ]
vmm.xs.transaction_end(xs_trans) else:
return None if qvmc is None:
device_majorminor = None raise QubesException("You must pass either qvm or vm argument")
try: vm_list = qvmc.values()
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/'))
if (device_majorminor and (tmp_major, tmp_minor) == device_majorminor) or \ devices_list = {}
(device_majorminor is None and dev_params == device): for vm in vm_list:
#TODO devices_list.update(block_list_vm(vm, system_disks))
vm_name = xl_ctx.domid_to_name(int(vm_xid)) return devices_list
frontend = block_devid_to_name(int(devid))
vmm.xs.transaction_end(xs_trans) def block_check_attached(qvmc, device):
return {"xid":int(vm_xid), "frontend": frontend, "devid": int(devid), "vm": vm_name} """
vmm.xs.transaction_end(xs_trans)
@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 return None
def block_attach(vm, backend_vm, device, frontend=None, mode="w", auto_detach=False, wait=True): def device_attach_check(vm, backend_vm, device, frontend, mode):
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):
""" Checks all the parameters, dies on errors """ """ Checks all the parameters, dies on errors """
if not vm.is_running(): if not vm.is_running():
raise QubesException("VM %s not running" % vm.name) 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(): if not backend_vm.is_running():
raise QubesException("VM %s not running" % backend_vm.name) 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: if frontend is None:
frontend = block_find_unused_frontend(vm) frontend = block_find_unused_frontend(vm)
if frontend is None: if frontend is None:
raise QubesException("No unused frontend found") raise QubesException("No unused frontend found")
else: else:
# Check if any device attached at this frontend # 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)) raise QubesException("Frontend %s busy in VM %s, detach it first" % (frontend, vm.name))
# Check if this device is attached to some domain # 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 attached_vm:
if auto_detach: if auto_detach:
block_detach(None, attached_vm['devid'], vm_xid=attached_vm['xid']) block_detach(attached_vm['vm'], attached_vm['frontend'])
else: 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('/'): disk = Element("disk")
backend_dev = 'script:file:' + device disk.set('type', 'block')
else: disk.set('device', 'disk')
backend_dev = 'phy:/dev/' + device 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) ] def block_detach(vm, frontend = "xvdi"):
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", vm_xid = None): xml = vm.libvirt_domain.XMLDesc()
# Get XID if not provided already parsed_xml = etree.fromstring(xml)
if vm_xid is None: attached = parsed_xml.xpath("//domain/devices/disk")
if not vm.is_running(): for disk in attached:
raise QubesException("VM %s not running" % vm.name) if frontend is not None and disk.find('target').get('dev') != frontend:
# FIXME: potential race # Not the device we are looking for
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
continue continue
devices_to_detach.append(devid) if frontend is None:
vmm.xs.transaction_end(xs_trans) # ignore system disks
for devid in devices_to_detach: if disk.find('domain') == None and \
xl_cmd = [ '/usr/sbin/xl', 'block-detach', str(vm_xid), devid] disk.find('source').get('dev').startswith(system_path[
subprocess.check_call(xl_cmd) "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 ###### ####### USB devices ######

View File

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