Merge remote-tracking branch 'kalkin/device-assignments' into core3-devel
* kalkin/device-assignments: (21 commits) PCI extension cache PCIDevice objects Make pylint ♥ Fix pylint warning no-else-return Fix pylint warning len-as-conditional device-list-attached event returns a dev/options tupples list DeviceAssignment options are now a dict Remove WrongAssignment exception Rename qubes.devices.BlockDevice to qubes.storage.BlockDevice Update relaxng devices option element Fix tests broken by the new assignment api Fix qubes.tests.devices Fix pci device integration tests qvm-device add support for assignments Update ext/pci to new api BaseVM add DeviceAssignment xml serialization Migrate DeviceCollection to new API Add PersistentCollection helper to qubes.devices Add DeviceAssignment qvm-device validates device parameters qvm-device fix handling of non block devices ...
This commit is contained in:
		
						commit
						50b812190b
					
				| @ -24,6 +24,7 @@ import xml.parsers.expat | ||||
| import lxml.etree | ||||
| 
 | ||||
| import qubes | ||||
| import qubes.devices | ||||
| import qubes.vm.appvm | ||||
| import qubes.vm.standalonevm | ||||
| import qubes.vm.templatevm | ||||
| @ -219,8 +220,10 @@ class Core2Qubes(qubes.Qubes): | ||||
|                 pcidevs = ast.literal_eval(pcidevs) | ||||
|             for pcidev in pcidevs: | ||||
|                 try: | ||||
|                     vm.devices["pci"].attach( | ||||
|                         self.domains[0].devices['pci'][pcidev]) | ||||
|                     dev = self.domains[0].devices['pci'][pcidev] | ||||
|                     assignment = qubes.devices.DeviceAssignment( | ||||
|                         backend_domain=dev.backend_domain, ident=dev.ident) | ||||
|                     vm.devices["pci"].attach(assignment) | ||||
|                 except qubes.exc.QubesException as e: | ||||
|                     self.log.error("VM {}: {}".format(vm.name, str(e))) | ||||
|         except (ValueError, LookupError) as err: | ||||
|  | ||||
							
								
								
									
										209
									
								
								qubes/devices.py
									
									
									
									
									
								
							
							
						
						
									
										209
									
								
								qubes/devices.py
									
									
									
									
									
								
							| @ -47,7 +47,6 @@ Such extension should provide: | ||||
| 
 | ||||
| import qubes.utils | ||||
| 
 | ||||
| 
 | ||||
| class DeviceNotAttached(qubes.exc.QubesException, KeyError): | ||||
|     '''Trying to detach not attached device''' | ||||
|     pass | ||||
| @ -57,6 +56,30 @@ class DeviceAlreadyAttached(qubes.exc.QubesException, KeyError): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class DeviceAssignment(object): # pylint: disable=too-few-public-methods | ||||
|     ''' Maps a device to a frontend_domain. ''' | ||||
| 
 | ||||
|     def __init__(self, backend_domain, ident, options=None, persistent=False, | ||||
|                  frontend_domain=None): | ||||
|         self.backend_domain = backend_domain | ||||
|         self.ident = ident | ||||
|         self.options = options or {} | ||||
|         self.persistent = persistent | ||||
|         self.frontend_domain = frontend_domain | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return "[%s]:%s" % (self.backend_domain, self.ident) | ||||
| 
 | ||||
|     def __hash__(self): | ||||
|         return hash((self.backend_domain, self.ident)) | ||||
| 
 | ||||
|     def __eq__(self, other): | ||||
|         if not isinstance(self, other.__class__): | ||||
|             raise NotImplementedError | ||||
| 
 | ||||
|         return self.backend_domain == other.backend_domain \ | ||||
|            and self.ident == other.ident | ||||
| 
 | ||||
| class DeviceCollection(object): | ||||
|     '''Bag for devices. | ||||
| 
 | ||||
| @ -115,87 +138,117 @@ class DeviceCollection(object): | ||||
|     def __init__(self, vm, class_): | ||||
|         self._vm = vm | ||||
|         self._class = class_ | ||||
|         self._set = set() | ||||
|         self._set = PersistentCollection() | ||||
| 
 | ||||
|         self.devclass = qubes.utils.get_entry_point_one( | ||||
|             'qubes.devices', self._class) | ||||
| 
 | ||||
|     def attach(self, device, persistent=True): | ||||
|     def attach(self, device_assignment: DeviceAssignment): | ||||
|         '''Attach (add) device to domain. | ||||
| 
 | ||||
|         :param DeviceInfo device: device object | ||||
|         ''' | ||||
| 
 | ||||
|         if device in self.attached(): | ||||
|         if not device_assignment.frontend_domain: | ||||
|             device_assignment.frontend_domain = self._vm | ||||
|         else: | ||||
|             assert device_assignment.frontend_domain == self._vm, \ | ||||
|                 "Trying to attach DeviceAssignment belonging to other domain" | ||||
| 
 | ||||
|         if not device_assignment.persistent and self._vm.is_halted(): | ||||
|             raise qubes.exc.QubesVMNotRunningError(self._vm, | ||||
|                 "Devices can only be attached  non-persistent to a running vm") | ||||
|         device = self._device(device_assignment) | ||||
|         if device in self.assignments(): | ||||
|             raise DeviceAlreadyAttached( | ||||
|                 'device {!r} of class {} already attached to {!r}'.format( | ||||
|                 'device {!s} of class {} already attached to {!s}'.format( | ||||
|                     device, self._class, self._vm)) | ||||
|         self._vm.fire_event_pre('device-pre-attach:'+self._class, device=device) | ||||
|         if persistent: | ||||
|             self._set.add(device) | ||||
|         if device_assignment.persistent: | ||||
|             self._set.add(device_assignment) | ||||
|         self._vm.fire_event('device-attach:' + self._class, device=device) | ||||
| 
 | ||||
| 
 | ||||
|     def detach(self, device, persistent=True): | ||||
|     def detach(self, device_assignment: DeviceAssignment): | ||||
|         '''Detach (remove) device from domain. | ||||
| 
 | ||||
|         :param DeviceInfo device: device object | ||||
|         ''' | ||||
| 
 | ||||
|         if device not in self.attached(): | ||||
|         if not device_assignment.frontend_domain: | ||||
|             device_assignment.frontend_domain = self._vm | ||||
| 
 | ||||
|         if device_assignment in self._set and not self._vm.is_halted(): | ||||
|             raise qubes.exc.QubesVMNotHaltedError(self._vm, | ||||
|                 "Can not remove a persistent attachment from a non halted vm") | ||||
|         if device_assignment not in self.assignments(): | ||||
|             raise DeviceNotAttached( | ||||
|                 'device {!s} of class {} not attached to {!s}'.format( | ||||
|                     device, self._class, self._vm)) | ||||
|                     device_assignment.ident, self._class, self._vm)) | ||||
| 
 | ||||
|         device = self._device(device_assignment) | ||||
|         self._vm.fire_event_pre('device-pre-detach:'+self._class, device=device) | ||||
|         if persistent: | ||||
|             self._set.remove(device) | ||||
|         if device in self._set: | ||||
|             device_assignment.persistent = True | ||||
|             self._set.discard(device_assignment) | ||||
| 
 | ||||
|         self._vm.fire_event('device-detach:' + self._class, device=device) | ||||
| 
 | ||||
|     def attached(self, persistent=None): | ||||
|         '''List devices which are (or may be) attached to this vm | ||||
|     def attached(self): | ||||
|         '''List devices which are (or may be) attached to this vm ''' | ||||
|         attached = self._vm.fire_event('device-list-attached:' + self._class) | ||||
|         if attached: | ||||
|             return [dev for dev, _ in attached] | ||||
| 
 | ||||
|         return [] | ||||
| 
 | ||||
|     def persistent(self): | ||||
|         ''' Devices persistently attached and safe to access before libvirt | ||||
|             bootstrap. | ||||
|         ''' | ||||
|         return [self._device(a) for a in self._set] | ||||
| 
 | ||||
