diff --git a/Makefile b/Makefile index 5632f326..6c798289 100644 --- a/Makefile +++ b/Makefile @@ -57,6 +57,8 @@ ifeq ($(OS),Linux) $(MAKE) install -C linux/system-config endif python setup.py install -O1 --skip-build --root $(DESTDIR) + ln -s qvm-device $(DESTDIR)/usr/bin/qvm-pci + ln -s qvm-device $(DESTDIR)/usr/bin/qvm-usb # $(MAKE) install -C tests $(MAKE) install -C relaxng mkdir -p $(DESTDIR)/etc/qubes diff --git a/doc/example.xml b/doc/example.xml index c55b952b..6d63723f 100644 --- a/doc/example.xml +++ b/doc/example.xml @@ -22,7 +22,7 @@ - 01:23.45 + diff --git a/qubes/core2migration.py b/qubes/core2migration.py index 01ecbf07..bea22ac7 100644 --- a/qubes/core2migration.py +++ b/qubes/core2migration.py @@ -187,12 +187,12 @@ class Core2Qubes(qubes.Qubes): services = ast.literal_eval(services) else: services = {} - for service, value in services.iteritems(): + for service, value in services.items(): feature = service for repl_feature, repl_service in \ qubes.ext.r3compatibility.\ R3Compatibility.features_to_services.\ - iteritems(): + items(): if repl_service == service: feature = repl_feature vm.features[feature] = value @@ -205,7 +205,8 @@ class Core2Qubes(qubes.Qubes): pcidevs = ast.literal_eval(pcidevs) for pcidev in pcidevs: try: - vm.devices["pci"].attach(pcidev) + vm.devices["pci"].attach( + self.domains[0].devices['pci'][pcidev]) except qubes.exc.QubesException as e: self.log.error("VM {}: {}".format(vm.name, str(e))) except (ValueError, LookupError) as err: diff --git a/qubes/devices.py b/qubes/devices.py index a10bf1c2..329ef36e 100644 --- a/qubes/devices.py +++ b/qubes/devices.py @@ -23,11 +23,42 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # -import re +'''API for various types of devices. +Main concept is that some domain main +expose (potentially multiple) devices, which can be attached to other domains. +Devices can be of different classes (like 'pci', 'usb', etc). Each device +class is implemented by an extension. + +Devices are identified by pair of (backend domain, `ident`), where `ident` is +:py:class:`str`. + +Such extension should provide: + - `qubes.devices` endpoint - a class descendant from + :py:class:`qubes.devices.DeviceInfo`, designed to hold device description ( + including class-specific properties) + - handle `device-attach:class` and `device-detach:class` events for + performing the attach/detach action; events are fired even when domain isn't + running and extension should be prepared for this + - handle `device-list:class` event - list devices exposed by particular + domain; it should return list of appropriate DeviceInfo objects + - handle `device-get:class` event - get one device object exposed by this + domain of given identifier + - handle `device-list-attached:class` event - list currently attached + devices to this domain +''' import qubes.utils +class DeviceNotAttached(qubes.exc.QubesException, KeyError): + '''Trying to detach not attached device''' + pass + +class DeviceAlreadyAttached(qubes.exc.QubesException, KeyError): + '''Trying to attach already attached device''' + pass + + class DeviceCollection(object): '''Bag for devices. @@ -35,6 +66,52 @@ class DeviceCollection(object): :param vm: VM for which we manage devices :param class_: device class + + This class emits following events on VM object: + + .. event:: device-attach: (device) + + Fired when device is attached to a VM. + + :param device: :py:class:`DeviceInfo` object to be attached + + .. event:: device-pre-attach: (device) + + Fired before device is attached to a VM + + :param device: :py:class:`DeviceInfo` object to be attached + + .. event:: device-detach: (device) + + Fired when device is detached from a VM. + + :param device: :py:class:`DeviceInfo` object to be attached + + .. event:: device-pre-detach: (device) + + Fired before device is detached from a VM + + :param device: :py:class:`DeviceInfo` object to be attached + + .. event:: device-list: + + Fired to get list of devices exposed by a VM. Handlers of this + event should return a list of py:class:`DeviceInfo` objects (or + appropriate class specific descendant) + + .. event:: device-get: (ident) + + Fired to get a single device, given by the `ident` parameter. + Handlers of this event should either return appropriate object of + :py:class:`DeviceInfo`, or :py:obj:`None`. Especially should not + raise :py:class:`exceptions.KeyError`. + + .. event:: device-list-attached: (persistent) + + Fired to get list of currently attached devices to a VM. Handlers + of this event should return list of devices actually attached to + a domain, regardless of its settings. + ''' def __init__(self, vm, class_): @@ -42,56 +119,107 @@ class DeviceCollection(object): self._class = class_ self._set = set() + self.devclass = qubes.utils.get_entry_point_one( + 'qubes.devices', self._class) - def attach(self, device): + def attach(self, device, persistent=True): '''Attach (add) device to domain. - :param str device: device identifier (format is class-dependent) + :param DeviceInfo device: device object ''' - try: - devclass = qubes.utils.get_entry_point_one( - 'qubes.devices', self._class) - except KeyError: - devclass = str - - if not isinstance(device, devclass): - device = devclass(device) - - if device in self: - raise KeyError( + if device in self.attached(): + raise DeviceAlreadyAttached( 'device {!r} of class {} already attached to {!r}'.format( device, self._class, self._vm)) self._vm.fire_event_pre('device-pre-attach:' + self._class, device) - self._set.add(device) + if persistent: + self._set.add(device) self._vm.fire_event('device-attach:' + self._class, device) - def detach(self, device): + def detach(self, device, persistent=True): '''Detach (remove) device from domain. - :param str device: device identifier (format is class-dependent) + :param DeviceInfo device: device object ''' - if device not in self: - raise KeyError( - 'device {!r} of class {} not attached to {!r}'.format( + if device not in self.attached(): + raise DeviceNotAttached( + 'device {!s} of class {} not attached to {!s}'.format( device, self._class, self._vm)) self._vm.fire_event_pre('device-pre-detach:' + self._class, device) - self._set.remove(device) + if persistent: + self._set.remove(device) self._vm.fire_event('device-detach:' + self._class, device) + def attached(self, persistent=None): + '''List devices which are (or may be) attached to this vm + + Devices may be attached persistently (so they are included in + :file:`qubes.xml`) or not. Device can also be in :file:`qubes.xml`, + but be temporarily detached. + + :param bool persistent: only include devices which are (or are not) \ + attached persistently - None means both + ''' + seen = self._set.copy() + + # ask for really attached devices only when requested not only + # persistent ones + if persistent is not True: + attached = self._vm.fire_event( + 'device-list-attached:' + self._class, + persistent=persistent) + for device in attached: + device_persistent = device in self._set + if persistent is not None and device_persistent != persistent: + continue + assert device.frontend_domain == self._vm, \ + '{!r} != {!r}'.format(device.frontend_domain, self._vm) + + yield device + + try: + seen.remove(device) + except KeyError: + pass + + if persistent is False: + return + + for device in seen: + # get fresh object - may contain updated information + device = device.backend_domain.devices[self._class][device.ident] + yield device + + def available(self): + '''List devices exposed by this vm''' + devices = self._vm.fire_event('device-list:' + self._class) + return devices def __iter__(self): - return iter(self._set) + return iter(self.available()) + def __getitem__(self, ident): + '''Get device object with given ident. - def __contains__(self, item): - return item in self._set + :returns: py:class:`DeviceInfo` + If domain isn't running, it is impossible to check device validity, + so return UnknownDevice object. Also do the same for non-existing + devices - otherwise it will be impossible to detach already + disconnected device. - def __len__(self): - return len(self._set) + :raises AssertionError: when multiple devices with the same ident are + found + ''' + dev = self._vm.fire_event('device-get:' + self._class, ident) + if dev: + assert len(dev) == 1 + return dev[0] + else: + return UnknownDevice(self._vm, ident) class DeviceManager(dict): @@ -109,30 +237,59 @@ class DeviceManager(dict): return self[key] -class RegexDevice(str): - regex = None - def __init__(self, *args, **kwargs): - super(RegexDevice, self).__init__(*args, **kwargs) +class DeviceInfo(object): + # pylint: disable=too-few-public-methods + def __init__(self, backend_domain, ident, description=None, + frontend_domain=None, **kwargs): + #: domain providing this device + self.backend_domain = backend_domain + #: device identifier (unique for given domain and device type) + self.ident = ident + # allow redefining those as dynamic properties in subclasses + try: + #: human readable description/name of the device + self.description = description + except AttributeError: + pass + try: + #: (running) domain to which device is currently attached + self.frontend_domain = frontend_domain + except AttributeError: + pass + self.data = kwargs - if self.regex is None: - raise NotImplementedError( - 'You should overload .regex attribute in subclass') + if hasattr(self, 'regex'): + # pylint: disable=no-member + dev_match = self.regex.match(ident) + if not dev_match: + raise ValueError('Invalid device identifier: {!r}'.format( + ident)) - dev_match = self.regex.match(self) - if not dev_match: - raise ValueError('Invalid device identifier: {!r}'.format(self)) + for group in self.regex.groupindex: + setattr(self, group, dev_match.group(group)) - for group in self.regex.groupindex: - setattr(self, group, dev_match.group(group)) + def __hash__(self): + return hash(self.ident) + def __eq__(self, other): + return ( + self.backend_domain == other.backend_domain and + self.ident == other.ident + ) -class PCIDevice(RegexDevice): - regex = re.compile( - r'^(?P[0-9a-f]+):(?P[0-9a-f]+)\.(?P[0-9a-f]+)$') + def __str__(self): + return '{!s}:{!s}'.format(self.backend_domain, self.ident) - @property - def libvirt_name(self): - return 'pci_0000_{}_{}_{}'.format(self.bus, self.device, self.function) +class UnknownDevice(DeviceInfo): + # pylint: disable=too-few-public-methods + '''Unknown device - for example exposed by domain not running currently''' + + def __init__(self, backend_domain, ident, description=None, + frontend_domain=None, **kwargs): + if description is None: + description = "Unknown device" + super(UnknownDevice, self).__init__(backend_domain, ident, description, + frontend_domain, **kwargs) class BlockDevice(object): diff --git a/qubes/events.py b/qubes/events.py index be7d1309..f3381fe0 100644 --- a/qubes/events.py +++ b/qubes/events.py @@ -129,7 +129,7 @@ class Emitter(object): ''' if not self.events_enabled: - return + return [] effects = [] for cls in order: diff --git a/qubes/ext/pci.py b/qubes/ext/pci.py new file mode 100644 index 00000000..2f08af26 --- /dev/null +++ b/qubes/ext/pci.py @@ -0,0 +1,303 @@ +#!/usr/bin/python2 -O +# vim: fileencoding=utf-8 +# +# The Qubes OS Project, https://www.qubes-os.org/ +# +# Copyright (C) 2016 Marek Marczykowski-Górecki +# +# +# 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 re +import subprocess +import libvirt +import lxml +import lxml.etree + +import qubes.devices +import qubes.ext + +#: cache of PCI device classes +pci_classes = None + + +def load_pci_classes(): + # List of known device classes, subclasses and programming interfaces + # Syntax: + # C class class_name + # subclass subclass_name <-- single tab + # prog-if prog-if_name <-- two tabs + result = {} + with open('/usr/share/hwdata/pci.ids') as pciids: + class_id = None + subclass_id = None + for line in pciids.readlines(): + line = line.rstrip() + if line.startswith('\t\t') and class_id and subclass_id: + (progif_id, _, class_name) = line[2:].split(' ', 2) + result[class_id + subclass_id + progif_id] = \ + class_name + elif line.startswith('\t') and class_id: + (subclass_id, _, class_name) = line[1:].split(' ', 2) + # store both prog-if specific entry and generic one + result[class_id + subclass_id + '00'] = \ + class_name + result[class_id + subclass_id] = \ + class_name + elif line.startswith('C '): + (_, class_id, _, class_name) = line.split(' ', 3) + result[class_id + '0000'] = class_name + result[class_id + '00'] = class_name + subclass_id = None + + return result + + +def pcidev_class(dev_xmldesc): + sysfs_path = dev_xmldesc.findtext('path') + assert sysfs_path + try: + class_id = open(sysfs_path + '/class').read().strip() + except OSError: + return "Unknown" + + if not qubes.ext.pci.pci_classes: + qubes.ext.pci.pci_classes = load_pci_classes() + if class_id.startswith('0x'): + class_id = class_id[2:] + try: + # ignore prog-if + return qubes.ext.pci.pci_classes[class_id[0:4]] + except KeyError: + return "Unknown" + + +def attached_devices(app): + """Return map device->domain-name for all currently attached devices""" + + # Libvirt do not expose nice API to query where the device is + # attached. The only way would be to query _all_ the domains ( + # each with separate libvirt call) and look if the device is + # there. Horrible waste of resources. + # Instead, do this on much lower level - xenstore info for + # xen-pciback driver, where we get all the info at once + + xs = app.vmm.xs + devices = {} + for domid in xs.ls('', 'backend/pci'): + for devid in xs.ls('', 'backend/pci/' + domid): + devpath = 'backend/pci/' + domid + '/' + devid + domain_name = xs.read('', devpath + '/domain') + try: + domain = app.domains[domain_name] + except KeyError: + # unknown domain - maybe from another qubes.xml? + continue + devnum = xs.read('', devpath + '/num_devs') + for dev in range(int(devnum)): + dbdf = xs.read('', devpath + '/dev-' + str(dev)) + bdf = dbdf[len('0000:'):] + devices[bdf] = domain + + return devices + + +def _device_desc(hostdev_xml): + return '{devclass}: {vendor} {product}'.format( + devclass=pcidev_class(hostdev_xml), + vendor=hostdev_xml.findtext('capability/vendor'), + product=hostdev_xml.findtext('capability/product'), + ) + + + +class PCIDevice(qubes.devices.DeviceInfo): + # pylint: disable=too-few-public-methods + regex = re.compile( + r'^(?P[0-9a-f]+):(?P[0-9a-f]+)\.(?P[0-9a-f]+)$') + libvirt_regex = re.compile( + r'^pci_0000_(?P[0-9a-f]+)_(?P[0-9a-f]+)_' + r'(?P[0-9a-f]+)$') + + def __init__(self, backend_domain, ident, libvirt_name=None): + if libvirt_name: + dev_match = self.libvirt_regex.match(libvirt_name) + assert dev_match + ident = '{bus}:{device}.{function}'.format(**dev_match.groupdict()) + + super(PCIDevice, self).__init__(backend_domain, ident, None) + + # lazy loading + self._description = None + + @property + def libvirt_name(self): + # pylint: disable=no-member + # noinspection PyUnresolvedReferences + return 'pci_0000_{}_{}_{}'.format(self.bus, self.device, self.function) + + @property + def description(self): + if self._description is None: + hostdev_details = \ + self.backend_domain.app.vmm.libvirt_conn.nodeDeviceLookupByName( + self.libvirt_name + ) + self._description = _device_desc(lxml.etree.fromstring( + hostdev_details.XMLDesc())) + return self._description + + @property + def frontend_domain(self): + # TODO: cache this + all_attached = attached_devices(self.backend_domain.app) + return all_attached.get(self.ident, None) + + +class PCIDeviceExtension(qubes.ext.Extension): + def __init__(self): + super(PCIDeviceExtension, self).__init__() + # lazy load this + self.pci_classes = {} + + @qubes.ext.handler('device-list:pci') + def on_device_list_pci(self, vm, event): + # pylint: disable=unused-argument,no-self-use + # only dom0 expose PCI devices + if vm.qid != 0: + return + + for dev in vm.app.vmm.libvirt_conn.listAllDevices(): + if 'pci' not in dev.listCaps(): + continue + + xml_desc = lxml.etree.fromstring(dev.XMLDesc()) + libvirt_name = xml_desc.findtext('name') + yield PCIDevice(vm, None, libvirt_name=libvirt_name) + + @qubes.ext.handler('device-get:pci') + def on_device_get_pci(self, vm, event, ident): + # pylint: disable=unused-argument,no-self-use + if not vm.app.vmm.offline_mode: + yield PCIDevice(vm, ident) + + @qubes.ext.handler('device-list-attached:pci') + def on_device_list_attached(self, vm, event, **kwargs): + # pylint: disable=unused-argument,no-self-use + if not vm.is_running() or isinstance(vm, qubes.vm.adminvm.AdminVM): + return + xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc()) + + for hostdev in xml_desc.findall('devices/hostdev'): + if hostdev.get('type') != 'pci': + continue + address = hostdev.find('source/address') + bus = address.get('bus')[2:] + device = address.get('slot')[2:] + function = address.get('function')[2:] + + ident = '{bus}:{device}.{function}'.format( + bus=bus, + device=device, + function=function, + ) + yield PCIDevice(vm.app.domains[0], ident) + + @qubes.ext.handler('device-pre-attach:pci') + def on_device_pre_attached_pci(self, vm, event, device): + # pylint: disable=unused-argument + if not os.path.exists('/sys/bus/pci/devices/0000:{}'.format( + device.ident)): + raise qubes.exc.QubesException( + 'Invalid PCI device: {}'.format(device.ident)) + + if not vm.is_running(): + return + + try: + self.bind_pci_to_pciback(vm.app, device) + vm.libvirt_domain.attachDevice( + vm.app.env.get_template('libvirt/devices/pci.xml').render( + device=device, vm=vm)) + except subprocess.CalledProcessError as e: + vm.log.exception('Failed to attach PCI device {!r} on the fly,' + ' changes will be seen after VM restart.'.format( + device.ident), e) + + @qubes.ext.handler('device-pre-detach:pci') + def on_device_pre_detached_pci(self, vm, event, device): + # pylint: disable=unused-argument,no-self-use + if not vm.is_running(): + return + + # this cannot be converted to general API, because there is no + # provision in libvirt for extracting device-side BDF; we need it for + # qubes.DetachPciDevice, which unbinds driver, not to oops the kernel + + p = subprocess.Popen(['xl', 'pci-list', str(vm.xid)], + stdout=subprocess.PIPE) + result = p.communicate()[0] + m = re.search(r'^(\d+.\d+)\s+0000:{}$'.format(device.ident), result, + flags=re.MULTILINE) + if not m: + vm.log.error('Device %s already detached', device.ident) + return + vmdev = m.group(1) + try: + vm.run_service('qubes.DetachPciDevice', + user='root', input='00:{}'.format(vmdev)) + vm.libvirt_domain.detachDevice( + vm.app.env.get_template('libvirt/devices/pci.xml').render( + device=device, vm=vm)) + except (subprocess.CalledProcessError, libvirt.libvirtError) as e: + vm.log.exception('Failed to detach PCI device {!r} on the fly,' + ' changes will be seen after VM restart.'.format( + device.ident), e) + raise + + @qubes.ext.handler('domain-pre-start') + def on_domain_pre_start(self, vm, _event, **_kwargs): + # Bind pci devices to pciback driver + for pci in vm.devices['pci'].attached(): + self.bind_pci_to_pciback(vm.app, pci) + + @staticmethod + def bind_pci_to_pciback(app, device): + '''Bind PCI device to pciback driver. + + :param qubes.devices.PCIDevice device: device to attach + + Devices should be unbound from their normal kernel drivers and bound to + the dummy driver, which allows for attaching them to a domain. + ''' + try: + node = app.vmm.libvirt_conn.nodeDeviceLookupByName( + device.libvirt_name) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_NODE_DEVICE: + raise qubes.exc.QubesException( + 'PCI device {!r} does not exist'.format( + device)) + raise + + try: + node.dettach() + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_INTERNAL_ERROR: + # allreaddy dettached + pass + else: + raise diff --git a/qubes/storage/__init__.py b/qubes/storage/__init__.py index 4d04a818..64827ad1 100644 --- a/qubes/storage/__init__.py +++ b/qubes/storage/__init__.py @@ -255,9 +255,9 @@ class Storage(object): if not rw: lxml.etree.SubElement(disk, 'readonly') - if self.vm.qid != 0: + if volume.domain is not None: lxml.etree.SubElement(disk, 'backenddomain').set( - 'name', volume.pool.split('p_')[1]) + 'name', volume.domain.name) xml_string = lxml.etree.tostring(disk, encoding='utf-8') self.vm.libvirt_domain.attachDevice(xml_string) diff --git a/qubes/storage/domain.py b/qubes/storage/domain.py index d9d6207d..dce2acde 100644 --- a/qubes/storage/domain.py +++ b/qubes/storage/domain.py @@ -86,7 +86,7 @@ class DomainPool(Pool): devices[name][atr] = value - return [DomainVolume(n, self.name, **atrs) + return [DomainVolume(n, self.vm, self.name, **atrs) for n, atrs in devices.items()] def clone(self, source, target): @@ -99,11 +99,12 @@ class DomainPool(Pool): class DomainVolume(Volume): ''' A volume provided by a block device in an domain ''' - def __init__(self, name, pool, desc, mode, **kwargs): + def __init__(self, vm, name, pool, desc, mode, **kwargs): rw = (mode == 'w') super(DomainVolume, self).__init__(desc, pool, vid=name, removable=True, rw=rw, **kwargs) + self.domain = vm @property def revisions(self): diff --git a/qubes/storage/file.py b/qubes/storage/file.py index a98ec7e4..d056b8b7 100644 --- a/qubes/storage/file.py +++ b/qubes/storage/file.py @@ -31,6 +31,7 @@ import os.path import re import subprocess +import qubes.devices import qubes.storage BLKSIZE = 512 diff --git a/qubes/tests/__init__.py b/qubes/tests/__init__.py index 14667878..d3c58779 100644 --- a/qubes/tests/__init__.py +++ b/qubes/tests/__init__.py @@ -135,12 +135,15 @@ class TestEmitter(qubes.events.Emitter): self.fired_events = collections.Counter() def fire_event(self, event, *args, **kwargs): - super(TestEmitter, self).fire_event(event, *args, **kwargs) + effects = super(TestEmitter, self).fire_event(event, *args, **kwargs) self.fired_events[(event, args, tuple(sorted(kwargs.items())))] += 1 + return effects def fire_event_pre(self, event, *args, **kwargs): - super(TestEmitter, self).fire_event_pre(event, *args, **kwargs) + effects = super(TestEmitter, self).fire_event_pre(event, *args, + **kwargs) self.fired_events[(event, args, tuple(sorted(kwargs.items())))] += 1 + return effects def expectedFailureIfTemplate(templates): """ @@ -972,6 +975,8 @@ def load_tests(loader, tests, pattern): # pylint: disable=unused-argument 'qubes.tests.vm.mix.net', 'qubes.tests.vm.adminvm', 'qubes.tests.app', + 'qubes.tests.tools.qvm_device', + 'qubes.tests.tools.qvm_ls', ): tests.addTests(loader.loadTestsFromName(modname)) @@ -984,6 +989,7 @@ def load_tests(loader, tests, pattern): # pylint: disable=unused-argument for modname in ( # integration tests 'qubes.tests.int.basic', + 'qubes.tests.int.devices_pci', 'qubes.tests.int.dom0_update', 'qubes.tests.int.network', 'qubes.tests.int.dispvm', diff --git a/qubes/tests/devices.py b/qubes/tests/devices.py index ebd4332b..a851ac83 100644 --- a/qubes/tests/devices.py +++ b/qubes/tests/devices.py @@ -27,24 +27,68 @@ import qubes.devices import qubes.tests +class TestDevice(qubes.devices.DeviceInfo): + pass + + +class TestVMCollection(dict): + def __iter__(self): + return iter(set(self.values())) + + +class TestApp(object): + def __init__(self): + self.domains = TestVMCollection() + + +class TestVM(qubes.tests.TestEmitter): + def __init__(self, app, name, *args, **kwargs): + super(TestVM, self).__init__(*args, **kwargs) + self.app = app + self.name = name + self.device = TestDevice(self, 'testdev', 'Description') + self.events_enabled = True + self.devices = { + 'testclass': qubes.devices.DeviceCollection(self, 'testclass') + } + self.app.domains[name] = self + self.app.domains[self] = self + + def __str__(self): + return self.name + + @qubes.events.handler('device-list-attached:testclass') + def dev_testclass_list_attached(self, event, persistent): + for vm in self.app.domains: + if vm.device.frontend_domain == self: + yield vm.device + + @qubes.events.handler('device-list:testclass') + def dev_testclass_list(self, event): + yield self.device + + class TC_00_DeviceCollection(qubes.tests.QubesTestCase): def setUp(self): - self.emitter = qubes.tests.TestEmitter() - self.collection = qubes.devices.DeviceCollection(self.emitter, 'testclass') + self.app = TestApp() + self.emitter = TestVM(self.app, 'vm') + self.app.domains['vm'] = self.emitter + self.device = self.emitter.device + self.collection = self.emitter.devices['testclass'] def test_000_init(self): self.assertFalse(self.collection._set) def test_001_attach(self): - self.collection.attach('testdev') + self.collection.attach(self.device) self.assertEventFired(self.emitter, 'device-pre-attach:testclass') self.assertEventFired(self.emitter, 'device-attach:testclass') self.assertEventNotFired(self.emitter, 'device-pre-detach:testclass') self.assertEventNotFired(self.emitter, 'device-detach:testclass') def test_002_detach(self): - self.collection.attach('testdev') - self.collection.detach('testdev') + self.collection.attach(self.device) + self.collection.detach(self.device) self.assertEventFired(self.emitter, 'device-pre-attach:testclass') self.assertEventFired(self.emitter, 'device-attach:testclass') self.assertEventFired(self.emitter, 'device-pre-detach:testclass') @@ -52,31 +96,56 @@ class TC_00_DeviceCollection(qubes.tests.QubesTestCase): def test_010_empty_detach(self): with self.assertRaises(LookupError): - self.collection.detach('testdev') + self.collection.detach(self.device) def test_011_double_attach(self): - self.collection.attach('testdev') + self.collection.attach(self.device) with self.assertRaises(LookupError): - self.collection.attach('testdev') + self.collection.attach(self.device) def test_012_double_detach(self): - self.collection.attach('testdev') - self.collection.detach('testdev') + self.collection.attach(self.device) + self.collection.detach(self.device) with self.assertRaises(LookupError): - self.collection.detach('testdev') + self.collection.detach(self.device) + + def test_013_list_attached_persistent(self): + self.assertEqual(set([]), set(self.collection.attached())) + self.collection.attach(self.device) + self.assertEqual({self.device}, set(self.collection.attached())) + self.assertEqual({self.device}, + set(self.collection.attached(persistent=True))) + self.assertEqual(set([]), + set(self.collection.attached(persistent=False))) + + def test_014_list_attached_non_persistent(self): + self.collection.attach(self.device, persistent=False) + # device-attach event not implemented, so manipulate object manually + self.device.frontend_domain = self.emitter + self.assertEqual({self.device}, + set(self.collection.attached())) + self.assertEqual(set([]), + set(self.collection.attached(persistent=True))) + self.assertEqual({self.device}, + set(self.collection.attached(persistent=False))) + + def test_015_list_available(self): + self.assertEqual({self.device}, set(self.collection)) class TC_01_DeviceManager(qubes.tests.QubesTestCase): def setUp(self): - self.emitter = qubes.tests.TestEmitter() + self.app = TestApp() + self.emitter = TestVM(self.app, 'vm') self.manager = qubes.devices.DeviceManager(self.emitter) def test_000_init(self): self.assertEqual(self.manager, {}) def test_001_missing(self): - self.manager['testclass'].attach('testdev') + device = TestDevice(self.emitter.app.domains['vm'], 'testdev') + self.manager['testclass'].attach(device) self.assertEventFired(self.emitter, 'device-attach:testclass') diff --git a/qubes/tests/int/backup.py b/qubes/tests/int/backup.py index db29c2b3..7caae244 100644 --- a/qubes/tests/int/backup.py +++ b/qubes/tests/int/backup.py @@ -67,7 +67,7 @@ class TC_00_Backup(qubes.tests.BackupTestsMixin, qubes.tests.QubesTestCase): self.assertEquals(orig_value, restored_value, "VM {} - property {} not properly restored".format( vm.name, prop)) - for dev_class in ["pci", "usb"]: + for dev_class in vm.devices.keys(): for dev in vm.devices[dev_class]: self.assertIn(dev, restored_vm.devices[dev_class]) diff --git a/qubes/tests/int/devices_pci.py b/qubes/tests/int/devices_pci.py new file mode 100644 index 00000000..73a6c1d2 --- /dev/null +++ b/qubes/tests/int/devices_pci.py @@ -0,0 +1,173 @@ +#!/usr/bin/python2 -O +# vim: fileencoding=utf-8 +# pylint: disable=protected-access,pointless-statement + +# +# The Qubes OS Project, https://www.qubes-os.org/ +# +# Copyright (C) 2015-2016 Joanna Rutkowska +# Copyright (C) 2015-2016 Wojtek Porczyk +# +# 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 subprocess + +import time + +import qubes.devices +import qubes.ext.pci +import qubes.tests + + +class TC_00_Devices_PCI(qubes.tests.SystemTestsMixin, + qubes.tests.QubesTestCase): + def setUp(self): + super(TC_00_Devices_PCI, self).setUp() + if self._testMethodName not in ['test_000_list']: + pcidev = os.environ.get('QUBES_TEST_PCIDEV', None) + if pcidev is None: + self.skipTest('Specify PCI device with QUBES_TEST_PCIDEV ' + 'environment variable') + self.dev = self.app.domains[0].devices['pci'][pcidev] + if isinstance(self.dev, qubes.devices.UnknownDevice): + self.skipTest('Specified device {} does not exists'.format(pcidev)) + self.init_default_template() + self.vm = self.app.add_new_vm(qubes.vm.appvm.AppVM, + name=self.make_vm_name('vm'), + label='red', + ) + self.vm.create_on_disk() + self.vm.features['pci-no-strict-reset/' + pcidev] = True + self.app.save() + + def test_000_list(self): + p = subprocess.Popen(['lspci'], stdout=subprocess.PIPE) + # get a dict: BDF -> description + actual_devices = dict( + l.split(' (')[0].split(' ', 1) + for l in p.communicate()[0].splitlines()) + for dev in self.app.domains[0].devices['pci']: + self.assertIsInstance(dev, qubes.ext.pci.PCIDevice) + self.assertEqual(dev.backend_domain, self.app.domains[0]) + self.assertIn(dev.ident, actual_devices) + self.assertEqual(dev.description, actual_devices[dev.ident]) + self.assertIsInstance(dev.frontend_domain, + (qubes.vm.BaseVM, None.__class__)) + actual_devices.pop(dev.ident) + + if actual_devices: + self.fail('Not all devices listed, missing: {}'.format( + actual_devices)) + + def test_010_attach_offline(self): + self.assertIsNone(self.dev.frontend_domain) + self.assertNotIn(self.dev, self.vm.devices['pci'].attached()) + self.assertNotIn(self.dev, self.vm.devices['pci'].attached( + persistent=True)) + self.assertNotIn(self.dev, self.vm.devices['pci'].attached( + persistent=False)) + + self.vm.devices['pci'].attach(self.dev) + self.app.save() + + # still should be None, as domain is not started yet + self.assertIsNone(self.dev.frontend_domain) + self.assertIn(self.dev, self.vm.devices['pci'].attached()) + self.assertIn(self.dev, self.vm.devices['pci'].attached( + persistent=True)) + self.assertNotIn(self.dev, self.vm.devices['pci'].attached( + persistent=False)) + self.vm.start() + + self.assertEqual(self.dev.frontend_domain, self.vm) + self.assertIn(self.dev, self.vm.devices['pci'].attached()) + self.assertIn(self.dev, self.vm.devices['pci'].attached( + persistent=True)) + self.assertNotIn(self.dev, self.vm.devices['pci'].attached( + persistent=False)) + + p = self.vm.run('lspci', passio_popen=True) + (stdout, _) = p.communicate() + self.assertIn(self.dev.description, stdout) + + def test_011_attach_online(self): + self.vm.start() + self.vm.devices['pci'].attach(self.dev) + + self.assertEqual(self.dev.frontend_domain, self.vm) + self.assertIn(self.dev, self.vm.devices['pci'].attached()) + self.assertIn(self.dev, self.vm.devices['pci'].attached( + persistent=True)) + self.assertNotIn(self.dev, self.vm.devices['pci'].attached( + persistent=False)) + + # give VM kernel some time to discover new device + time.sleep(1) + p = self.vm.run('lspci', passio_popen=True) + (stdout, _) = p.communicate() + self.assertIn(self.dev.description, stdout) + + def test_012_attach_online_temp(self): + self.vm.start() + self.vm.devices['pci'].attach(self.dev, persistent=False) + + self.assertEqual(self.dev.frontend_domain, self.vm) + self.assertIn(self.dev, self.vm.devices['pci'].attached()) + self.assertNotIn(self.dev, self.vm.devices['pci'].attached( + persistent=True)) + self.assertIn(self.dev, self.vm.devices['pci'].attached( + persistent=False)) + + # give VM kernel some time to discover new device + time.sleep(1) + p = self.vm.run('lspci', passio_popen=True) + (stdout, _) = p.communicate() + self.assertIn(self.dev.description, stdout) + + def test_020_detach_online(self): + self.vm.devices['pci'].attach(self.dev) + self.app.save() + self.vm.start() + + self.assertIn(self.dev, self.vm.devices['pci'].attached()) + self.assertIn(self.dev, self.vm.devices['pci'].attached( + persistent=True)) + self.assertNotIn(self.dev, self.vm.devices['pci'].attached( + persistent=False)) + self.assertEqual(self.dev.frontend_domain, self.vm) + + self.vm.devices['pci'].detach(self.dev) + + self.assertIsNone(self.dev.frontend_domain) + self.assertNotIn(self.dev, self.vm.devices['pci'].attached()) + self.assertNotIn(self.dev, self.vm.devices['pci'].attached( + persistent=True)) + self.assertNotIn(self.dev, self.vm.devices['pci'].attached( + persistent=False)) + + p = self.vm.run('lspci', passio_popen=True) + (stdout, _) = p.communicate() + self.assertNotIn(self.dev.description, stdout) + + # can't do this right now because of kernel bug - it cause the whole + # PCI bus being deregistered, which emit some warning in sysfs + # handling code (removing non-existing "0000:00" group) + # + # p = self.vm.run('dmesg', passio_popen=True) + # (stdout, _) = p.communicate() + # # check for potential oops + # self.assertNotIn('end trace', stdout) + diff --git a/qubes/tests/int/tools/qvm_check.py b/qubes/tests/int/tools/qvm_check.py index 8e11e59f..84628b0b 100644 --- a/qubes/tests/int/tools/qvm_check.py +++ b/qubes/tests/int/tools/qvm_check.py @@ -1,6 +1,5 @@ #!/usr/bin/python2 -O # vim: fileencoding=utf-8 -# pylint: disable=protected-access,pointless-statement # # The Qubes OS Project, https://www.qubes-os.org/ # diff --git a/qubes/tests/tools/__init__.py b/qubes/tests/tools/__init__.py index 388083ed..cae5797e 100644 --- a/qubes/tests/tools/__init__.py +++ b/qubes/tests/tools/__init__.py @@ -1 +1,42 @@ -# pylint: skip-file +#!/usr/bin/python2 -O +# vim: fileencoding=utf-8 + +# +# The Qubes OS Project, https://www.qubes-os.org/ +# +# Copyright (C) 2015 Marek Marczykowski-Górecki +# +# +# 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 sys +try: + import StringIO +except ImportError: + from io import StringIO + + +class StdoutBuffer(object): + def __init__(self): + self.stdout = StringIO.StringIO() + + def __enter__(self): + sys.stdout = self.stdout + return self.stdout + + def __exit__(self, exc_type, exc_val, exc_tb): + sys.stdout = sys.__stdout__ + return False diff --git a/qubes/tests/tools/qvm_device.py b/qubes/tests/tools/qvm_device.py new file mode 100644 index 00000000..6d7232d4 --- /dev/null +++ b/qubes/tests/tools/qvm_device.py @@ -0,0 +1,150 @@ +#!/usr/bin/python2 -O +# vim: fileencoding=utf-8 +# pylint: disable=protected-access,pointless-statement + +# +# The Qubes OS Project, https://www.qubes-os.org/ +# +# Copyright (C) 2015 Marek Marczykowski-Górecki +# +# +# 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 qubes +import qubes.devices +import qubes.tools.qvm_device + +import qubes.tests +import qubes.tests.devices +import qubes.tests.tools + +class TestNamespace(object): + def __init__(self, app, domains=None, device=None): + super(TestNamespace, self).__init__() + self.app = app + self.devclass = 'testclass' + if domains: + self.domains = domains + if device: + self.device = device + + +class TC_00_Actions(qubes.tests.QubesTestCase): + def setUp(self): + super(TC_00_Actions, self).setUp() + self.app = qubes.tests.devices.TestApp() + self.vm1 = qubes.tests.devices.TestVM(self.app, 'vm1') + self.vm2 = qubes.tests.devices.TestVM(self.app, 'vm2') + self.device = self.vm2.device + + def test_000_list_all(self): + args = TestNamespace(self.app) + with qubes.tests.tools.StdoutBuffer() as buf: + qubes.tools.qvm_device.list_devices(args) + self.assertEventFired(self.vm1, + 'device-list:testclass') + self.assertEventFired(self.vm2, + 'device-list:testclass') + self.assertEventNotFired(self.vm1, + 'device-list-attached:testclass') + self.assertEventNotFired(self.vm2, + 'device-list-attached:testclass') + self.assertEqual( + buf.getvalue(), + 'vm1:testdev Description \n' + 'vm2:testdev Description \n' + ) + + def test_001_list_one(self): + args = TestNamespace(self.app, [self.vm1]) + # simulate attach + self.vm2.device.frontend_domain = self.vm1 + self.vm1.devices['testclass']._set.add(self.device) + with qubes.tests.tools.StdoutBuffer() as buf: + qubes.tools.qvm_device.list_devices(args) + self.assertEventFired(self.vm1, + 'device-list-attached:testclass') + self.assertEventNotFired(self.vm1, + 'device-list:testclass') + self.assertEventNotFired(self.vm2, + 'device-list:testclass') + self.assertEventNotFired(self.vm2, + 'device-list-attached:testclass') + self.assertEqual( + buf.getvalue(), + 'vm2:testdev Description vm1\n' + ) + + def test_002_list_one_non_persistent(self): + args = TestNamespace(self.app, [self.vm1]) + # simulate attach + self.vm2.device.frontend_domain = self.vm1 + with qubes.tests.tools.StdoutBuffer() as buf: + qubes.tools.qvm_device.list_devices(args) + self.assertEventFired(self.vm1, + 'device-list-attached:testclass') + self.assertEventNotFired(self.vm1, + 'device-list:testclass') + self.assertEventNotFired(self.vm2, + 'device-list:testclass') + self.assertEventNotFired(self.vm2, + 'device-list-attached:testclass') + self.assertEqual( + buf.getvalue(), + 'vm2:testdev Description vm1\n' + ) + + def test_010_attach(self): + args = TestNamespace( + self.app, + [self.vm1], + self.device + ) + qubes.tools.qvm_device.attach_device(args) + self.assertEventFired(self.vm1, + 'device-attach:testclass', [self.device]) + self.assertEventNotFired(self.vm2, + 'device-attach:testclass', [self.device]) + + def test_011_double_attach(self): + args = TestNamespace( + self.app, + [self.vm1], + self.device + ) + qubes.tools.qvm_device.attach_device(args) + with self.assertRaises(qubes.exc.QubesException): + qubes.tools.qvm_device.attach_device(args) + + def test_020_detach(self): + args = TestNamespace( + self.app, + [self.vm1], + self.device + ) + # simulate attach + self.vm2.device.frontend_domain = self.vm1 + self.vm1.devices['testclass']._set.add(self.device) + qubes.tools.qvm_device.detach_device(args) + + def test_021_detach_not_attached(self): + args = TestNamespace( + self.app, + [self.vm1], + self.device + ) + with self.assertRaises(qubes.exc.QubesException): + qubes.tools.qvm_device.detach_device(args) diff --git a/qubes/tests/vm/init.py b/qubes/tests/vm/init.py index 05185f36..389bf42c 100644 --- a/qubes/tests/vm/init.py +++ b/qubes/tests/vm/init.py @@ -31,6 +31,18 @@ import qubes.vm import qubes.tests +class TestVMM(object): + def __init__(self): + super(TestVMM, self).__init__() + self.offline_mode = True + + +class TestApp(object): + def __init__(self): + super(TestApp, self).__init__() + self.domains = {} + self.vmm = TestVMM() + class TestVM(qubes.vm.BaseVM): qid = qubes.property('qid', type=int) @@ -66,7 +78,7 @@ class TC_10_BaseVM(qubes.tests.QubesTestCase): - 00:11.22 + @@ -81,7 +93,8 @@ class TC_10_BaseVM(qubes.tests.QubesTestCase): def test_000_load(self): node = self.xml.xpath('//domain')[0] - vm = TestVM(None, node) + vm = TestVM(TestApp(), node) + vm.app.domains['domain1'] = vm vm.load_properties(load_stage=None) vm.load_extras() @@ -97,7 +110,8 @@ class TC_10_BaseVM(qubes.tests.QubesTestCase): }) self.assertItemsEqual(vm.devices.keys(), ('pci',)) - self.assertItemsEqual(vm.devices['pci'], ('00:11.22',)) + self.assertItemsEqual(list(vm.devices['pci'].attached(persistent=True)), + [qubes.ext.pci.PCIDevice(vm, '00:11.22')]) self.assertXMLIsValid(vm.__xml__(), 'domain.rng') diff --git a/qubes/tools/__init__.py b/qubes/tools/__init__.py index f26fb3f9..43843480 100644 --- a/qubes/tools/__init__.py +++ b/qubes/tools/__init__.py @@ -525,6 +525,13 @@ def print_table(table): cmd = ['column', '-t', '-s', unit_separator] text_table = '\n'.join([unit_separator.join(row) for row in table]) - p = subprocess.Popen(cmd, stdin=subprocess.PIPE) - p.stdin.write(text_table) - p.communicate() + # for tests... + if sys.stdout != sys.__stdout__: + p = subprocess.Popen(cmd + ['-c', '80'], stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + p.stdin.write(text_table) + (out, _) = p.communicate() + sys.stdout.write(out) + else: + p = subprocess.Popen(cmd, stdin=subprocess.PIPE) + p.communicate(text_table) diff --git a/qubes/tools/qvm_device.py b/qubes/tools/qvm_device.py new file mode 100644 index 00000000..df20ad94 --- /dev/null +++ b/qubes/tools/qvm_device.py @@ -0,0 +1,203 @@ +#!/usr/bin/python2 +# coding=utf-8 +# pylint: disable=C,R +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2016 Bahtiar `kalkin-` Gadimov +# Copyright (C) 2016 Marek Marczykowski-Górecki +# +# +# 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. + +'''Qubes volume and block device managment''' + +from __future__ import print_function + +import argparse +import os +import sys + +import qubes +import qubes.devices +import qubes.exc +import qubes.tools + + +def prepare_table(dev_list): + ''' Converts a list of :py:class:`qubes.devices.DeviceInfo` objects to a + list of tupples for the :py:func:`qubes.tools.print_table`. + + If :program:`qvm-devices` is running in a TTY, it will ommit duplicate + data. + + :param list dev_list: List of :py:class:`qubes.devices.DeviceInfo` + objects. + :returns: list of tupples + ''' + output = [] + header = [] + if sys.stdout.isatty(): + header += [('BACKEND:DEVID', 'DESCRIPTION', 'USED BY')] # NOQA + + for dev in dev_list: + output += [( + "{!s}:{!s}".format(dev.backend_domain, dev.ident), + dev.description, + str(dev.frontend_domain) if dev.frontend_domain else "", + )] + + return header + sorted(output) + + +def list_devices(args): + ''' Called by the parser to execute the qubes-devices list + subcommand. ''' + app = args.app + + result = [] + if hasattr(args, 'domains') and args.domains: + for domain in args.domains: + result.extend(domain.devices[args.devclass].attached()) + else: + for backend in app.domains: + result.extend(backend.devices[args.devclass]) + + qubes.tools.print_table(prepare_table(result)) + + +def attach_device(args): + ''' Called by the parser to execute the :program:`qvm-devices attach` + subcommand. + ''' + device = args.device + vm = args.domains[0] + vm.devices[args.devclass].attach(device) + + +def detach_device(args): + ''' Called by the parser to execute the :program:`qvm-devices detach` + subcommand. + ''' + device = args.device + vm = args.domains[0] + vm.devices[args.devclass].detach(device) + + +def init_list_parser(sub_parsers): + ''' Configures the parser for the :program:`qvm-devices list` subcommand ''' + # pylint: disable=protected-access + list_parser = sub_parsers.add_parser('list', aliases=('ls', 'l'), + help='list devices') + + vm_name_group = qubes.tools.VmNameGroup( + list_parser, required=False, vm_action=qubes.tools.VmNameAction, + help='list devices assigned to specific domain(s)') + list_parser._mutually_exclusive_groups.append(vm_name_group) + list_parser.set_defaults(func=list_devices) + + +class DeviceAction(qubes.tools.QubesAction): + ''' Action for argument parser that gets the + :py:class:``qubes.storage.Volume`` from a POOL_NAME:VOLUME_ID string. + ''' + # pylint: disable=too-few-public-methods + + def __init__(self, help='A domain & device id combination', + required=True, allow_unknown=False, **kwargs): + # pylint: disable=redefined-builtin + super(DeviceAction, self).__init__(help=help, required=required, + **kwargs) + self.allow_unknown = allow_unknown + + def __call__(self, parser, namespace, values, option_string=None): + ''' Set ``namespace.device`` to ``values`` ''' + setattr(namespace, self.dest, values) + + def parse_qubes_app(self, parser, namespace): + ''' Acquire the :py:class:``qubes.devices.DeviceInfo`` object from + ``namespace.app``. + ''' + assert hasattr(namespace, 'app') + assert hasattr(namespace, 'devclass') + app = namespace.app + devclass = namespace.devclass + + try: + backend_name, devid = getattr(namespace, self.dest).split(':', 1) + try: + backend = app.domains[backend_name] + dev = backend.devices[devclass][devid] + if not self.allow_unknown and isinstance(dev, + qubes.devices.UnknownDevice): + parser.error_runtime('no device {!r} in qube {!r}'.format( + backend_name, devid)) + except KeyError: + parser.error_runtime('no domain {!r}'.format(backend_name)) + except ValueError: + parser.error('expected a domain & device id combination like ' + 'foo:bar') + +def get_parser(device_class=None): + '''Create :py:class:`argparse.ArgumentParser` suitable for + :program:`qvm-block`. + ''' + parser = qubes.tools.QubesArgumentParser(description=__doc__, want_app=True) + parser.register('action', 'parsers', qubes.tools.AliasedSubParsersAction) + if device_class: + parser.add_argument('devclass', const=device_class, + action='store_const', + help=argparse.SUPPRESS) + else: + parser.add_argument('devclass', metavar='DEVICE_CLASS', action='store', + help="Device class to manage ('pci', 'usb', etc)") + sub_parsers = parser.add_subparsers( + title='commands', + description="For more information see qvm-device command -h", + dest='command') + init_list_parser(sub_parsers) + attach_parser = sub_parsers.add_parser( + 'attach', help="Attach device to domain", aliases=('at', 'a')) + attach_parser.add_argument('VMNAME', action=qubes.tools.RunningVmNameAction) + attach_parser.add_argument(metavar='BACKEND:DEVICE_ID', dest='device', + action=qubes.tools.VolumeAction) + attach_parser.set_defaults(func=detach_device) + detach_parser = sub_parsers.add_parser( + "detach", help="Detach device from domain", aliases=('d', 'dt')) + detach_parser.add_argument('VMNAME', action=qubes.tools.RunningVmNameAction) + detach_parser.add_argument(metavar='BACKEND:DEVICE_ID', dest='device', + action=qubes.tools.VolumeAction) + detach_parser.set_defaults(func=detach_device) + + return parser + + +def main(args=None): + '''Main routine of :program:`qvm-block`.''' + basename = os.path.basename(sys.argv[0]) + devclass = None + if basename.startswith('qvm-') and basename != 'qvm-device': + devclass = basename[4:] + args = get_parser(devclass).parse_args(args) + try: + args.func(args) + except qubes.exc.QubesException as e: + print(e.message, file=sys.stderr) + return 1 + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/qubes/vm/__init__.py b/qubes/vm/__init__.py index f07512af..de167770 100644 --- a/qubes/vm/__init__.py +++ b/qubes/vm/__init__.py @@ -198,7 +198,11 @@ class BaseVM(qubes.PropertyHolder): for parent in self.xml.xpath('./devices'): devclass = parent.get('class') for node in parent.xpath('./device'): - self.devices[devclass].attach(node.text) + device = self.devices[devclass].devclass( + self.app.domains[node.get('backend-domain')], + node.get('id') + ) + self.devices[devclass].attach(device) # tags for node in self.xml.xpath('./tags/tag'): @@ -227,9 +231,10 @@ class BaseVM(qubes.PropertyHolder): for devclass in self.devices: devices = lxml.etree.Element('devices') devices.set('class', devclass) - for device in self.devices[devclass]: + for device in self.devices[devclass].attached(persistent=True): node = lxml.etree.Element('device') - node.text = device + node.set('backend-domain', device.backend_domain.name) + node.set('id', device.ident) devices.append(node) element.append(devices) diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index c28f20e6..b893aa24 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -243,7 +243,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): # CORE2: swallowed uses_default_kernelopts kernelopts = qubes.property('kernelopts', type=str, load_stage=4, default=(lambda self: qubes.config.defaults['kernelopts_pcidevs'] - if len(self.devices['pci']) > 0 + if list(self.devices['pci'].attached(persistent=True)) else self.template.kernelopts if hasattr(self, 'template') else qubes.config.defaults['kernelopts']), ls_width=30, @@ -476,6 +476,9 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): self.events_enabled = True self.fire_event('domain-init') + def __hash__(self): + return self.qid + def __xml__(self): element = super(QubesVM, self).__xml__() @@ -585,82 +588,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): raise qubes.exc.QubesException( 'Failed to reset autostart for VM in systemd') - @qubes.events.handler('device-pre-attach:pci') - def on_device_pre_attached_pci(self, event, device): - # pylint: disable=unused-argument - if not os.path.exists('/sys/bus/pci/devices/0000:{}'.format(device)): - raise qubes.exc.QubesException( - 'Invalid PCI device: {}'.format(device)) - - if not self.is_running(): - return - - try: - self.bind_pci_to_pciback(device) - self.libvirt_domain.attachDevice( - self.app.env.get_template('libvirt/devices/pci.xml').render( - device=device)) - except subprocess.CalledProcessError as e: - self.log.exception('Failed to attach PCI device {!r} on the fly,' - ' changes will be seen after VM restart.'.format(device), e) - - @qubes.events.handler('device-pre-detach:pci') - def on_device_pre_detached_pci(self, event, device): - # pylint: disable=unused-argument - if not self.is_running(): - return - - # this cannot be converted to general API, because there is no - # provision in libvirt for extracting device-side BDF; we need it for - # qubes.DetachPciDevice, which unbinds driver, not to oops the kernel - - p = subprocess.Popen(['xl', 'pci-list', str(self.xid)], - stdout=subprocess.PIPE) - result = p.communicate()[0] - m = re.search(r'^(\d+.\d+)\s+0000:{}$'.format(device), result, - flags=re.MULTILINE) - if not m: - self.log.error('Device %s already detached', device) - return - vmdev = m.group(1) - try: - self.run_service('qubes.DetachPciDevice', - user='root', input='00:{}'.format(vmdev)) - self.libvirt_domain.detachDevice( - self.app.env.get_template('libvirt/devices/pci.xml').render( - device=device)) - except (subprocess.CalledProcessError, libvirt.libvirtError) as e: - self.log.exception('Failed to detach PCI device {!r} on the fly,' - ' changes will be seen after VM restart.'.format(device), e) - raise - - def bind_pci_to_pciback(self, device): - '''Bind PCI device to pciback driver. - - :param qubes.devices.PCIDevice device: device to attach - - Devices should be unbound from their normal kernel drivers and bound to - the dummy driver, which allows for attaching them to a domain. - ''' - try: - node = self.app.vmm.libvirt_conn.nodeDeviceLookupByName( - device.libvirt_name) - except libvirt.libvirtError as e: - if e.get_error_code() == libvirt.VIR_ERR_NO_NODE_DEVICE: - raise qubes.exc.QubesException( - 'PCI device {!r} does not exist (domain {!r})'.format( - device, self.name)) - raise - - try: - node.dettach() - except libvirt.libvirtError as e: - if e.get_error_code() == libvirt.VIR_ERR_INTERNAL_ERROR: - # allreaddy dettached - pass - else: - raise - # # methods for changing domain state # @@ -699,10 +626,6 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): qmemman_client = self.request_memory(mem_required) - # Bind pci devices to pciback driver - for pci in self.devices['pci']: - self.bind_pci_to_pciback(pci) - self.libvirt_domain.createWithFlags(libvirt.VIR_DOMAIN_START_PAUSED) try: @@ -802,7 +725,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): if not self.is_running() and not self.is_paused(): raise qubes.exc.QubesVMNotRunningError(self) - if len(self.devices['pci']) > 0: + if list(self.devices['pci'].attached()): raise qubes.exc.QubesNotImplementedError( 'Cannot suspend domain {!r} which has PCI devices attached' .format(self.name)) diff --git a/relaxng/qubes.rng b/relaxng/qubes.rng index 667e263b..7197aa28 100644 --- a/relaxng/qubes.rng +++ b/relaxng/qubes.rng @@ -206,14 +206,24 @@ the parser will complain about missing combine= attribute on the second . - One device. This tag should contain some - identifier, format of which depends on - particular device class. + One device. It's identified by by a pair of + backend domain and some identifier (device class + dependant). - - - [0-9a-f]{2}:[0-9a-f]{2}.[0-9a-f]{2} - + + + Backend domain name. + + + [a-z0-9_]+ + + + + + + [0-9a-f]{2}:[0-9a-f]{2}.[0-9a-f]{2} + + diff --git a/rpm_spec/core-dom0.spec b/rpm_spec/core-dom0.spec index 9ccbd029..a0cca0db 100644 --- a/rpm_spec/core-dom0.spec +++ b/rpm_spec/core-dom0.spec @@ -250,6 +250,7 @@ fi %{python_sitelib}/qubes/tools/qvm_block.py* %{python_sitelib}/qubes/tools/qubes_lvm.py* %{python_sitelib}/qubes/tools/qvm_create.py* +%{python_sitelib}/qubes/tools/qvm_device.py* %{python_sitelib}/qubes/tools/qvm_features.py* %{python_sitelib}/qubes/tools/qvm_check.py* %{python_sitelib}/qubes/tools/qvm_clone.py* @@ -268,6 +269,7 @@ fi %dir %{python_sitelib}/qubes/ext %{python_sitelib}/qubes/ext/__init__.py* %{python_sitelib}/qubes/ext/gui.py* +%{python_sitelib}/qubes/ext/pci.py* %{python_sitelib}/qubes/ext/qubesmanager.py* %{python_sitelib}/qubes/ext/r3compatibility.py* @@ -297,6 +299,7 @@ fi %dir %{python_sitelib}/qubes/tests/tools %{python_sitelib}/qubes/tests/tools/__init__.py* %{python_sitelib}/qubes/tests/tools/init.py* +%{python_sitelib}/qubes/tests/tools/qvm_device.py* %{python_sitelib}/qubes/tests/tools/qvm_ls.py* %dir %{python_sitelib}/qubes/tests/int @@ -304,6 +307,7 @@ fi %{python_sitelib}/qubes/tests/int/backup.py* %{python_sitelib}/qubes/tests/int/backupcompatibility.py* %{python_sitelib}/qubes/tests/int/basic.py* +%{python_sitelib}/qubes/tests/int/devices_pci.py* %{python_sitelib}/qubes/tests/int/dispvm.py* %{python_sitelib}/qubes/tests/int/dom0_update.py* %{python_sitelib}/qubes/tests/int/network.py* diff --git a/setup.py b/setup.py index 95a3065f..414c1a1a 100644 --- a/setup.py +++ b/setup.py @@ -40,9 +40,11 @@ if __name__ == '__main__': 'qubes.ext.qubesmanager = qubes.ext.qubesmanager:QubesManager', 'qubes.ext.gui = qubes.ext.gui:GUI', 'qubes.ext.r3compatibility = qubes.ext.r3compatibility:R3Compatibility', + 'qubes.ext.pci = qubes.ext.pci:PCIDeviceExtension', ], 'qubes.devices': [ - 'pci = qubes.devices:PCIDevice', + 'pci = qubes.ext.pci:PCIDevice', + 'testclass = qubes.tests.devices:TestDevice', ], 'qubes.storage': [ 'file = qubes.storage.file:FilePool', diff --git a/templates/libvirt/devices/pci.xml b/templates/libvirt/devices/pci.xml index 5f81800f..cc33c05c 100644 --- a/templates/libvirt/devices/pci.xml +++ b/templates/libvirt/devices/pci.xml @@ -1,4 +1,8 @@ - +
{% endif %} - {% if vm.devices['pci'] and vm.features.get('pci-e820-host', True) %} + {% if vm.devices['pci'].attached(persistent=True) | list + and vm.features.get('pci-e820-host', True) %} @@ -80,7 +81,7 @@ {% endif %} {% if device.domain %} - + {% endif %} {% if device.script %} @@ -90,10 +91,10 @@ {% endfor %} {% if vm.netvm %} - {% include 'libvirt/devices/net.xml' %} + {% include 'libvirt/devices/net.xml' with context %} {% endif %} - {% for device in vm.devices.pci %} + {% for device in vm.devices.pci.attached(persistent=True) %} {% include 'libvirt/devices/pci.xml' %} {% endfor %}