Skip to content

Commit ff0d883

Browse files
committed
q-dev: full identity
better error message update docs
1 parent 160f50e commit ff0d883

File tree

3 files changed

+68
-29
lines changed

3 files changed

+68
-29
lines changed

doc/manpages/qvm-device.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ Synopsis
99
| :command:`qvm-device` *DEVICE_CLASS* {list,ls,l} [*options*] <*vm-name*>
1010
| :command:`qvm-device` *DEVICE_CLASS* {attach,at,a} [*options*] <*vm-name*> <*device*>
1111
| :command:`qvm-device` *DEVICE_CLASS* {detach,dt,d} [*options*] <*vm-name*> [<*device*>]
12+
| :command:`qvm-device` *DEVICE_CLASS* {assign,s} [*options*] <*vm-name*> <*device*>
13+
| :command:`qvm-device` *DEVICE_CLASS* {unassign,u} [*options*] <*vm-name*> [<*device*>]
1214
| :command:`qvm-device` *DEVICE_CLASS* {info,i} [*options*] <*vm-name*> [<*device*>]
13-
| :command:`qvm-*DEVICE_CLASS*` {list,ls,l,attach,at,a,detach,dt,d,info,i} [*options*] <*vmname*> ...
15+
| :command:`qvm-*DEVICE_CLASS*` {list,ls,l,attach,at,a,detach,dt,d,assign,s,unassign,u,info,i} [*options*] <*vmname*> ...
1416
1517
.. note:: :command:`qvm-block`, :command:`qvm-usb` and :command:`qvm-pci` are just aliases for :command:`qvm-device block`, :command:`qvm-device usb` and :command:`qvm-device pci` respectively.
1618

qubesadmin/devices.py

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def __lt__(self, other):
105105
if isinstance(other, Device):
106106
return (self.backend_domain, self.ident) < \
107107
(other.backend_domain, other.ident)
108-
return NotImplemented
108+
return NotImplemented()
109109

110110
def __repr__(self):
111111
return "[%s]:%s" % (self.backend_domain, self.ident)
@@ -160,9 +160,10 @@ def devclass(self, devclass: str):
160160

161161
class DeviceCategory(Enum):
162162
"""
163+
Category of peripheral device.
164+
163165
Arbitrarily selected interfaces that are important to users,
164166
thus deserving special recognition such as a custom icon, etc.
165-
166167
"""
167168
Other = "*******"
168169

@@ -212,12 +213,16 @@ def from_str(interface_encoding: str) -> 'DeviceCategory':
212213

213214