|     def assignments(self, persistent=None): | ||||
|         '''List assignments for devices which are (or may be) attached to the | ||||
|            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 | ||||
|         :param bool persistent: only include devices which are or are not | ||||
|         attached persistently. | ||||
|         ''' | ||||
|         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 | ||||
|         devices = self._vm.fire_event('device-list-attached:' + self._class, | ||||
|             persistent=persistent) | ||||
|         result = [] | ||||
|         for dev, options in devices: | ||||
|             if dev in self._set and persistent is False: | ||||
|                 continue | ||||
|             elif dev in self._set: | ||||
|                 result.append(self._set.get(dev)) | ||||
|             elif dev not in self._set and persistent is True: | ||||
|                 continue | ||||
|             else: | ||||
|                 result.append( | ||||
|                     DeviceAssignment(backend_domain=dev.backend_domain, | ||||
|                                      ident=dev.ident, options=options, | ||||
|                                      frontend_domain=self._vm)) | ||||
|         if persistent is not False and not result: | ||||
|             result.extend(self._set) | ||||
|         return result | ||||
| 
 | ||||
|     def available(self): | ||||
|         '''List devices exposed by this vm''' | ||||
|         devices = self._vm.fire_event('device-list:' + self._class) | ||||
|         return devices | ||||
| 
 | ||||
|     def _device(self, assignment: DeviceAssignment): | ||||
|         ''' Helper method for geting a `qubes.devices.DeviceInfo` object from | ||||
|             `qubes.devices.DeviceAssignment`. ''' | ||||
| 
 | ||||
|         return assignment.backend_domain.devices[self._class][assignment.ident] | ||||
| 
 | ||||
|     def __iter__(self): | ||||
|         return iter(self.available()) | ||||
| 
 | ||||
| @ -216,6 +269,7 @@ class DeviceCollection(object): | ||||
|         if dev: | ||||
|             assert len(dev) == 1 | ||||
|             return dev[0] | ||||
| 
 | ||||
|         return UnknownDevice(self._vm, ident) | ||||
| 
 | ||||
| 
 | ||||
| @ -235,9 +289,10 @@ class DeviceManager(dict): | ||||
| 
 | ||||
| 
 | ||||
| class DeviceInfo(object): | ||||
|     ''' Holds all information about a device ''' | ||||
|     # pylint: disable=too-few-public-methods | ||||
|     def __init__(self, backend_domain, ident, description=None, | ||||
|             frontend_domain=None, **kwargs): | ||||
|                  frontend_domain=None, options=None, **kwargs): | ||||
|         #: domain providing this device | ||||
|         self.backend_domain = backend_domain | ||||
|         #: device identifier (unique for given domain and device type) | ||||
| @ -253,6 +308,7 @@ class DeviceInfo(object): | ||||
|             self.frontend_domain = frontend_domain | ||||
|         except AttributeError: | ||||
|             pass | ||||
|         self.options = options or dict() | ||||
|         self.data = kwargs | ||||
| 
 | ||||
|         if hasattr(self, 'regex'): | ||||
| @ -289,15 +345,52 @@ class UnknownDevice(DeviceInfo): | ||||
|             frontend_domain, **kwargs) | ||||
| 
 | ||||
| 
 | ||||
| class BlockDevice(object): | ||||
|     # pylint: disable=too-few-public-methods | ||||
|     def __init__(self, path, name, script=None, rw=True, domain=None, | ||||
|                  devtype='disk'): | ||||
|         assert name, 'Missing device name' | ||||
|         assert path, 'Missing device path' | ||||
|         self.path = path | ||||
|         self.name = name | ||||
|         self.rw = rw | ||||
|         self.script = script | ||||
|         self.domain = domain | ||||
|         self.devtype = devtype | ||||
| class PersistentCollection(object): | ||||
| 
 | ||||
|     ''' Helper object managing persistent `DeviceAssignment`s. | ||||
|     ''' | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         self._dict = {} | ||||
| 
 | ||||
|     def add(self, assignment: DeviceAssignment): | ||||
|         ''' Add assignment to collection ''' | ||||
|         assert assignment.persistent and assignment.frontend_domain | ||||
|         vm = assignment.backend_domain | ||||
|         ident = assignment.ident | ||||
|         key = (vm, ident) | ||||
|         assert key not in self._dict | ||||
| 
 | ||||
|         self._dict[key] = assignment | ||||
| 
 | ||||
|     def discard(self, assignment): | ||||
|         ''' Discard assignment from collection ''' | ||||
|         assert assignment.persistent and assignment.frontend_domain | ||||
|         vm = assignment.backend_domain | ||||
|         ident = assignment.ident | ||||
|         key = (vm, ident) | ||||
|         if key not in self._dict: | ||||
|             raise KeyError | ||||
|         del self._dict[key] | ||||
| 
 | ||||
|     def __contains__(self, device) -> bool: | ||||
|         vm = device.backend_domain | ||||
|         ident = device.ident | ||||
|         key = (vm, ident) | ||||
|         return key in self._dict | ||||
| 
 | ||||
|     def get(self, device: DeviceInfo) -> DeviceAssignment: | ||||
|         ''' Returns the corresponding `qubes.devices.DeviceAssignment` for the | ||||
|             device. ''' | ||||
|         vm = device.backend_domain | ||||
|         ident = device.ident | ||||
|         key = (vm, ident) | ||||
|         if key not in self._dict: | ||||
|             raise KeyError | ||||
|         return self._dict[key] | ||||
| 
 | ||||
|     def __iter__(self): | ||||
|         return self._dict.values().__iter__() | ||||
| 
 | ||||
|     def __len__(self) -> int: | ||||
|         return len(self._dict.keys()) | ||||
|  | ||||
| @ -19,6 +19,9 @@ | ||||
| # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||||
| # | ||||
| 
 | ||||
| ''' Qubes PCI Extensions ''' | ||||
| 
 | ||||
| import functools | ||||
| import os | ||||
| import re | ||||
| import subprocess | ||||
| @ -34,7 +37,7 @@ pci_classes = None | ||||
| 
 | ||||
| 
 | ||||
| def load_pci_classes(): | ||||
|     # List of known device classes, subclasses and programming interfaces | ||||
|     ''' List of known device classes, subclasses and programming interfaces. ''' | ||||
|     # Syntax: | ||||
|     # C class       class_name | ||||
|     #       subclass        subclass_name           <-- single tab | ||||
| @ -122,7 +125,6 @@ def _device_desc(hostdev_xml): | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| class PCIDevice(qubes.devices.DeviceInfo): | ||||
|     # pylint: disable=too-few-public-methods | ||||
|     regex = re.compile( | ||||
| @ -191,7 +193,7 @@ class PCIDeviceExtension(qubes.ext.Extension): | ||||
|     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) | ||||
|             yield _cache_get(vm, ident) | ||||
| 
 | ||||
|     @qubes.ext.handler('device-list-attached:pci') | ||||
|     def on_device_list_attached(self, vm, event, **kwargs): | ||||
| @ -213,7 +215,7 @@ class PCIDeviceExtension(qubes.ext.Extension): | ||||
|                 device=device, | ||||
|                 function=function, | ||||
|             ) | ||||
|             yield PCIDevice(vm.app.domains[0], ident) | ||||
|             yield (PCIDevice(vm.app.domains[0], ident), {}) | ||||
| 
 | ||||
|     @qubes.ext.handler('device-pre-attach:pci') | ||||
|     def on_device_pre_attached_pci(self, vm, event, device): | ||||
| @ -227,6 +229,7 @@ class PCIDeviceExtension(qubes.ext.Extension): | ||||
|             return | ||||
| 
 | ||||
