Skip to content

Commit 5210963

Browse files
committed
q-dev: fix tests and cleanup
1 parent 5f76980 commit 5210963

File tree

5 files changed

+140
-66
lines changed

5 files changed

+140
-66
lines changed

qubesadmin/device_protocol.py

+104-51
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ def qbool(value):
7474

7575

7676
class DeviceSerializer:
77+
"""
78+
Group of method for serialization of device properties.
79+
"""
7780
ALLOWED_CHARS_KEY = set(
7881
string.digits + string.ascii_letters
7982
+ r"!#$%&()*+,-./:;<>?@[\]^_{|}~")
@@ -197,7 +200,7 @@ def parse_basic_device_properties(
197200
f"Unrecognized device identity '{properties['device_id']}' "
198201
f"expected '{expected_device.device_id}'"
199202
)
200-
expected._device_id = properties.get('device_id', expected_devid)
203+
properties['device_id'] = properties.get('device_id', expected_devid)
201204

202205
properties['port'] = expected
203206

@@ -225,8 +228,8 @@ def sanitize_str(
225228
"""
226229
Sanitize given untrusted string.
227230
228-
If `replace_char` is not None, ignore `error_message` and replace invalid
229-
characters with the string.
231+
If `replace_char` is not None, ignore `error_message` and replace
232+
invalid characters with the string.
230233
"""
231234
if replace_char is None:
232235
not_allowed_chars = set(untrusted_value) - allowed_chars
@@ -249,7 +252,7 @@ class Port:
249252
Attributes:
250253
backend_domain (QubesVM): The domain which exposes devices,
251254
e.g.`sys-usb`.
252-
port_id (str): A unique identifier for the port within the backend domain.
255+
port_id (str): A unique (in backend domain) identifier for the port.
253256
devclass (str): The class of the port (e.g., 'usb', 'pci').
254257
"""
255258
def __init__(self, backend_domain, port_id, devclass):
@@ -284,6 +287,7 @@ def __str__(self):
284287

285288
@property
286289
def backend_name(self) -> str:
290+
# pylint: disable=missing-function-docstring
287291
if self.backend_domain not in (None, "*"):
288292
return self.backend_domain.name
289293
return "*"
@@ -292,6 +296,9 @@ def backend_name(self) -> str:
292296
def from_qarg(
293297
cls, representation: str, devclass, domains, blind=False
294298
) -> 'Port':
299+
"""
300+
Parse qrexec argument <back_vm>+<port_id> to retrieve Port.
301+
"""
295302
if blind:
296303
get_domain = domains.get_blind
297304
else:
@@ -302,6 +309,9 @@ def from_qarg(
302309
def from_str(
303310
cls, representation: str, devclass, domains, blind=False
304311
) -> 'Port':
312+
"""
313+
Parse string <back_vm>:<port_id> to retrieve Port.
314+
"""
305315
if blind:
306316
get_domain = domains.get_blind
307317
else:
@@ -316,6 +326,9 @@ def _parse(
316326
get_domain: Callable,
317327
sep: str
318328
) -> 'Port':
329+
"""
330+
Parse string representation and return instance of Port.
331+
"""
319332
backend_name, port_id = representation.split(sep, 1)
320333
backend = get_domain(backend_name)
321334
return cls(backend_domain=backend, port_id=port_id, devclass=devclass)
@@ -364,7 +377,7 @@ def __init__(
364377
self.port: Optional[Port] = port
365378
self._device_id = device_id
366379

367-
def clone(self, **kwargs):
380+
def clone(self, **kwargs) -> 'VirtualDevice':
368381
"""
369382
Clone object and substitute attributes with explicitly given.
370383
"""
@@ -376,45 +389,64 @@ def clone(self, **kwargs):
376389
return self.__class__(**attr)
377390

378391
@property
379-
def port(self):
392+
def port(self) -> Union[Port, str]:
393+
# pylint: disable=missing-function-docstring
380394
return self._port
381395

382396
@port.setter
383-
def port(self, value):
397+
def port(self, value: Union[Port, str, None]):
398+
# pylint: disable=missing-function-docstring
384399
self._port = value if value is not None else '*'
385400

386401
@property
387-
def device_id(self):
402+
def device_id(self) -> str:
403+
# pylint: disable=missing-function-docstring
388404
if self._device_id is not None:
389405
return self._device_id
390406
return '*'
391407

392408
@property
393-
def backend_domain(self):
409+
def is_device_id_set(self) -> bool:
410+
"""
411+
Check if `device_id` is explicitly set.
412+
"""
413+
return self._device_id is not None
414+
415+
@property
416+
def backend_domain(self) -> Union[QubesVM, str]:
417+
# pylint: disable=missing-function-docstring
394418
if self.port != '*' and self.port.backend_domain is not None:
395419
return self.port.backend_domain
396420
return '*'
397421

398422
@property
399-
def backend_name(self):
423+
def backend_name(self) -> str:
424+
"""
425+
Return backend domain name if any or `*`.
426+
"""
400427
if self.port != '*':
401428
return self.port.backend_name
402429
return '*'
403430

404431
@property
405-
def port_id(self):
432+
def port_id(self) -> str:
433+
# pylint: disable=missing-function-docstring
406434
if self.port != '*' and self.port.port_id is not None:
407435
return self.port.port_id
408436
return '*'
409437

410438
@property
411-
def devclass(self):
439+
def devclass(self) -> str:
440+
# pylint: disable=missing-function-docstring
412441
if self.port != '*' and self.port.devclass is not None:
413442
return self.port.devclass
414443
return '*'
415444

416445
@property
417-
def description(self):
446+
def description(self) -> str:
447+
"""
448+
Return human-readable description of the device identity.
449+
"""
418450
if self.device_id == '*':
419451
return 'any device'
420452
return self.device_id
@@ -451,17 +483,16 @@ def __lt__(self, other):
451483
if self.port != '*' and other.port == '*':
452484
return False
453485
reprs = {self: [self.port], other: [other.port]}
454-
for obj in reprs:
486+
for obj, obj_repr in reprs.items():
455487
if obj.device_id != '*':
456-
reprs[obj].append(obj.device_id)
488+
obj_repr.append(obj.device_id)
457489
return reprs[self] < reprs[other]
458-
elif isinstance(other, Port):
490+
if isinstance(other, Port):
459491
_other = VirtualDevice(other, '*')
460492
return self < _other
461-
else:
462-
raise TypeError(
463-
f"Comparing instances of {type(self)} and '{type(other)}' "
464-
"is not supported")
493+
raise TypeError(
494+
f"Comparing instances of {type(self)} and '{type(other)}' "
495+
"is not supported")
465496

466497
def __repr__(self):
467498
return f"{self.port!r}:{self.device_id}"
@@ -478,6 +509,9 @@ def from_qarg(
478509
blind=False,
479510
backend=None,
480511
) -> 'VirtualDevice':
512+
"""
513+
Parse qrexec argument <back_vm>+<port_id>:<device_id> to get device info
514+
"""
481515
if backend is None:
482516
if blind:
483517
get_domain = domains.get_blind
@@ -492,6 +526,9 @@ def from_str(
492526
cls, representation: str, devclass: Optional[str], domains,
493527
blind=False, backend=None
494528
) -> 'VirtualDevice':
529+
"""
530+
Parse string <back_vm>+<port_id>:<device_id> to get device info
531+
"""
495532
if backend is None:
496533
if blind:
497534
get_domain = domains.get_blind
@@ -510,6 +547,9 @@ def _parse(
510547
backend,
511548
sep: str
512549
) -> 'VirtualDevice':
550+
"""
551+
Parse string representation and return instance of VirtualDevice.
552+
"""
513553
if backend is None:
514554
backend_name, identity = representation.split(sep, 1)
515555
if backend_name != '*':
@@ -721,14 +761,23 @@ def _load_classes(bus: str):
721761
return result
722762

723763
def matches(self, other: 'DeviceInterface') -> bool:
764+
"""
765+
Check if this `DeviceInterface` (pattern) matches given one.
766+
767+
The matching is done character by character using the string
768+
representation (`repr`) of both objects. A wildcard character (`'*'`)
769+
in the pattern (i.e., `self`) can match any character in the candidate
770+
(i.e., `other`).
771+
The two representations must be of the same length.
772+
"""
724773
pattern = repr(self)
725774
candidate = repr(other)
726775
if len(pattern) != len(candidate):
727776
return False
728-
for p, c in zip(pattern, candidate):
729-
if p == '*':
777+
for patt, cand in zip(pattern, candidate):
778+
if patt == '*':
730779
continue
731-
if p != c:
780+
if patt != cand:
732781
return False
733782
return True
734783

@@ -929,7 +978,8 @@ def serialize(self) -> bytes:
929978
'parent_devclass', self.parent_device.devclass)
930979

931980
for key, value in self.data.items():
932-
properties += b' ' + DeviceSerializer.pack_property("_" + key, value)
981+
properties += b' ' + DeviceSerializer.pack_property(
982+
"_" + key, value)
933983

934984
return properties
935985

@@ -952,6 +1002,7 @@ def deserialize(
9521002
device = cls._deserialize(rest, device)
9531003
# pylint: disable=broad-exception-caught
9541004
except Exception as exc:
1005+
print(str(exc), file=sys.stderr)
9551006
device = UnknownDevice.from_device(device)
9561007

9571008
return device
@@ -1026,36 +1077,28 @@ def device_id(self, value):
10261077
class UnknownDevice(DeviceInfo):
10271078
# pylint: disable=too-few-public-methods
10281079
"""Unknown device - for example, exposed by domain not running currently"""
1080+
10291081
@staticmethod
1030-
def from_device(device) -> 'UnknownDevice':
1082+
def from_device(device: VirtualDevice) -> 'UnknownDevice':
1083+
"""
1084+
Return `UnknownDevice` based on any virtual device.
1085+
"""
10311086
return UnknownDevice(device.port, device_id=device.device_id)
10321087

10331088

10341089
class AssignmentMode(Enum):
1090+
"""
1091+
Device assignment modes
1092+
"""
10351093
MANUAL = "manual"
10361094
ASK = "ask-to-attach"
10371095
AUTO = "auto-attach"
10381096
REQUIRED = "required"
10391097

10401098

10411099
class DeviceAssignment:
1042-
""" Maps a device to a frontend_domain.
1043-
1044-
There are 3 flags `attached`, `automatically_attached` and `required`.
1045-
The meaning of valid combinations is as follows:
1046-
1. (True, False, False) -> domain is running, device is manually attached
1047-
and could be manually detach any time.
1048-
2. (True, True, False) -> domain is running, device is attached
1049-
and could be manually detach any time (see 4.),
1050-
but in the future will be auto-attached again.
1051-
3. (True, True, True) -> domain is running, device is attached
1052-
and couldn't be detached.
1053-
4. (False, Ture, False) -> device is assigned to domain, but not attached
1054-
because either (i) domain is halted,
1055-
device (ii) manually detached or
1056-
(iii) attach to different domain.
1057-
5. (False, True, True) -> domain is halted, device assigned to domain
1058-
and required to start domain.
1100+
"""
1101+
Maps a device to a frontend_domain.
10591102
"""
10601103

10611104
def __init__(
@@ -1116,23 +1159,28 @@ def __lt__(self, other):
11161159
"is not supported")
11171160

11181161
@property
1119-
def backend_domain(self):
1162+
def backend_domain(self) -> QubesVM:
1163+
# pylint: disable=missing-function-docstring
11201164
return self.virtual_device.backend_domain
11211165

11221166
@property
11231167
def backend_name(self) -> str:
1168+
# pylint: disable=missing-function-docstring
11241169
return self.virtual_device.backend_name
11251170

11261171
@property
1127-
def port_id(self):
1172+
def port_id(self) -> str:
1173+
# pylint: disable=missing-function-docstring
11281174
return self.virtual_device.port_id
11291175

11301176
@property
1131-
def devclass(self):
1177+
def devclass(self) -> str:
1178+
# pylint: disable=missing-function-docstring
11321179
return self.virtual_device.devclass
11331180

11341181
@property
1135-
def device_id(self):
1182+
def device_id(self) -> str:
1183+
# pylint: disable=missing-function-docstring
11361184
return self.virtual_device.device_id
11371185

11381186
@property
@@ -1241,7 +1289,8 @@ def serialize(self) -> bytes:
12411289
'frontend_domain', self.frontend_domain.name)
12421290