214215
class DeviceInterface:
216+
"""
217+
Peripheral device interface wrapper.
218+
"""
219+
215220
def __init__(self, interface_encoding: str, devclass: Optional[str] = None):
216221
ifc_padded = interface_encoding.ljust(6, '*')
217222
if devclass:
218223
if len(ifc_padded) > 6:
219224
print(
220-
f"interface_encoding is too long "
225+
f"{interface_encoding=} is too long "
221226
f"(is {len(interface_encoding)}, expected max. 6) "
222227
f"for given {devclass=}",
223228
file=sys.stderr
@@ -229,7 +234,7 @@ def __init__(self, interface_encoding: str, devclass: Optional[str] = None):
229234
devclass = known_devclasses.get(interface_encoding[0], None)
230235
if len(ifc_padded) > 7:
231236
print(
232-
f"interface_encoding is too long "
237+
f"{interface_encoding=} is too long "
233238
f"(is {len(interface_encoding)}, expected max. 7)",
234239
file=sys.stderr
235240
)
@@ -258,18 +263,26 @@ def unknown(cls) -> 'DeviceInterface':
258263
""" Value for unknown device interface. """
259264
return cls("?******")
260265

261-
@property
262266
def __repr__(self):
263267
return self._interface_encoding
264268

265-
@property
266269
def __str__(self):
267270
if self.devclass == "block":
268271
return "Block device"
269272
if self.devclass in ("usb", "pci"):
270-
self._load_classes(self.devclass).get(
271-
self._interface_encoding[1:],
272-
f"Unclassified {self.devclass} device")
273+
result = self._load_classes(self.devclass).get(
274+
self._interface_encoding[1:], None)
275+
if result is None:
276+
result = self._load_classes(self.devclass).get(
277+
self._interface_encoding[1:-2] + '**', None)
278+
if result is None:
279+
result = self._load_classes(self.devclass).get(
280+
self._interface_encoding[1:-4] + '****', None)
281+
if result is None:
282+
result = f"Unclassified {self.devclass} device"
283+
return result
284+
if self.devclass == 'mic':
285+
return "Microphone"
273286
return repr(self)
274287

275288
@staticmethod
@@ -591,6 +604,27 @@ def _deserialize(
591604
def frontend_domain(self):
592605
return self.data.get("frontend_domain", None)
593606

607+
@property
608+
def full_identity(self) -> str:
609+
"""
610+
Get user understandable identification of device not related to ports.
611+
612+
In addition to the description returns presented interfaces.
613+
It is used to auto-attach usb devices, so an attacking device needs to
614+
mimic not only a name, but also interfaces of trusted device (and have
615+
to be plugged to the same port). For a common user it is all the data
616+
she uses to recognize the device.
617+
"""
618+
allowed_chars = string.digits + string.ascii_letters + '-_.'
619+
description = ""
620+
for char in self.description:
621+
if char in allowed_chars:
622+
description += char
623+
else:
624+
description += "_"
625+
interfaces = ''.join(repr(ifc) for ifc in self.interfaces)
626+
return f'{description}:{interfaces}'
627+
594628

595629
def serialize_str(value: str):
596630
return repr(str(value))
@@ -771,7 +805,8 @@ def _deserialize(
771805
allowed_chars_key = string.digits + string.ascii_letters + '-_.'
772806
allowed_chars_value = allowed_chars_key + ',+:'
773807

774-
untrusted_decoded = untrusted_serialization.decode('ascii', 'strict')
808+
untrusted_decoded = untrusted_serialization.decode(
809+
'ascii', 'strict').strip()
775810
keys = []
776811
values = []
777812
untrusted_key, _, untrusted_rest = untrusted_decoded.partition("='")
@@ -808,7 +843,7 @@ def _deserialize(
808843

809844
if properties['backend_domain'] != expected_backend_domain.name:
810845
raise UnexpectedDeviceProperty(
811-
f"Got device exposed by {properties['backend_domain']}"
846+
f"Got device exposed by {properties['backend_domain']} "
812847
f"when expected devices from {expected_backend_domain.name}.")
813848
properties['backend_domain'] = expected_backend_domain
814849

@@ -895,31 +930,31 @@ def detach(self, device_assignment: DeviceAssignment) -> None:
895930
device_assignment.backend_domain,
896931
device_assignment.ident))
897932

898-
def assign(self, device_assignment: DeviceAssignment) -> None:
933+
def assign(self, assignment: DeviceAssignment) -> None:
899934
"""
900935
Assign device to domain (add to :file:`qubes.xml`).
901936
902-
:param DeviceAssignment device_assignment: device object
937+
:param DeviceAssignment assignment: device object
903938
"""
904939

905-
if not device_assignment.frontend_domain:
906-
device_assignment.frontend_domain = self._vm
940+
if not assignment.frontend_domain:
941+
assignment.frontend_domain = self._vm
907942
else:
908-
assert device_assignment.frontend_domain == self._vm, \
943+
assert assignment.frontend_domain == self._vm, \
909944
"Trying to assign DeviceAssignment belonging to other domain"
910-
if not device_assignment.devclass_is_set:
911-
device_assignment.devclass = self._class
912-
elif device_assignment.devclass != self._class:
913-
raise ValueError(
945+
if not assignment.devclass_is_set:
946+
assignment.devclass = self._class
947+
elif assignment.devclass != self._class:
948+
raise qubesadmin.ext.QubesValueError(
914949
f"Device assignment class does not match to expected: "
915-
f"{device_assignment.devclass=}!={self._class=}")
950+
f"{assignment.devclass=}!={self._class=}")
916951

917952
self._vm.qubesd_call(None,
918953
'admin.vm.device.{}.Assign'.format(self._class),
919954
'{!s}+{!s}'.format(
920-
device_assignment.backend_domain,
921-
device_assignment.ident),
922-
device_assignment.serialize())
955+
assignment.backend_domain,
956+
assignment.ident),
957+
assignment.serialize())
923958

924959
def unassign(self, device_assignment: DeviceAssignment) -> None:
925960
"""

qubesadmin/tools/qvm_device.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ def assign_device(args):
174174
options = dict(opt.split('=', 1) for opt in args.option or [])
175175
if args.ro:
176176
options['read-only'] = 'yes'
177+
if device.devclass == 'usb':
178+
options['identity'] = device.full_identity
177179
device_assignment.attach_automatically = True
178180
device_assignment.required = args.required
179181
device_assignment.options = options
@@ -266,10 +268,10 @@ def parse_qubes_app(self, parser, namespace):
266268
raise KeyError(device_id)
267269
except KeyError:
268270
parser.error_runtime(
269-
"backend vm {!r} doesn't expose device {!r}".format(
270-
vmname, device_id))
271-
device = qubesadmin.devices.Device(vm, device_id)
272-
setattr(namespace, self.dest, device)
271+
f"backend vm {vmname!r} doesn't expose "
272+
f"{devclass} device {device_id!r}")
273+
dev = qubesadmin.devices.Device(vm, device_id, devclass)
274+
setattr(namespace, self.dest, dev)
273275
except ValueError:
274276
parser.error(
275277
'expected a backend vm & device id combination like foo:bar '

0 commit comments

Comments
 (0)