|         try: | ||||
|             device = _cache_get(vm, device.ident) | ||||
|             self.bind_pci_to_pciback(vm.app, device) | ||||
|             vm.libvirt_domain.attachDevice( | ||||
|                 vm.app.env.get_template('libvirt/devices/pci.xml').render( | ||||
| @ -246,9 +249,10 @@ class PCIDeviceExtension(qubes.ext.Extension): | ||||
|         # provision in libvirt for extracting device-side BDF; we need it for | ||||
|         # qubes.DetachPciDevice, which unbinds driver, not to oops the kernel | ||||
| 
 | ||||
|         device = _cache_get(vm, device.ident) | ||||
|         p = subprocess.Popen(['xl', 'pci-list', str(vm.xid)], | ||||
|                 stdout=subprocess.PIPE) | ||||
|         result = p.communicate()[0] | ||||
|         result = p.communicate()[0].decode() | ||||
|         m = re.search(r'^(\d+.\d+)\s+0000:{}$'.format(device.ident), result, | ||||
|             flags=re.MULTILINE) | ||||
|         if not m: | ||||
| @ -270,8 +274,9 @@ class PCIDeviceExtension(qubes.ext.Extension): | ||||
|     @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) | ||||
|         for assignment in vm.devices['pci'].persistent(): | ||||
|             device = _cache_get(vm, assignment.ident) | ||||
|             self.bind_pci_to_pciback(vm.app, device) | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def bind_pci_to_pciback(app, device): | ||||
| @ -300,3 +305,8 @@ class PCIDeviceExtension(qubes.ext.Extension): | ||||
|                 pass | ||||
|             else: | ||||
|                 raise | ||||
| 
 | ||||
| @functools.lru_cache(maxsize=None) | ||||
| def _cache_get(vm, ident): | ||||
|     ''' Caching wrapper around `PCIDevice(vm, ident)`. ''' | ||||
|     return PCIDevice(vm, ident) | ||||
|  | ||||
| @ -55,6 +55,7 @@ class Element(object): | ||||
|         if wrap: | ||||
|             return ''.join(self.schema.wrapper.fill(p) + '\n\n' | ||||
|                 for p in textwrap.dedent(xml.text.strip('\n')).split('\n\n')) | ||||
| 
 | ||||
|         return ' '.join(xml.text.strip().split()) | ||||
| 
 | ||||
| 
 | ||||
| @ -211,6 +212,7 @@ Quick example, worth thousands lines of specification: | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     main(*sys.argv[1:])  # pylint: disable=no-value-for-parameter | ||||
|     # pylint: disable=no-value-for-parameter | ||||
|     main(*sys.argv[1:]) | ||||
| 
 | ||||
| # vim: ts=4 sw=4 et | ||||
|  | ||||
| @ -34,7 +34,6 @@ from datetime import datetime | ||||
| import lxml.etree | ||||
| import pkg_resources | ||||
| import qubes | ||||
| import qubes.devices | ||||
| import qubes.exc | ||||
| import qubes.utils | ||||
| 
 | ||||
| @ -46,6 +45,21 @@ class StoragePoolException(qubes.exc.QubesException): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class BlockDevice(object): | ||||
|     ''' Represents a storage block device. ''' | ||||
|     # pylint: disable=too-few-public-methods | ||||
|     def __init__(self, path, name, script=None, rw=True, domain=None, | ||||
|                  devtype='disk'): | ||||
|         assert name, 'Missing device name' | ||||
|         assert path, 'Missing device path' | ||||
|         self.path = path | ||||
|         self.name = name | ||||
|         self.rw = rw | ||||
|         self.script = script | ||||
|         self.domain = domain | ||||
|         self.devtype = devtype | ||||
| 
 | ||||
| 
 | ||||
| class Volume(object): | ||||
|     ''' Encapsulates all data about a volume for serialization to qubes.xml and | ||||
|         libvirt config. | ||||
| @ -119,10 +133,10 @@ class Volume(object): | ||||
|         return lxml.etree.Element('volume', **config) | ||||
| 
 | ||||
|     def block_device(self): | ||||
|         ''' Return :py:class:`qubes.devices.BlockDevice` for serialization in | ||||
|         ''' Return :py:class:`BlockDevice` for serialization in | ||||
|             the libvirt XML template as <disk>. | ||||
|         ''' | ||||
|         return qubes.devices.BlockDevice(self.path, self.name, self.script, | ||||
|         return BlockDevice(self.path, self.name, self.script, | ||||
|                                          self.rw, self.domain, self.devtype) | ||||
| 
 | ||||
|     @property | ||||
| @ -446,6 +460,7 @@ class Storage(object): | ||||
|             "You need to pass a Volume or pool name as str" | ||||
|         if isinstance(volume, Volume): | ||||
|             return self.pools[volume.name] | ||||
| 
 | ||||
|         return self.vm.app.pools[volume] | ||||
| 
 | ||||
|     def commit(self): | ||||
| @ -475,6 +490,7 @@ class Storage(object): | ||||
|             "You need to pass a Volume or pool name as str" | ||||
|         if isinstance(volume, Volume): | ||||
|             return self.pools[volume.name].export(volume) | ||||
| 
 | ||||
|         return self.pools[volume].export(self.vm.volumes[volume]) | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -30,7 +30,6 @@ import os.path | ||||
| import re | ||||
| import subprocess | ||||
| 
 | ||||
| import qubes.devices | ||||
| import qubes.storage | ||||
| 
 | ||||
| BLKSIZE = 512 | ||||
| @ -358,7 +357,7 @@ class FileVolume(qubes.storage.Volume): | ||||
|             return 'block-snapshot' | ||||
| 
 | ||||
|     def block_device(self): | ||||
|         ''' Return :py:class:`qubes.devices.BlockDevice` for serialization in | ||||
|         ''' Return :py:class:`qubes.storage.BlockDevice` for serialization in | ||||
|             the libvirt XML template as <disk>. | ||||
|         ''' | ||||
|         path = self.path | ||||
| @ -366,7 +365,7 @@ class FileVolume(qubes.storage.Volume): | ||||
|             path += ":" + self.path_source_cow | ||||
|         if self._is_origin or self._is_snapshot: | ||||
|             path += ":" + self.path_cow | ||||
|         return qubes.devices.BlockDevice(path, self.name, self.script, self.rw, | ||||
|         return qubes.storage.BlockDevice(path, self.name, self.script, self.rw, | ||||
|                                          self.domain, self.devtype) | ||||
| 
 | ||||
|     @property | ||||
| @ -378,6 +377,7 @@ class FileVolume(qubes.storage.Volume): | ||||
| 
 | ||||
|         if not os.path.exists(old_revision): | ||||
|             return {} | ||||
| 
 | ||||
|         seconds = os.path.getctime(old_revision) | ||||
|         iso_date = qubes.storage.isodate(seconds).split('.', 1)[0] | ||||
|         return {iso_date: old_revision} | ||||
|  | ||||
| @ -394,13 +394,14 @@ class ThinVolume(qubes.storage.Volume): | ||||
|             "You shouldn't use lvm size setter") | ||||
| 
 | ||||
|     def block_device(self): | ||||
|         ''' Return :py:class:`qubes.devices.BlockDevice` for serialization in | ||||
|         ''' Return :py:class:`qubes.storage.BlockDevice` for serialization in | ||||
|             the libvirt XML template as <disk>. | ||||
|         ''' | ||||
|         if self.snap_on_start: | ||||
|             return qubes.devices.BlockDevice( | ||||
|             return qubes.storage.BlockDevice( | ||||
|                 '/dev/' + self._vid_snap, self.name, self.script, | ||||
|                 self.rw, self.domain, self.devtype) | ||||
| 
 | ||||
|         return super(ThinVolume, self).block_device() | ||||
| 
 | ||||
|     @property | ||||
|  | ||||
| @ -26,6 +26,7 @@ import qubes.devices | ||||
| import qubes.tests | ||||
| 
 | ||||
| class TestDevice(qubes.devices.DeviceInfo): | ||||
|     # pylint: disable=too-few-public-methods | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| @ -35,6 +36,7 @@ class TestVMCollection(dict): | ||||
| 
 | ||||
| 
 | ||||
| class TestApp(object): | ||||
|     # pylint: disable=too-few-public-methods | ||||
|     def __init__(self): | ||||
|         self.domains = TestVMCollection() | ||||
| 
 | ||||
| @ -51,20 +53,25 @@ class TestVM(qubes.tests.TestEmitter): | ||||
|         } | ||||
|         self.app.domains[name] = self | ||||
|         self.app.domains[self] = self | ||||
|         self.running = False | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
| 
 | ||||
|     @qubes.events.handler('device-list-attached:testclass') | ||||
|     def dev_testclass_list_attached(self, event, persistent): | ||||
|     def dev_testclass_list_attached(self, event, persistent = False): | ||||
|         for vm in self.app.domains: | ||||
|             if vm.device.frontend_domain == self: | ||||
|                 yield vm.device | ||||
|                 yield (vm.device, {}) | ||||
| 
 | ||||
|     @qubes.events.handler('device-list:testclass') | ||||
|     def dev_testclass_list(self, event): | ||||
|         yield self.device | ||||
| 
 | ||||
|     def is_halted(self): | ||||
|         return not self.running | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| class TC_00_DeviceCollection(qubes.tests.QubesTestCase): | ||||
|     def setUp(self): | ||||
| @ -73,20 +80,25 @@ class TC_00_DeviceCollection(qubes.tests.QubesTestCase): | ||||
|         self.app.domains['vm'] = self.emitter | ||||
|         self.device = self.emitter.device | ||||
|         self.collection = self.emitter.devices['testclass'] | ||||
|         self.assignment = qubes.devices.DeviceAssignment( | ||||
|             backend_domain = self.device.backend_domain, | ||||
|             ident = self.device.ident, | ||||
|             persistent=True | ||||
|         ) | ||||
| 
 | ||||
|     def test_000_init(self): | ||||
|         self.assertFalse(self.collection._set) | ||||
| 
 | ||||
|     def test_001_attach(self): | ||||
|         self.collection.attach(self.device) | ||||
|         self.collection.attach(self.assignment) | ||||
|         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(self.device) | ||||
|         self.collection.detach(self.device) | ||||
|         self.collection.attach(self.assignment) | ||||
|         self.collection.detach(self.assignment) | ||||
|         self.assertEventFired(self.emitter, 'device-pre-attach:testclass') | ||||
|         self.assertEventFired(self.emitter, 'device-attach:testclass') | ||||
|         self.assertEventFired(self.emitter, 'device-pre-detach:testclass') | ||||
| @ -94,41 +106,43 @@ class TC_00_DeviceCollection(qubes.tests.QubesTestCase): | ||||
| 
 | ||||
|     def test_010_empty_detach(self): | ||||
|         with self.assertRaises(LookupError): | ||||
|             self.collection.detach(self.device) | ||||
|             self.collection.detach(self.assignment) | ||||
| 
 | ||||
|     def test_011_double_attach(self): | ||||
|         self.collection.attach(self.device) | ||||
|         self.collection.attach(self.assignment) | ||||
| 
 | ||||
|         with self.assertRaises(LookupError): | ||||
|             self.collection.attach(self.device) | ||||
|             self.collection.attach(self.assignment) | ||||
| 
 | ||||
|     def test_012_double_detach(self): | ||||
|         self.collection.attach(self.device) | ||||
|         self.collection.detach(self.device) | ||||
|         self.collection.attach(self.assignment) | ||||
|         self.collection.detach(self.assignment) | ||||
| 
 | ||||
|         with self.assertRaises(LookupError): | ||||
|             self.collection.detach(self.device) | ||||
|         with self.assertRaises(qubes.devices.DeviceNotAttached): | ||||
|             self.collection.detach(self.assignment) | ||||
| 
 | ||||
|     def test_013_list_attached_persistent(self): | ||||
|         self.assertEqual(set([]), set(self.collection.attached())) | ||||
|         self.assertEqual(set([]), set(self.collection.persistent())) | ||||
|         self.collection.attach(self.assignment) | ||||
|         self.assertEventFired(self.emitter, 'device-list-attached:testclass') | ||||
|         self.collection.attach(self.device) | ||||
|         self.assertEqual({self.device}, set(self.collection.attached())) | ||||
|         self.assertEqual({self.device}, set(self.collection.persistent())) | ||||
|         self.assertEqual({self.device}, | ||||
|             set(self.collection.attached(persistent=True))) | ||||
|             set(self.collection.persistent())) | ||||
|         self.assertEqual(set([]), | ||||
|             set(self.collection.attached(persistent=False))) | ||||
|             set(self.collection.attached())) | ||||
| 
 | ||||
