Skip to content

Commit c7d244a

Browse files
committed
q-dev: update device_protocol.py
1 parent 4aac101 commit c7d244a

File tree

1 file changed

+106
-53
lines changed

1 file changed

+106
-53
lines changed

qubes/device_protocol.py

+106-53
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ def qbool(value):
5454

5555

5656
class DeviceSerializer:
57+
"""
58+
Group of method for serialization of device properties.
59+
"""
5760
ALLOWED_CHARS_KEY = set(
5861
string.digits + string.ascii_letters
5962
+ r"!#$%&()*+,-./:;<>?@[\]^_{|}~")
@@ -177,7 +180,7 @@ def parse_basic_device_properties(
177180
f"Unrecognized device identity '{properties['device_id']}' "
178181
f"expected '{expected_device.device_id}'"
179182
)
180-
expected._device_id = properties.get('device_id', expected_devid)
183+
properties['device_id'] = properties.get('device_id', expected_devid)
181184

182185
properties['port'] = expected
183186

@@ -205,8 +208,8 @@ def sanitize_str(
205208
"""
206209
Sanitize given untrusted string.
207210
208-
If `replace_char` is not None, ignore `error_message` and replace invalid
209-
characters with the string.
211+
If `replace_char` is not None, ignore `error_message` and replace
212+
invalid characters with the string.
210213
"""
211214
if replace_char is None:
212215
not_allowed_chars = set(untrusted_value) - allowed_chars
@@ -229,7 +232,7 @@ class Port:
229232
Attributes:
230233
backend_domain (QubesVM): The domain which exposes devices,
231234
e.g.`sys-usb`.
232-
port_id (str): A unique identifier for the port within the backend domain.
235+
port_id (str): A unique (in backend domain) identifier for the port.
233236
devclass (str): The class of the port (e.g., 'usb', 'pci').
234237
"""
235238
def __init__(self, backend_domain, port_id, devclass):
@@ -264,6 +267,7 @@ def __str__(self):
264267

265268
@property
266269
def backend_name(self) -> str:
270+
# pylint: disable=missing-function-docstring
267271
if self.backend_domain not in (None, "*"):
268272
return self.backend_domain.name
269273
return "*"
@@ -272,6 +276,9 @@ def backend_name(self) -> str:
272276
def from_qarg(
273277
cls, representation: str, devclass, domains, blind=False
274278
) -> 'Port':
279+
"""
280+
Parse qrexec argument <back_vm>+<port_id> to retrieve Port.
281+
"""
275282
if blind:
276283
get_domain = domains.get_blind
277284
else:
@@ -282,6 +289,9 @@ def from_qarg(
282289
def from_str(
283290
cls, representation: str, devclass, domains, blind=False
284291
) -> 'Port':
292+
"""
293+
Parse string <back_vm>:<port_id> to retrieve Port.
294+
"""
285295
if blind:
286296
get_domain = domains.get_blind
287297
else:
@@ -296,6 +306,9 @@ def _parse(
296306
get_domain: Callable,
297307
sep: str
298308
) -> 'Port':
309+
"""
310+
Parse string representation and return instance of Port.
311+
"""
299312
backend_name, port_id = representation.split(sep, 1)
300313
backend = get_domain(backend_name)
301314
return cls(backend_domain=backend, port_id=port_id, devclass=devclass)
@@ -344,7 +357,7 @@ def __init__(
344357
self.port: Optional[Port] = port
345358
self._device_id = device_id
346359

347-
def clone(self, **kwargs):
360+
def clone(self, **kwargs) -> 'VirtualDevice':
348361
"""
349362
Clone object and substitute attributes with explicitly given.
350363
"""
@@ -353,48 +366,67 @@ def clone(self, **kwargs):
353366
"device_id": self.device_id,
354367
}
355368
attr.update(kwargs)
356-
return self.__class__(**attr)
369+
return VirtualDevice(**attr)
357370

358371
@property
359-
def port(self):
372+
def port(self) -> Union[Port, str]:
373+
# pylint: disable=missing-function-docstring
360374
return self._port
361375

362376
@port.setter
363-
def port(self, value):
377+
def port(self, value: Union[Port, str, None]):
378+
# pylint: disable=missing-function-docstring
364379
self._port = value if value is not None else '*'
365380

366381
@property
367-
def device_id(self):
382+
def device_id(self) -> str:
383+
# pylint: disable=missing-function-docstring
368384
if self._device_id is not None:
369385
return self._device_id
370386
return '*'
371387

372388
@property
373-
def backend_domain(self):
389+
def is_device_id_set(self) -> bool:
390+
"""
391+
Check if `device_id` is explicitly set.
392+
"""
393+
return self._device_id is not None
394+
395+
@property
396+
def backend_domain(self) -> Union[QubesVM, str]:
397+
# pylint: disable=missing-function-docstring
374398
if self.port != '*' and self.port.backend_domain is not None:
375399
return self.port.backend_domain
376400
return '*'
377401

378402
@property
379-
def backend_name(self):
403+
def backend_name(self) -> str:
404+
"""
405+
Return backend domain name if any or `*`.
406+
"""
380407
if self.port != '*':
381408
return self.port.backend_name
382409
return '*'
383410

384411
@property
385-
def port_id(self):
412+
def port_id(self) -> str:
413+
# pylint: disable=missing-function-docstring
386414
if self.port != '*' and self.port.port_id is not None:
387415
return self.port.port_id
388416
return '*'
389417

390418
@property
391-
def devclass(self):
419+
def devclass(self) -> str:
420+
# pylint: disable=missing-function-docstring
392421
if self.port != '*' and self.port.devclass is not None:
393422
return self.port.devclass
394423
return '*'
395424

396425
@property
397-
def description(self):
426+
def description(self) -> str:
427+
"""
428+
Return human-readable description of the device identity.
429+
"""
398430
if self.device_id == '*':
399431
return 'any device'
400432
return self.device_id
@@ -431,17 +463,16 @@ def __lt__(self, other):
431463
if self.port != '*' and other.port == '*':
432464
return False
433465
reprs = {self: [self.port], other: [other.port]}
434-
for obj in reprs:
466+
for obj, obj_repr in reprs.items():
435467
if obj.device_id != '*':
436-
reprs[obj].append(obj.device_id)
468+
obj_repr.append(obj.device_id)
437469
return reprs[self] < reprs[other]
438-
elif isinstance(other, Port):
470+
if isinstance(other, Port):
439471
_other = VirtualDevice(other, '*')
440472
return self < _other
441-
else:
442-
raise TypeError(
443-
f"Comparing instances of {type(self)} and '{type(other)}' "
444-
"is not supported")
473+
raise TypeError(
474+
f"Comparing instances of {type(self)} and '{type(other)}' "
475+
"is not supported")
445476

446477
def __repr__(self):
447478
return f"{self.port!r}:{self.device_id}"
@@ -458,6 +489,9 @@ def from_qarg(
458489
blind=False,
459490
backend=None,
460491
) -> 'VirtualDevice':
492+
"""
493+
Parse qrexec argument <back_vm>+<port_id>:<device_id> to get device info
494+
"""
461495
if backend is None:
462496
if blind:
463497
get_domain = domains.get_blind
@@ -472,6 +506,9 @@ def from_str(
472506
cls, representation: str, devclass: Optional[str], domains,
473507
blind=False, backend=None
474508
) -> 'VirtualDevice':
509+
"""
510+
Parse string <back_vm>+<port_id>:<device_id> to get device info
511+
"""
475512
if backend is None:
476513
if blind:
477514
get_domain = domains.get_blind
@@ -490,6 +527,9 @@ def _parse(
490527
backend,
491528
sep: str
492529
) -> 'VirtualDevice':
530+
"""
531+
Parse string representation and return instance of VirtualDevice.
532+
"""
493533
if backend is None:
494534
backend_name, identity = representation.split(sep, 1)
495535
if backend_name != '*':
@@ -701,14 +741,23 @@ def _load_classes(bus: str):
701741
return result
702742

703743
def matches(self, other: 'DeviceInterface') -> bool:
744+
"""
745+
Check if this `DeviceInterface` (pattern) matches given one.
746+
747+
The matching is done character by character using the string
748+
representation (`repr`) of both objects. A wildcard character (`'*'`)
749+
in the pattern (i.e., `self`) can match any character in the candidate
750+
(i.e., `other`).
751+
The two representations must be of the same length.
752+
"""
704753
pattern = repr(self)
705754
candidate = repr(other)
706755
if len(pattern) != len(candidate):
707756
return False
708-
for p, c in zip(pattern, candidate):
709-
if p == '*':
757+
for patt, cand in zip(pattern, candidate):
758+
if patt == '*':
710759
continue
711-
if p != c:
760+
if patt != cand:
712761
return False
713762
return True
714763

@@ -909,7 +958,8 @@ def serialize(self) -> bytes:
909958
'parent_devclass', self.parent_device.devclass)
910959

911960
for key, value in self.data.items():
912-
properties += b' ' + DeviceSerializer.pack_property("_" + key, value)
961+
properties += b' ' + DeviceSerializer.pack_property(
962+
"_" + key, value)
913963

914964
return properties
915965

@@ -932,6 +982,7 @@ def deserialize(
932982
device = cls._deserialize(rest, device)
933983
# pylint: disable=broad-exception-caught
934984
except Exception as exc:
985+
print(str(exc), file=sys.stderr)
935986
device = UnknownDevice.from_device(device)
936987

937988
return device
@@ -1006,36 +1057,28 @@ def device_id(self, value):
10061057
class UnknownDevice(DeviceInfo):
10071058
# pylint: disable=too-few-public-methods
10081059
"""Unknown device - for example, exposed by domain not running currently"""
1060+
10091061
@staticmethod
1010-
def from_device(device) -> 'UnknownDevice':
1062+
def from_device(device: VirtualDevice) -> 'UnknownDevice':
1063+
"""
1064+
Return `UnknownDevice` based on any virtual device.
1065+
"""
10111066
return UnknownDevice(device.port, device_id=device.device_id)
10121067

10131068

10141069
class AssignmentMode(Enum):
1070+
"""
1071+
Device assignment modes
1072+
"""
10151073
MANUAL = "manual"
10161074
ASK = "ask-to-attach"
10171075
AUTO = "auto-attach"
10181076
REQUIRED = "required"
10191077

10201078

10211079
class DeviceAssignment:
1022-
""" Maps a device to a frontend_domain.
1023-
1024-
There are 3 flags `attached`, `automatically_attached` and `required`.
1025-
The meaning of valid combinations is as follows:
1026-
1. (True, False, False) -> domain is running, device is manually attached
1027-
and could be manually detach any time.
1028-
2. (True, True, False) -> domain is running, device is attached
1029-
and could be manually detach any time (see 4.),
1030-
but in the future will be auto-attached again.
1031-
3. (True, True, True) -> domain is running, device is attached
1032-
and couldn't be detached.
1033-
4. (False, Ture, False) -> device is assigned to domain, but not attached
1034-
because either (i) domain is halted,
1035-
device (ii) manually detached or
1036-
(iii) attach to different domain.
1037-
5. (False, True, True) -> domain is halted, device assigned to domain
1038-
and required to start domain.
1080+
"""
1081+
Maps a device to a frontend_domain.
10391082
"""
10401083

10411084
def __init__(
@@ -1096,28 +1139,33 @@ def __lt__(self, other):
10961139
"is not supported")
10971140

10981141
@property
1099-
def backend_domain(self):
1142+
def backend_domain(self) -> QubesVM:
1143+
# pylint: disable=missing-function-docstring
11001144
return self.virtual_device.backend_domain
11011145

11021146
@property
11031147
def backend_name(self) -> str:
1148+
# pylint: disable=missing-function-docstring
11041149
return self.virtual_device.backend_name
11051150

11061151
@property
1107-
def port_id(self):
1152+
def port_id(self) -> str:
1153+
# pylint: disable=missing-function-docstring
11081154
return self.virtual_device.port_id
11091155

11101156
@property
1111-
def devclass(self):
1157+
def devclass(self) -> str:
1158+
# pylint: disable=missing-function-docstring
11121159
return self.virtual_device.devclass
11131160

11141161
@property
1115-
def device_id(self):
1162+
def device_id(self) -> str:
1163+
# pylint: disable=missing-function-docstring
11161164
return self.virtual_device.device_id
11171165

11181166
@property
11191167
def devices(self) -> List[DeviceInfo]:
1120-
"""Get DeviceInfo object corresponding to this DeviceAssignment"""
1168+
"""Get DeviceInfo objects corresponding to this DeviceAssignment"""
11211169
if self.port_id != '*':
11221170
# could return UnknownDevice
11231171
return [self.backend_domain.devices[self.devclass][self.port_id]]
@@ -1221,7 +1269,8 @@ def serialize(self) -> bytes:
12211269
'frontend_domain', self.frontend_domain.name)
12221270