12431291
for key, value in self.options.items():
1244-
properties += b' ' + DeviceSerializer.pack_property("_" + key, value)
1292+
properties += b' ' + DeviceSerializer.pack_property(
1293+
"_" + key, value)
12451294

12461295
return properties
12471296

@@ -1275,22 +1324,26 @@ def _deserialize(
12751324

12761325
DeviceSerializer.parse_basic_device_properties(
12771326
expected_device, properties)
1327+
1328+
expected_device = expected_device.clone(
1329+
device_id=properties['device_id'])
12781330
# we do not need port, we need device
12791331
del properties['port']
1280-
expected_device._device_id = properties.get(
1281-
'device_id', expected_device.device_id)
12821332
properties.pop('device_id', None)
12831333
properties['device'] = expected_device
12841334

12851335
return cls(**properties)
12861336

12871337
def matches(self, device: VirtualDevice) -> bool:
1338+
"""
1339+
Checks if the given device matches the assignment.
1340+
"""
12881341
if self.devclass != device.devclass:
12891342
return False
12901343
if self.backend_domain != device.backend_domain:
12911344
return False
1292-
if self.port_id != '*' and self.port_id != device.port_id:
1345+
if self.port_id not in ('*', device.port_id):
12931346
return False
1294-
if self.device_id != '*' and self.device_id != device.device_id:
1347+
if self.device_id not in ('*', device.device_id):
12951348
return False
12961349
return True

qubesadmin/devices.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
Devices can be of different classes (like 'pci', 'usb', etc.). Each device
2929
class is implemented by an extension.
3030
31-
Devices are identified by pair of (backend domain, `port_id`), where `port_id` is
32-
:py:class:`str`.
31+
Devices are identified by pair of (backend domain, `port_id`), where `port_id`
32+
is :py:class:`str`.
3333
"""
3434
import itertools
3535
from typing import Optional, Iterable

0 commit comments

Comments
 (0)