|     def test_014_list_attached_non_persistent(self): | ||||
|         self.collection.attach(self.device, persistent=False) | ||||
|         self.assignment.persistent = False | ||||
|         self.emitter.running = True | ||||
|         self.collection.attach(self.assignment) | ||||
|         # 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))) | ||||
|             set(self.collection.persistent())) | ||||
|         self.assertEqual({self.device}, | ||||
|             set(self.collection.attached(persistent=False))) | ||||
|             set(self.collection.attached())) | ||||
|         self.assertEventFired(self.emitter, 'device-list-attached:testclass') | ||||
| 
 | ||||
|     def test_015_list_available(self): | ||||
| @ -147,6 +161,7 @@ class TC_01_DeviceManager(qubes.tests.QubesTestCase): | ||||
| 
 | ||||
|     def test_001_missing(self): | ||||
|         device = TestDevice(self.emitter.app.domains['vm'], 'testdev') | ||||
|         self.manager['testclass'].attach(device) | ||||
|         assignment = qubes.devices.DeviceAssignment(backend_domain=device.backend_domain, ident=device.ident, persistent=True) | ||||
|         self.manager['testclass'].attach(assignment) | ||||
|         self.assertEventFired(self.emitter, 'device-attach:testclass') | ||||
| 
 | ||||
|  | ||||
| @ -41,6 +41,7 @@ class TC_00_Devices_PCI(qubes.tests.SystemTestsMixin, | ||||
|                 self.skipTest('Specify PCI device with QUBES_TEST_PCIDEV ' | ||||
|                               'environment variable') | ||||
|             self.dev = self.app.domains[0].devices['pci'][pcidev] | ||||
|             self.assignment = qubes.devices.DeviceAssignment(backend_domain=self.dev.backend_domain, ident=self.dev.ident, persistent=True) | ||||
|             if isinstance(self.dev, qubes.devices.UnknownDevice): | ||||
|                 self.skipTest('Specified device {} does not exists'.format(pcidev)) | ||||
|             self.init_default_template() | ||||
| @ -64,110 +65,111 @@ class TC_00_Devices_PCI(qubes.tests.SystemTestsMixin, | ||||
|             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)) | ||||
|     def assertDeviceNotInCollection(self, dev, dev_col): | ||||
|         self.assertNotIn(dev, dev_col.attached()) | ||||
|         self.assertNotIn(dev, dev_col.persistent()) | ||||
|         self.assertNotIn(dev, dev_col.assignments()) | ||||
|         self.assertNotIn(dev, dev_col.assignments(persistent=True)) | ||||
| 
 | ||||
|         self.vm.devices['pci'].attach(self.dev) | ||||
|     def test_010_attach_offline_persistent(self): | ||||
|         dev_col = self.vm.devices['pci'] | ||||
|         self.assertDeviceNotInCollection(self.dev, dev_col) | ||||
|         dev_col.attach(self.assignment) | ||||
|         self.app.save() | ||||
|         self.assertNotIn(self.dev, dev_col.attached()) | ||||
|         self.assertIn(self.dev, dev_col.persistent()) | ||||
|         self.assertIn(self.dev, dev_col.assignments()) | ||||
|         self.assertIn(self.dev, dev_col.assignments(persistent=True)) | ||||
|         self.assertNotIn(self.dev, dev_col.assignments(persistent=False)) | ||||
| 
 | ||||
| 
 | ||||
|         # 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)) | ||||
| 
 | ||||
|         self.assertIn(self.dev, dev_col.attached()) | ||||
|         p = self.vm.run('lspci', passio_popen=True) | ||||
|         (stdout, _) = p.communicate() | ||||
|         self.assertIn(self.dev.description, stdout) | ||||
|         self.assertIn(self.dev.description, stdout.decode()) | ||||
| 
 | ||||