12231271
for key, value in self.options.items():
1224-
properties += b' ' + DeviceSerializer.pack_property("_" + key, value)
1272+
properties += b' ' + DeviceSerializer.pack_property(
1273+
"_" + key, value)
12251274

12261275
return properties
12271276

@@ -1255,22 +1304,26 @@ def _deserialize(
12551304

12561305
DeviceSerializer.parse_basic_device_properties(
12571306
expected_device, properties)
1307+
1308+
expected_device = expected_device.clone(
1309+
device_id=properties['device_id'])
12581310
# we do not need port, we need device
12591311
del properties['port']
1260-
expected_device._device_id = properties.get(
1261-
'device_id', expected_device.device_id)
12621312
properties.pop('device_id', None)
12631313
properties['device'] = expected_device
12641314

12651315
return cls(**properties)
12661316

12671317
def matches(self, device: VirtualDevice) -> bool:
1318+
"""
1319+
Checks if the given device matches the assignment.
1320+
"""
12681321
if self.devclass != device.devclass:
12691322
return False
12701323
if self.backend_domain != device.backend_domain:
12711324
return False
1272-
if self.port_id != '*' and self.port_id != device.port_id:
1325+
if self.port_id not in ('*', device.port_id):
12731326
return False
1274-
if self.device_id != '*' and self.device_id != device.device_id:
1327+
if self.device_id not in ('*', device.device_id):
12751328
return False
12761329
return True

0 commit comments

Comments
 (0)