Skip to content

Commit 458c1c1

Browse files
committed
q-dev: port
1 parent 7b755c7 commit 458c1c1

11 files changed

+160
-171
lines changed

doc/qubes-devices.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ The microphone cannot be assigned (potentially) to any VM (attempting to attach
100100
Understanding Device Self Identity
101101
----------------------------------
102102

103-
It is important to understand that :py:class:`qubes.device_protocol.Device` does not
103+
It is important to understand that :py:class:`qubes.device_protocol.Port` does not
104104
correspond to the device itself, but rather to the *port* to which the device
105105
is connected. Therefore, when assigning a device to a VM, such as
106106
`sys-usb:1-1.1`, the port `1-1.1` is actually assigned, and thus

qubes/api/admin.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
import qubes.vm
4646
import qubes.vm.adminvm
4747
import qubes.vm.qubesvm
48-
from qubes.device_protocol import Device
48+
from qubes.device_protocol import Port
4949

5050

5151
class QubesMgmtEventsDispatcher:
@@ -1309,7 +1309,7 @@ async def vm_device_assign(self, endpoint, untrusted_payload):
13091309
dev = self.app.domains[backend_domain].devices[devclass][ident]
13101310

13111311
assignment = qubes.device_protocol.DeviceAssignment.deserialize(
1312-
untrusted_payload, expected_device=dev
1312+
untrusted_payload, expected_port=dev
13131313
)
13141314

13151315
self.fire_event_for_permission(
@@ -1364,7 +1364,7 @@ async def vm_device_attach(self, endpoint, untrusted_payload):
13641364
dev = self.app.domains[backend_domain].devices[devclass][ident]
13651365

13661366
assignment = qubes.device_protocol.DeviceAssignment.deserialize(
1367-
untrusted_payload, expected_device=dev
1367+
untrusted_payload, expected_port=dev
13681368
)
13691369

13701370
self.fire_event_for_permission(
@@ -1425,7 +1425,7 @@ async def vm_device_set_required(self, endpoint, untrusted_payload):
14251425
# qrexec already verified that no strange characters are in self.arg
14261426
backend_domain_name, ident = self.arg.split('+', 1)
14271427
backend_domain = self.app.domains[backend_domain_name]
1428-
dev = Device(backend_domain, ident, devclass)
1428+
dev = Port(backend_domain, ident, devclass)
14291429

14301430
self.fire_event_for_permission(device=dev, assignment=assignment)
14311431

qubes/device_protocol.py

+71-80
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
# Copyright (C) 2010-2016 Joanna Rutkowska <[email protected]>
66
# Copyright (C) 2015-2016 Wojtek Porczyk <[email protected]>
77
# Copyright (C) 2016 Bahtiar `kalkin-` Gadimov <[email protected]>
8-
# Copyright (C) 2017 Marek Marczykowski-Górecki
8+
# Copyright (C) 2017 Marek Marczykowski-Górecki
99
1010
# Copyright (C) 2024 Piotr Bartman-Szwarc
11-
11+
1212
#
1313
# This library is free software; you can redistribute it and/or
1414
# modify it under the terms of the GNU Lesser General Public
@@ -53,101 +53,82 @@ def qbool(value):
5353
return qubes.property.bool(None, None, value)
5454

5555

56-
class Device:
56+
class Port:
5757
"""
58-
Basic class of a *bus* device with *ident* exposed by a *backend domain*.
58+
Class of a *bus* device port with *ident* exposed by a *backend domain*.
5959
6060
Attributes:
6161
backend_domain (QubesVM): The domain which exposes devices,
6262
e.g.`sys-usb`.
63-
ident (str): A unique identifier for the device within
64-
the backend domain.
65-
devclass (str, optional): The class of the device (e.g., 'usb', 'pci').
63+
ident (str): A unique identifier for the port within the backend domain.
64+
devclass (str): The class of the port (e.g., 'usb', 'pci').
6665
"""
6766
ALLOWED_CHARS_KEY = set(
6867
string.digits + string.ascii_letters
6968
+ r"!#$%&()*+,-./:;<>?@[\]^_{|}~")
7069
ALLOWED_CHARS_PARAM = ALLOWED_CHARS_KEY.union(set(string.punctuation + ' '))
7170

72-
def __init__(self, backend_domain, ident, devclass=None):
71+
def __init__(self, backend_domain, ident, devclass):
7372
self.__backend_domain = backend_domain
7473
self.__ident = ident
75-
self.__bus = devclass
74+
self.__devclass = devclass
7675

7776
def __hash__(self):
78-
return hash((str(self.backend_domain), self.ident))
77+
return hash((self.backend_domain.name, self.ident, self.devclass))
7978

8079
def __eq__(self, other):
81-
if isinstance(other, Device):
80+
if isinstance(other, Port):
8281
return (
8382
self.backend_domain == other.backend_domain and
84-
self.ident == other.ident
83+
self.ident == other.ident and
84+
self.devclass == other.devclass
8585
)
86-
raise TypeError(f"Comparing instances of 'Device' and '{type(other)}' "
86+
raise TypeError(f"Comparing instances of 'Port' and '{type(other)}' "
8787
"is not supported")
8888

8989
def __lt__(self, other):
90-
if isinstance(other, Device):
91-
return (self.backend_domain.name, self.ident) < \
92-
(other.backend_domain.name, other.ident)
93-
raise TypeError(f"Comparing instances of 'Device' and '{type(other)}' "
90+
if isinstance(other, Port):
91+
return (self.backend_domain.name, self.devclass, self.ident) < \
92+
(other.backend_domain.name, other.devclass, other.ident)
93+
raise TypeError(f"Comparing instances of 'Port' and '{type(other)}' "
9494
"is not supported")
9595

9696
def __repr__(self):
97-
return "[%s]:%s" % (self.backend_domain, self.ident)
97+
return f"[{self.backend_domain.name}]:{self.devclass}:{self.ident}"
9898

9999
def __str__(self):
100-
return '{!s}:{!s}'.format(self.backend_domain, self.ident)
100+
return f"{self.backend_domain.name}:{self.ident}"
101101

102102
@property
103103
def ident(self) -> str:
104104
"""
105-
Immutable device identifier.
105+
Immutable port identifier.
106106
107-
Unique for given domain and device type.
107+
Unique for given domain and devclass.
108108
"""
109109
return self.__ident
110110

111111
@property
112112
def backend_domain(self) -> QubesVM:
113-
""" Which domain provides this device. (immutable)"""
113+
""" Which domain exposed this port. (immutable)"""
114114
return self.__backend_domain
115115

116116
@property
117117
def devclass(self) -> str:
118-
""" Immutable* Device class such like: 'usb', 'pci' etc.
118+
""" Immutable port class such like: 'usb', 'pci' etc.
119119
120-
For unknown devices "peripheral" is returned.
121-
122-
*see `@devclass.setter`
120+
For unknown classes "peripheral" is returned.
123121
"""
124-
if self.__bus:
125-
return self.__bus
122+
if self.__devclass:
123+
return self.__devclass
126124
return "peripheral"
127125

128-
@property
129-
def devclass_is_set(self) -> bool:
130-
"""
131-
Returns true if devclass is already initialised.
132-
"""
133-
return bool(self.__bus)
134-
135-
@devclass.setter
136-
def devclass(self, devclass: str):
137-
""" Once a value is set, it should not be overridden.
138-
139-
However, if it has not been set, i.e., the value is `None`,
140-
we can override it."""
141-
if self.__bus is not None:
142-
raise TypeError("Attribute devclass is immutable")
143-
self.__bus = devclass
144-
145126
@classmethod
146127
def unpack_properties(
147128
cls, untrusted_serialization: bytes
148129
) -> Tuple[Dict, Dict]:
149130
"""
150-
Unpacks basic device properties from a serialized encoded string.
131+
Unpacks basic port properties from a serialized encoded string.
151132
152133
Returns:
153134
tuple: A tuple containing two dictionaries, properties and options,
@@ -215,17 +196,17 @@ def pack_property(cls, key: str, value: str):
215196

216197
@staticmethod
217198
def check_device_properties(
218-
expected_device: 'Device', properties: Dict[str, Any]):
199+
expected_port: 'Port', properties: Dict[str, Any]):
219200
"""
220-
Validates properties against an expected device configuration.
201+
Validates properties against an expected port configuration.
221202
222203
Modifies `properties`.
223204
224205
Raises:
225206
UnexpectedDeviceProperty: If any property does not match
226207
the expected values.
227208
"""
228-
expected = expected_device
209+
expected = expected_port
229210
exp_vm_name = expected.backend_domain.name
230211
if properties.get('backend_domain', exp_vm_name) != exp_vm_name:
231212
raise UnexpectedDeviceProperty(
@@ -239,13 +220,11 @@ def check_device_properties(
239220
f"when expected id: {expected.ident}.")
240221
properties['ident'] = expected.ident
241222

242-
if expected.devclass_is_set:
243-
if (properties.get('devclass', expected.devclass)
244-
!= expected.devclass):
245-
raise UnexpectedDeviceProperty(
246-
f"Got {properties['devclass']} device "
247-
f"when expected {expected.devclass}.")
248-
properties['devclass'] = expected.devclass
223+
if properties.get('devclass', expected.devclass) != expected.devclass:
224+
raise UnexpectedDeviceProperty(
225+
f"Got {properties['devclass']} device "
226+
f"when expected {expected.devclass}.")
227+
properties['devclass'] = expected.devclass
249228

250229

251230
class DeviceCategory(Enum):
@@ -428,27 +407,24 @@ def _load_classes(bus: str):
428407
return result
429408

430409

431-
class DeviceInfo(Device):
410+
class DeviceInfo(Port):
432411
""" Holds all information about a device """
433412

434413
def __init__(
435414
self,
436-
backend_domain: QubesVM,
437-
ident: str,
438-
*,
439-
devclass: Optional[str] = None,
415+
port: Port,
440416
vendor: Optional[str] = None,
441417
product: Optional[str] = None,
442418
manufacturer: Optional[str] = None,
443419
name: Optional[str] = None,
444420
serial: Optional[str] = None,
445421
interfaces: Optional[List[DeviceInterface]] = None,
446-
parent: Optional[Device] = None,
422+
parent: Optional[Port] = None,
447423
attachment: Optional[QubesVM] = None,
448424
self_identity: Optional[str] = None,
449425
**kwargs
450426
):
451-
super().__init__(backend_domain, ident, devclass)
427+
super().__init__(port.backend_domain, port.ident, port.devclass)
452428

453429
self._vendor = vendor
454430
self._product = product
@@ -462,6 +438,13 @@ def __init__(
462438

463439
self.data = kwargs
464440

441+
@property
442+
def port(self) -> Port:
443+
"""
444+
Device port visible in Qubes.
445+
"""
446+
return Port(self.backend_domain, self.ident, self.devclass)
447+
465448
@property
466449
def vendor(self) -> str:
467450
"""
@@ -570,7 +553,7 @@ def interfaces(self) -> List[DeviceInterface]:
570553
return self._interfaces
571554

572555
@property
573-
def parent_device(self) -> Optional[Device]:
556+
def parent_device(self) -> Optional[Port]:
574557
"""
575558
The parent device, if any.
576559
@@ -663,28 +646,27 @@ def deserialize(
663646
def _deserialize(
664647
cls,
665648
untrusted_serialization: bytes,
666-
expected_device: Device
649+
expected_port: Port
667650
) -> 'DeviceInfo':
668651
"""
669652
Actually deserializes the object.
670653
"""
671654
properties, options = cls.unpack_properties(untrusted_serialization)
672655
properties.update(options)
673656

674-
cls.check_device_properties(expected_device, properties)
657+
cls.check_device_properties(expected_port, properties)
675658

676659
if 'attachment' not in properties or not properties['attachment']:
677660
properties['attachment'] = None
678661
else:
679-
app = expected_device.backend_domain.app
662+
app = expected_port.backend_domain.app
680663
properties['attachment'] = app.domains.get_blind(
681664
properties['attachment'])
682665

683-
if (expected_device.devclass_is_set
684-
and properties['devclass'] != expected_device.devclass):
666+
if properties['devclass'] != expected_port.devclass:
685667
raise UnexpectedDeviceProperty(
686668
f"Got {properties['devclass']} device "
687-
f"when expected {expected_device.devclass}.")
669+
f"when expected {expected_port.devclass}.")
688670

689671
if 'interfaces' in properties:
690672
interfaces = properties['interfaces']
@@ -694,15 +676,23 @@ def _deserialize(
694676
properties['interfaces'] = interfaces
695677

696678
if 'parent_ident' in properties:
697-
properties['parent'] = Device(
698-
backend_domain=expected_device.backend_domain,
679+
properties['parent'] = Port(
680+
backend_domain=expected_port.backend_domain,
699681
ident=properties['parent_ident'],
700682
devclass=properties['parent_devclass'],
701683
)
702684
del properties['parent_ident']
703685
del properties['parent_devclass']
704686

705-
return cls(**properties)
687+
port = Port(
688+
properties['backend_domain'],
689+
properties['ident'],
690+
properties['devclass'])
691+
del properties['backend_domain']
692+
del properties['ident']
693+
del properties['devclass']
694+
695+
return cls(port, **properties)
706696

707697
@property
708698
def self_identity(self) -> str:
@@ -770,10 +760,11 @@ class UnknownDevice(DeviceInfo):
770760
"""Unknown device - for example, exposed by domain not running currently"""
771761

772762
def __init__(self, backend_domain, ident, *, devclass, **kwargs):
773-
super().__init__(backend_domain, ident, devclass=devclass, **kwargs)
763+
port = Port(backend_domain, ident, devclass)
764+
super().__init__(port, **kwargs)
774765

775766

776-
class DeviceAssignment(Device):
767+
class DeviceAssignment(Port):
777768
""" Maps a device to a frontend_domain.
778769
779770
There are 3 flags `attached`, `automatically_attached` and `required`.
@@ -821,7 +812,7 @@ def clone(self, **kwargs):
821812
return self.__class__(**attr)
822813

823814
@classmethod
824-
def from_device(cls, device: Device, **kwargs) -> 'DeviceAssignment':
815+
def from_device(cls, device: Port, **kwargs) -> 'DeviceAssignment':
825816
"""
826817
Get assignment of the device.
827818
"""
@@ -923,13 +914,13 @@ def serialize(self) -> bytes:
923914
def deserialize(
924915
cls,
925916
serialization: bytes,
926-
expected_device: Device,
917+
expected_port: Port,
927918
) -> 'DeviceAssignment':
928919
"""
929920
Recovers a serialized object, see: :py:meth:`serialize`.
930921
"""
931922
try:
932-
result = cls._deserialize(serialization, expected_device)
923+
result = cls._deserialize(serialization, expected_port)
933924
except Exception as exc:
934925
raise ProtocolError() from exc
935926
return result
@@ -938,15 +929,15 @@ def deserialize(
938929
def _deserialize(
939930
cls,
940931
untrusted_serialization: bytes,
941-
expected_device: Device,
932+
expected_port: Port,
942933
) -> 'DeviceAssignment':
943934
"""
944935
Actually deserializes the object.
945936
"""
946937
properties, options = cls.unpack_properties(untrusted_serialization)
947938
properties['options'] = options
948939

949-
cls.check_device_properties(expected_device, properties)
940+
cls.check_device_properties(expected_port, properties)
950941

951942
properties['attach_automatically'] = qbool(
952943
properties.get('attach_automatically', 'no'))

0 commit comments

Comments
 (0)