|     def test_011_attach_online(self): | ||||
| 
 | ||||
|     def test_011_attach_offline_temp_fail(self): | ||||
|         dev_col = self.vm.devices['pci'] | ||||
|         self.assertDeviceNotInCollection(self.dev, dev_col) | ||||
|         self.assignment.persistent = False | ||||
|         with self.assertRaises(qubes.exc.QubesVMNotRunningError): | ||||
|             dev_col.attach(self.assignment) | ||||
| 
 | ||||
| 
 | ||||
|     def test_020_attach_online_persistent(self): | ||||
|         self.vm.start() | ||||
|         self.vm.devices['pci'].attach(self.dev) | ||||
|         dev_col = self.vm.devices['pci'] | ||||
|         self.assertDeviceNotInCollection(self.dev, dev_col) | ||||
|         dev_col.attach(self.assignment) | ||||
| 
 | ||||
|         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)) | ||||
|         self.assertIn(self.dev, dev_col.attached()) | ||||
|         self.assertIn(self.dev, dev_col.persistent()) | ||||
|         self.assertIn(self.dev, dev_col.assignments()) | ||||
|         self.assertIn(self.dev, dev_col.assignments(persistent=True)) | ||||
|         self.assertNotIn(self.dev, dev_col.assignments(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) | ||||
|         self.assertIn(self.dev.description, stdout.decode()) | ||||
| 
 | ||||
|     def test_012_attach_online_temp(self): | ||||
| 
 | ||||
|     def test_021_persist_detach_online_fail(self): | ||||
|         dev_col = self.vm.devices['pci'] | ||||
|         self.assertDeviceNotInCollection(self.dev, dev_col) | ||||
|         dev_col.attach(self.assignment) | ||||
|         self.app.save() | ||||
|         self.vm.start() | ||||
|         self.vm.devices['pci'].attach(self.dev, persistent=False) | ||||
|         with self.assertRaises(qubes.exc.QubesVMNotHaltedError): | ||||
|             self.vm.devices['pci'].detach(self.assignment) | ||||
| 
 | ||||
|     def test_030_persist_attach_detach_offline(self): | ||||
|         dev_col = self.vm.devices['pci'] | ||||
|         self.assertDeviceNotInCollection(self.dev, dev_col) | ||||
|         dev_col.attach(self.assignment) | ||||
|         self.app.save() | ||||
|         self.assertNotIn(self.dev, dev_col.attached()) | ||||
|         self.assertIn(self.dev, dev_col.persistent()) | ||||
|         self.assertIn(self.dev, dev_col.assignments()) | ||||
|         self.assertIn(self.dev, dev_col.assignments(persistent=True)) | ||||
|         self.assertNotIn(self.dev, dev_col.assignments(persistent=False)) | ||||
|         dev_col.detach(self.assignment) | ||||
|         self.assertDeviceNotInCollection(self.dev, dev_col) | ||||
| 
 | ||||
|     def test_031_attach_detach_online_temp(self): | ||||
|         dev_col = self.vm.devices['pci'] | ||||
|         self.vm.start() | ||||
|         self.assignment.persistent = False | ||||
|         self.assertDeviceNotInCollection(self.dev, dev_col) | ||||
|         dev_col.attach(self.assignment) | ||||
| 
 | ||||
|         self.assertIn(self.dev, dev_col.attached()) | ||||
|         self.assertNotIn(self.dev, dev_col.persistent()) | ||||
|         self.assertIn(self.dev, dev_col.assignments()) | ||||
|         self.assertIn(self.dev, dev_col.assignments(persistent=False)) | ||||
|         self.assertNotIn(self.dev, dev_col.assignments(persistent=True)) | ||||
|         self.assertIn(self.dev, dev_col.assignments(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)) | ||||
|         self.assertIn(self.dev.description, stdout.decode()) | ||||
|         dev_col.detach(self.assignment) | ||||
|         self.assertDeviceNotInCollection(self.dev, dev_col) | ||||
| 
 | ||||
|         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) | ||||
| 
 | ||||
|         self.assertNotIn(self.dev.description, stdout.decode()) | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| # pylint: disable=protected-access,pointless-statement | ||||
| # pylint: disable=protected-access | ||||
| 
 | ||||
| # | ||||
| # The Qubes OS Project, https://www.qubes-os.org/ | ||||
| @ -21,6 +21,8 @@ | ||||
| # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||||
| # | ||||
| 
 | ||||
| ''' Tests for the `qvm-device` tool. ''' | ||||
| 
 | ||||
| import qubes | ||||
| import qubes.devices | ||||
| import qubes.tools.qvm_device | ||||
| @ -30,82 +32,86 @@ import qubes.tests.devices | ||||
| import qubes.tests.tools | ||||
| 
 | ||||
| class TestNamespace(object): | ||||
|     ''' A mock object for `argparse.Namespace`. | ||||
|     '''  # pylint: disable=too-few-public-methods | ||||
| 
 | ||||
|     def __init__(self, app, domains=None, device=None): | ||||
|         super(TestNamespace, self).__init__() | ||||
|         self.app = app | ||||
|         self.devclass = 'testclass' | ||||
|         self.persistent = True | ||||
|         if domains: | ||||
|             self.domains = domains | ||||
|         if device: | ||||
|             self.device = device | ||||
|             self.device_assignment = qubes.devices.DeviceAssignment( | ||||
|                 backend_domain=self.device.backend_domain, | ||||
|                 ident=self.device.ident, persistent=self.persistent) | ||||
| 
 | ||||
| 
 | ||||
| class TC_00_Actions(qubes.tests.QubesTestCase): | ||||
|     ''' Tests the output logic of the qvm-device tool ''' | ||||
|     def setUp(self): | ||||
|         super(TC_00_Actions, self).setUp() | ||||
|         self.app = qubes.tests.devices.TestApp() | ||||
|         def save(): | ||||
|             ''' A mock method for simulating a successful save ''' | ||||
|             return True | ||||
|         self.app.save = save | ||||
|         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): | ||||
|         ''' List all exposed vm devices. No devices are attached to other | ||||
|             domains. | ||||
|         ''' | ||||
|         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( | ||||
|                 [x.rstrip() for x in buf.getvalue().splitlines()], | ||||
|                 ['vm1:testdev  Description', | ||||
|                  'vm2:testdev  Description'] | ||||
|             ) | ||||
| 
 | ||||
|     def test_001_list_one(self): | ||||
|     def test_001_list_persistent_attach(self): | ||||
|         ''' Attach the device exposed by the `vm2` to the `vm1` persistently. | ||||
|         ''' | ||||
|         args = TestNamespace(self.app, [self.vm1]) | ||||
|         # simulate attach | ||||
|         assignment = qubes.devices.DeviceAssignment(backend_domain=self.vm2, | ||||
|             ident=self.device.ident, persistent=True, frontend_domain=self.vm1) | ||||
| 
 | ||||
|         self.vm2.device.frontend_domain = self.vm1 | ||||
|         self.vm1.devices['testclass']._set.add(self.device) | ||||
|         self.vm1.devices['testclass']._set.add(assignment) | ||||
|         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' | ||||
|                 'vm1:testdev  Description\n' | ||||
|                 'vm2:testdev  Description  vm1  vm1\n' | ||||
|             ) | ||||
| 
 | ||||
|     def test_002_list_one_non_persistent(self): | ||||
|     def test_002_list_list_temp_attach(self): | ||||
|         ''' Attach the device exposed by the `vm2` to the `vm1` | ||||
|             non-persistently. | ||||
|         ''' | ||||
|         args = TestNamespace(self.app, [self.vm1]) | ||||
|         # simulate attach | ||||
|         assignment = qubes.devices.DeviceAssignment(backend_domain=self.vm2, | ||||
|             ident=self.device.ident, persistent=True, frontend_domain=self.vm1) | ||||
| 
 | ||||
|         self.vm2.device.frontend_domain = self.vm1 | ||||
|         self.vm1.devices['testclass']._set.add(assignment) | ||||
|         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' | ||||
|             ) | ||||
|             self.assertEqual(buf.getvalue(), | ||||
|                             'vm1:testdev  Description\n' | ||||
|                             'vm2:testdev  Description  vm1  vm1\n') | ||||
| 
 | ||||
|     def test_010_attach(self): | ||||
|         ''' Test attach action ''' | ||||
|         args = TestNamespace( | ||||
|             self.app, | ||||
|             [self.vm1], | ||||
| @ -118,6 +124,7 @@ class TC_00_Actions(qubes.tests.QubesTestCase): | ||||
|             'device-attach:testclass', kwargs={'device': self.device}) | ||||
| 
 | ||||
|     def test_011_double_attach(self): | ||||
|         ''' Double attach should not be possible ''' | ||||
|         args = TestNamespace( | ||||
|             self.app, | ||||
|             [self.vm1], | ||||
| @ -128,6 +135,7 @@ class TC_00_Actions(qubes.tests.QubesTestCase): | ||||
|             qubes.tools.qvm_device.attach_device(args) | ||||
| 
 | ||||
|     def test_020_detach(self): | ||||
|         ''' Test detach action ''' | ||||
|         args = TestNamespace( | ||||
|             self.app, | ||||
|             [self.vm1], | ||||
| @ -135,10 +143,12 @@ class TC_00_Actions(qubes.tests.QubesTestCase): | ||||
|         ) | ||||
|         # simulate attach | ||||
|         self.vm2.device.frontend_domain = self.vm1 | ||||
|         self.vm1.devices['testclass']._set.add(self.device) | ||||
|         args.device_assignment.frontend_domain = self.vm1 | ||||
|         self.vm1.devices['testclass']._set.add(args.device_assignment) | ||||
|         qubes.tools.qvm_device.detach_device(args) | ||||
| 
 | ||||
|     def test_021_detach_not_attached(self): | ||||
|         ''' Invalid detach action should not be possible ''' | ||||
|         args = TestNamespace( | ||||
|             self.app, | ||||
|             [self.vm1], | ||||
|  | ||||
| @ -108,7 +108,7 @@ class TC_10_BaseVM(qubes.tests.QubesTestCase): | ||||
|         }) | ||||
| 
 | ||||
|         self.assertCountEqual(vm.devices.keys(), ('pci',)) | ||||
|         self.assertCountEqual(list(vm.devices['pci'].attached(persistent=True)), | ||||
|         self.assertCountEqual(list(vm.devices['pci'].persistent()), | ||||
|             [qubes.ext.pci.PCIDevice(vm, '00:11.22')]) | ||||
| 
 | ||||
|         self.assertXMLIsValid(vm.__xml__(), 'domain.rng') | ||||
|  | ||||
| @ -257,7 +257,7 @@ class VolumeAction(QubesAction): | ||||
|             try: | ||||
|                 pool = app.pools[pool_name] | ||||
|                 volume = [v for v in pool.volumes if v.vid == vid] | ||||
|                 assert volume > 1, 'Duplicate vids in pool %s' % pool_name | ||||
|                 assert len(volume) == 1, 'Duplicate vids in pool %s' % pool_name | ||||
|                 if not volume: | ||||
|                     parser.error_runtime( | ||||
|                         'no volume with id {!r} pool: {!r}'.format(vid, | ||||
| @ -352,9 +352,8 @@ class QubesArgumentParser(argparse.ArgumentParser): | ||||
| 
 | ||||
|         self.set_defaults(verbose=1, quiet=0) | ||||
| 
 | ||||
|     def parse_args(self, *args, **kwargs): | ||||
|         # pylint: disable=arguments-differ | ||||
|         namespace = super(QubesArgumentParser, self).parse_args(*args, **kwargs) | ||||
|     def parse_args(self, args=None, namespace=None): | ||||
|         namespace = super(QubesArgumentParser, self).parse_args(args, namespace) | ||||
| 
 | ||||
|         if self._want_app and not self._want_app_no_instance: | ||||
|             self.set_qubes_verbosity(namespace) | ||||
| @ -442,8 +441,8 @@ class AliasedSubParsersAction(argparse._SubParsersAction): | ||||
|             sup = super(AliasedSubParsersAction._AliasedPseudoAction, self) | ||||
|             sup.__init__(option_strings=[], dest=dest, help=help) | ||||
| 
 | ||||
|         def __call__(self, **kwargs): | ||||
|             pass | ||||
|         def __call__(self, parser, namespace, values, option_string=None): | ||||
|             raise NotImplementedError | ||||
| 
 | ||||
|     def add_parser(self, name, **kwargs): | ||||
|         if 'aliases' in kwargs: | ||||
|  | ||||
| @ -41,8 +41,7 @@ class QubesDaemonProtocol(asyncio.Protocol): | ||||
|         print('connection_lost(exc={!r})'.format(exc)) | ||||
|         self.untrusted_buffer.close() | ||||
| 
 | ||||
|     def data_received(self, untrusted_data): | ||||
|         # pylint: disable=arguments-differ | ||||
|     def data_received(self, untrusted_data):  # pylint: disable=arguments-differ | ||||
|         print('data_received(untrusted_data={!r})'.format(untrusted_data)) | ||||
|         if self.len_untrusted_buffer + len(untrusted_data) > self.buffer_size: | ||||
|             self.app.log.warning('request too long') | ||||
|  | ||||
| @ -38,8 +38,7 @@ parser.add_argument("--template", action="store_true", dest="template", | ||||
| 
 | ||||
| 
 | ||||
| def print_msg(domains, what_single, what_plural): | ||||
|     # pylint: disable=len-as-condition | ||||
|     if len(domains) == 0: | ||||
|     if not domains: | ||||
|         print("None of given VM {!s}".format(what_single)) | ||||
|     elif len(domains) == 1: | ||||
|         print("VM {!s} {!s}".format(domains[0], what_single)) | ||||
|  | ||||
| @ -49,17 +49,30 @@ def prepare_table(dev_list): | ||||
|     output = [] | ||||
|     header = [] | ||||
|     if sys.stdout.isatty(): | ||||
|         header += [('BACKEND:DEVID', 'DESCRIPTION', 'USED BY')]  # NOQA | ||||
|         header += [('VMNAME:DEVID', 'DESCRIPTION', 'USED BY', 'ASSIGNED')]  # NOQA | ||||
| 
 | ||||
|     for dev in dev_list: | ||||
|         output += [( | ||||
|             "{!s}:{!s}".format(dev.backend_domain, dev.ident), | ||||
|             dev.id, | ||||
|             dev.description, | ||||
|             str(dev.frontend_domain) if dev.frontend_domain else "", | ||||
|             str(dev.attached_to), | ||||
|             dev.assignments | ||||
|         )] | ||||
| 
 | ||||
|     return header + sorted(output) | ||||
| 
 | ||||
| class Line(object): | ||||
| 
 | ||||
|     def __init__(self, device: qubes.devices.DeviceInfo, attached_to = None): | ||||
|         self.id = "{!s}:{!s}".format(device.backend_domain, device.ident) | ||||
|         self.description = device.description | ||||
|         self.attached_to = attached_to if attached_to else "" | ||||
|         self.frontends = [] | ||||
| 
 | ||||
|     @property | ||||
|     def assignments(self): | ||||
|         return ', '.join(self.frontends) | ||||
| 
 | ||||
| 
 | ||||
| def list_devices(args): | ||||
|     ''' Called by the parser to execute the qubes-devices list | ||||
| @ -67,32 +80,60 @@ def list_devices(args): | ||||
|     app = args.app | ||||
| 
 | ||||
|     result = [] | ||||
|     devices = set() | ||||
|     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]) | ||||
|             for dev in domain.devices[args.devclass].attached(): | ||||
|                 devices.add(dev) | ||||
|             for dev in domain.devices[args.devclass].available(): | ||||
|                 devices.add(dev) | ||||
| 
 | ||||
|     qubes.tools.print_table(prepare_table(result)) | ||||
|     else: | ||||
|         for domain in app.domains: | ||||
|             for dev in domain.devices[args.devclass].available(): | ||||
|                 devices.add(dev) | ||||
| 
 | ||||
|     result = {dev: Line(dev) for dev in devices} | ||||
| 
 | ||||
|     for dev in result: | ||||
|         for domain in app.domains: | ||||
|             if domain == dev.backend_domain: | ||||
|                 continue | ||||
|             elif dev in domain.devices[args.devclass].attached(): | ||||
|                 result[dev].attached_to = str(domain) | ||||
| 
 | ||||
|             if dev in domain.devices[args.devclass].assignments(): | ||||
|                 if dev in domain.devices[args.devclass].persistent(): | ||||
|                     result[dev].frontends.append(str(domain)) | ||||
| 
 | ||||
| 
 | ||||
|     qubes.tools.print_table(prepare_table(result.values())) | ||||
| 
 | ||||
| 
 | ||||
| def attach_device(args): | ||||
|     ''' Called by the parser to execute the :program:`qvm-devices attach` | ||||
|         subcommand. | ||||
|     ''' | ||||
|     device = args.device | ||||
|     device_assignment = args.device_assignment | ||||
|     vm = args.domains[0] | ||||
|     vm.devices[args.devclass].attach(device) | ||||
|     app = args.app | ||||
|     device_assignment.persistent = args.persistent | ||||
|     vm.devices[args.devclass].attach(device_assignment) | ||||
|     if device_assignment.persistent: | ||||
|         app.save() | ||||
| 
 | ||||
| 
 | ||||
| def detach_device(args): | ||||
|     ''' Called by the parser to execute the :program:`qvm-devices detach` | ||||
|         subcommand. | ||||
|     ''' | ||||
|     device = args.device | ||||
|     device_assignment = args.device_assignment | ||||
|     vm = args.domains[0] | ||||
|     vm.devices[args.devclass].detach(device) | ||||
|     before = len(vm.devices[args.devclass].persistent()) | ||||
|     vm.devices[args.devclass].detach(device_assignment) | ||||
|     after = len(vm.devices[args.devclass].persistent()) | ||||
|     if after < before: | ||||
|         args.app.save() | ||||
| 
 | ||||
| 
 | ||||
| def init_list_parser(sub_parsers): | ||||
| @ -110,44 +151,48 @@ def init_list_parser(sub_parsers): | ||||
| 
 | ||||
| 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 | ||||
|         :py:class:``qubes.device.DeviceInfo`` from a BACKEND:DEVICE_ID string. | ||||
|     '''  # pylint: disable=too-few-public-methods | ||||
| 
 | ||||
|     def __init__(self, help='A domain & device id combination', | ||||
|                  required=True, allow_unknown=False, **kwargs): | ||||
|     def __init__(self, help='A pool & volume id combination', | ||||
|                  required=True, **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`` ''' | ||||
|         ''' Set ``namespace.vmname`` 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 | ||||
| 
 | ||||
|         assert hasattr(namespace, 'device') | ||||
|         backend_device_id = getattr(namespace, self.dest) | ||||
| 
 | ||||
|         assert hasattr(namespace, 'devclass') | ||||
|         devclass = namespace.devclass | ||||
| 
 | ||||
|         try: | ||||
|             backend_name, devid = getattr(namespace, self.dest).split(':', 1) | ||||
|             vmname, device_id = backend_device_id.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)) | ||||
|                 vm = app.domains[vmname] | ||||
|             except KeyError: | ||||
|                 parser.error_runtime('no domain {!r}'.format(backend_name)) | ||||
|                 parser.error_runtime("no backend vm {!r}".format(vmname)) | ||||
| 
 | ||||
|             try: | ||||
|                 vm.devices[devclass][device_id] | ||||
|             except KeyError: | ||||
|                 parser.error_runtime( | ||||
|                     "backend vm {!r} doesn't expose device {!r}" | ||||
|                     .format(vmname, device_id)) | ||||
|             device_assignment = qubes.devices.DeviceAssignment(vm, device_id,) | ||||
|             setattr(namespace, 'device_assignment', device_assignment) | ||||
|         except ValueError: | ||||
|             parser.error('expected a domain & device id combination like ' | ||||
|                          'foo:bar') | ||||
|             parser.error('expected a backend vm & device id combination ' \ | ||||
|                          'like foo:bar got %s' % backend_device_id) | ||||
| 
 | ||||
| 
 | ||||
| def get_parser(device_class=None): | ||||
|     '''Create :py:class:`argparse.ArgumentParser` suitable for | ||||
| @ -169,15 +214,29 @@ def get_parser(device_class=None): | ||||
|     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) | ||||
| 
 | ||||
|     attach_parser.add_argument('VMNAME', action=qubes.tools.VmNameAction) | ||||
|     detach_parser.add_argument('VMNAME', action=qubes.tools.VmNameAction) | ||||
| 
 | ||||
|     if device_class == 'block': | ||||
|         attach_parser.add_argument(metavar='BACKEND:DEVICE_ID', dest='device', | ||||
|                                 action=qubes.tools.VolumeAction) | ||||
|         detach_parser.add_argument(metavar='BACKEND:DEVICE_ID', dest='device', | ||||
|                                     action=qubes.tools.VolumeAction) | ||||
|     else: | ||||
|         attach_parser.add_argument(metavar='BACKEND:DEVICE_ID', | ||||
|                                    dest='device', | ||||
|                                    action=DeviceAction) | ||||
|         attach_parser.add_argument('-p', '--persistent', default=False, | ||||
|                                    help='device will attached on each start of the VMNAME', | ||||
|                                    action='store_true') | ||||
|         detach_parser.add_argument(metavar='BACKEND:DEVICE_ID', | ||||
|                                    dest='device', | ||||
|                                    action=DeviceAction) | ||||
| 
 | ||||
|     attach_parser.set_defaults(func=attach_device) | ||||
|     detach_parser.set_defaults(func=detach_device) | ||||
| 
 | ||||
|     return parser | ||||
|  | ||||
| @ -309,6 +309,7 @@ class StatusColumn(Column): | ||||
|         if ret is not None: | ||||
|             if getattr(vm, 'hvm', False): | ||||
|                 return ret.upper() | ||||
| 
 | ||||
|             return ret | ||||
| 
 | ||||
| 
 | ||||
| @ -477,6 +478,7 @@ class Table(object): | ||||
|         '''Format single table row (all columns for one domain).''' | ||||
|         if self.raw_data: | ||||
|             return '|'.join(col.format(vm) for col in self.columns) | ||||
| 
 | ||||
|         return ''.join(col.cell(vm) for col in self.columns) | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -111,18 +111,21 @@ def parse_size(size): | ||||
| def mbytes_to_kmg(size): | ||||
|     if size > 1024: | ||||
|         return "%d GiB" % (size / 1024) | ||||
| 
 | ||||
|     return "%d MiB" % size | ||||
| 
 | ||||
| 
 | ||||
| def kbytes_to_kmg(size): | ||||
|     if size > 1024: | ||||
|         return mbytes_to_kmg(size / 1024) | ||||
| 
 | ||||
|     return "%d KiB" % size | ||||
| 
 | ||||
| 
 | ||||
| def bytes_to_kmg(size): | ||||
|     if size > 1024: | ||||
|         return kbytes_to_kmg(size / 1024) | ||||
| 
 | ||||
|     return "%d B" % size | ||||
| 
 | ||||
| 
 | ||||
| @ -134,6 +137,7 @@ def size_to_human(size): | ||||
|         return str(round(size / 1024.0, 1)) + ' KiB' | ||||
|     elif size < 1024 * 1024 * 1024: | ||||
|         return str(round(size / (1024.0 * 1024), 1)) + ' MiB' | ||||
| 
 | ||||
|     return str(round(size / (1024.0 * 1024 * 1024), 1)) + ' GiB' | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -217,11 +217,17 @@ class BaseVM(qubes.PropertyHolder, metaclass=BaseVMMeta): | ||||
|         for parent in self.xml.xpath('./devices'): | ||||
|             devclass = parent.get('class') | ||||
|             for node in parent.xpath('./device'): | ||||
|                 device = self.devices[devclass].devclass( | ||||
|                 options = {} | ||||
|                 if node.get('options'): | ||||
|                     options = node.get('options').attribs(), | ||||
| 
 | ||||
|                 device_assignment = qubes.devices.DeviceAssignment( | ||||
|                     self.app.domains[node.get('backend-domain')], | ||||
|                     node.get('id') | ||||
|                     node.get('id'), | ||||
|                     options, | ||||
|                     persistent=True | ||||
|                 ) | ||||
|                 self.devices[devclass].attach(device) | ||||
|                 self.devices[devclass].attach(device_assignment) | ||||
| 
 | ||||
|         # tags | ||||
|         for node in self.xml.xpath('./tags/tag'): | ||||
| @ -250,10 +256,14 @@ class BaseVM(qubes.PropertyHolder, metaclass=BaseVMMeta): | ||||
|         for devclass in self.devices: | ||||
|             devices = lxml.etree.Element('devices') | ||||
|             devices.set('class', devclass) | ||||
|             for device in self.devices[devclass].attached(persistent=True): | ||||
|             for device in self.devices[devclass].assignments(persistent=True): | ||||
|                 node = lxml.etree.Element('device') | ||||
|                 node.set('backend-domain', device.backend_domain.name) | ||||
|                 node.set('id', device.ident) | ||||
|                 options_node = lxml.etree.Element('options') | ||||
|                 for key, val in device.options: | ||||
|                     options_node.set(key, val) | ||||
|                 node.append(options_node) | ||||
|                 devices.append(node) | ||||
|             element.append(devices) | ||||
| 
 | ||||
|  | ||||
| @ -115,8 +115,7 @@ class AdminVM(qubes.vm.qubesvm.QubesVM): | ||||
|             try: | ||||
|                 return self.app.vmm.libvirt_conn.getInfo()[1] | ||||
|             except libvirt.libvirtError as e: | ||||
|                 self.log.warning( | ||||
|                     'Failed to get memory limit for dom0: {}'.format(e)) | ||||
|                 self.log.warning('Failed to get memory limit for dom0: %s', e) | ||||
|                 return 4096 | ||||
| 
 | ||||
|     def verify_files(self): | ||||
| @ -127,7 +126,8 @@ class AdminVM(qubes.vm.qubesvm.QubesVM): | ||||
|         '''  # pylint: disable=no-self-use | ||||
|         return True | ||||
| 
 | ||||
|     def start(self, **kwargs): | ||||
|     def start(self, preparing_dvm=False, start_guid=True, notify_function=None, | ||||
|             mem_required=None): | ||||
|         '''Always raises an exception. | ||||
| 
 | ||||
|         .. seealso: | ||||
|  | ||||
| @ -49,6 +49,7 @@ def _default_ip(self): | ||||
|         return None | ||||
|     if self.netvm is not None: | ||||
|         return self.netvm.get_ip_for_vm(self)  # pylint: disable=no-member | ||||
| 
 | ||||
|     return self.get_ip_for_vm(self) | ||||
| 
 | ||||
| 
 | ||||
| @ -172,6 +173,7 @@ class NetVMMixin(qubes.events.Emitter): | ||||
|                 '10.139.1.1', | ||||
|                 '10.139.1.2', | ||||
|             ) | ||||
| 
 | ||||
|         return None | ||||
| 
 | ||||
|     def __init__(self, *args, **kwargs): | ||||
|  | ||||
| @ -430,7 +430,8 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): | ||||
|     # pylint: disable=no-member | ||||
|     kernelopts = qubes.property('kernelopts', type=str, load_stage=4, | ||||
|         default=(lambda self: qubes.config.defaults['kernelopts_pcidevs'] | ||||
|             if list(self.devices['pci'].attached(persistent=True)) | ||||
|             # pylint: disable=no-member | ||||
|             if list(self.devices['pci'].persistent()) | ||||
|             else self.template.kernelopts if hasattr(self, 'template') | ||||
|             else qubes.config.defaults['kernelopts']), | ||||
|         ls_width=30, | ||||
| @ -444,6 +445,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): | ||||
|     # XXX shouldn't this go to standalone VM and TemplateVM, and leave here | ||||
|     #     only plain property? | ||||
|     default_user = qubes.property('default_user', type=str, | ||||
|         # pylint: disable=no-member | ||||
|         default=(lambda self: self.template.default_user | ||||
|             if hasattr(self, 'template') else 'user'), | ||||
|         setter=_setter_default_user, | ||||
| @ -570,7 +572,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): | ||||
| 
 | ||||
|     @property | ||||
|     def block_devices(self): | ||||
|         ''' Return all :py:class:`qubes.devices.BlockDevice`s for current domain | ||||
|         ''' Return all :py:class:`qubes.storage.BlockDevice`s for current domain | ||||
|             for serialization in the libvirt XML template as <disk>. | ||||
|         ''' | ||||
|         return [v.block_device() for v in self.volumes.values()] | ||||
| @ -1435,7 +1437,9 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): | ||||
|                 else: | ||||
|                     if not self.is_fully_usable(): | ||||
|                         return "Transient" | ||||
| 
 | ||||
|                     return "Running" | ||||
| 
 | ||||
|             return 'Halted' | ||||
|         except libvirt.libvirtError as e: | ||||
|             if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN: | ||||
| @ -1614,6 +1618,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): | ||||
|             '/vm/{}/start_time'.format(self.uuid)) | ||||
|         if start_time != '': | ||||
|             return datetime.datetime.fromtimestamp(float(start_time)) | ||||
| 
 | ||||
|         return None | ||||
| 
 | ||||
|     def is_outdated(self): | ||||
|  | ||||
| @ -212,8 +212,8 @@ the parser will complain about missing combine= attribute on the second <start>. | ||||
|                             </doc:description> | ||||
|                             <attribute name="backend-domain"> | ||||
|                                 <doc:description> | ||||
|                                     Backend domain name. | ||||
|                                 </doc:description> | ||||
|                                   Backend domain name. | ||||
|                               </doc:description> | ||||
|                                 <data type="string"> | ||||
|                                     <param name="pattern">[a-z0-9_]+</param> | ||||
|                                 </data> | ||||
| @ -224,6 +224,15 @@ the parser will complain about missing combine= attribute on the second <start>. | ||||
|                                     <param name="pattern">[0-9a-f]{2}:[0-9a-f]{2}.[0-9a-f]{2}</param> | ||||
|                                 </data> | ||||
|                             </attribute> | ||||
|                               <optional> | ||||
|                                 <element name="options"> | ||||
|                                     <doc:description> | ||||
|                                       Options | ||||
|                                   </doc:description> | ||||
|                                 <data type="string"> | ||||
|                                 </data> | ||||
|                               </element> | ||||
|                             </optional> | ||||
|                         </element> | ||||
|                     </oneOrMore> | ||||
|                 </element> | ||||
|  | ||||
| @ -32,7 +32,7 @@ | ||||
|                 <viridian/> | ||||
|             {% endif %} | ||||
| 
 | ||||
|             {% if vm.devices['pci'].attached(persistent=True) | list | ||||
|             {% if vm.devices['pci'].persistent() | list | ||||
|                     and vm.features.get('pci-e820-host', True) %} | ||||
|                 <xen> | ||||
|                     <e820_host state="on"/> | ||||
| @ -106,7 +106,7 @@ | ||||
|                 {% include 'libvirt/devices/net.xml' with context %} | ||||
|             {% endif %} | ||||
| 
 | ||||
|             {% for device in vm.devices.pci.attached(persistent=True) %} | ||||
|             {% for device in vm.devices.pci.persistent() %} | ||||
|                 {% include 'libvirt/devices/pci.xml' %} | ||||
|             {% endfor %} | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Marek Marczykowski-Górecki
						Marek Marczykowski-Górecki