Skip to content

Commit

Permalink
q-dev: implementation of DeviceInfo.attachment
Browse files Browse the repository at this point in the history
  • Loading branch information
piotrbartman committed Jun 12, 2024
1 parent 33bb3f2 commit 4a426e6
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 40 deletions.
10 changes: 3 additions & 7 deletions qubes/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1300,7 +1300,6 @@ async def vm_device_assign(self, endpoint, untrusted_payload):
ep.name
for ep in importlib.metadata.entry_points(group='qubes.devices')), no_payload=True, scope='local', write=True)
async def vm_device_unassign(self, endpoint):
# TODO DeviceAssignment.deserialize() ? Device
devclass = endpoint

# qrexec already verified that no strange characters are in self.arg
Expand Down Expand Up @@ -1359,7 +1358,6 @@ async def vm_device_attach(self, endpoint, untrusted_payload):
for ep in importlib.metadata.entry_points(group='qubes.devices')),
no_payload=True, scope='local', execute=True)
async def vm_device_detach(self, endpoint):
# TODO DeviceAssignment.deserialize() ? Device
devclass = endpoint

# qrexec already verified that no strange characters are in self.arg
Expand All @@ -1374,15 +1372,13 @@ async def vm_device_detach(self, endpoint):
assignment = qubes.devices.DeviceAssignment(
dev.backend_domain, dev.ident)
await self.dest.devices[devclass].detach(assignment)
self.app.save() # TODO: not needed

# Attach/Detach action can both modify persistent state (with
# required=True or required=False) and volatile state of running VM
# (with required=None). For this reason, write=True + execute=True
# Assign/Unassign action can modify only persistent state of running VM.
# For this reason, write=True
@qubes.api.method('admin.vm.device.{endpoint}.Set.assignment',
endpoints=(ep.name
for ep in importlib.metadata.entry_points(group='qubes.devices')),
scope='local', write=True, execute=True)
scope='local', write=True)
async def vm_device_set_assignment(self, endpoint, untrusted_payload):
"""
Update assignment of already attached device.
Expand Down
87 changes: 56 additions & 31 deletions qubes/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,6 @@ def _load_classes(bus: str):
class DeviceInfo(Device):
""" Holds all information about a device """

# pylint: disable=too-few-public-methods
def __init__(
self,
backend_domain: 'qubes.vm.BaseVM',
Expand All @@ -331,6 +330,7 @@ def __init__(
serial: Optional[str] = None,
interfaces: Optional[List[DeviceInterface]] = None,
parent: Optional[Device] = None,
attachment: Optional['qubes.vm.BaseVM'] = None,
**kwargs
):
super().__init__(backend_domain, ident, devclass)
Expand All @@ -342,6 +342,7 @@ def __init__(
self._serial = serial
self._interfaces = interfaces
self._parent = parent
self._attachment = attachment

self.data = kwargs

Expand Down Expand Up @@ -473,17 +474,17 @@ def subdevices(self) -> List['DeviceInfo']:
if dev.parent_device.ident == self.ident]

@property
def attachments(self) -> List['DeviceAssignment']:
def attachment(self) -> Optional['qubes.vm.BaseVM']:
"""
Device attachments
VM to which device is attached (frontend domain).
"""
return [] # TODO
return self._attachment

def serialize(self) -> bytes:
"""
Serialize object to be transmitted via Qubes API.
"""
# 'backend_domain', 'interfaces', 'data', 'parent_device'
# 'backend_domain', 'attachment', 'interfaces', 'data', 'parent_device'
# are not string, so they need special treatment
default_attrs = {
'ident', 'devclass', 'vendor', 'product', 'manufacturer', 'name',
Expand All @@ -494,9 +495,14 @@ def serialize(self) -> bytes:
(key, getattr(self, key)) for key in default_attrs)
)

back_name = serialize_str(self.backend_domain.name)
backend_domain_prop = (b"backend_domain=" + back_name.encode('ascii'))
properties += b' ' + backend_domain_prop
qname = serialize_str(self.backend_domain.name)
backend_prop = (b"backend_domain=" + qname.encode('ascii'))
properties += b' ' + backend_prop

if self.attachment:
qname = serialize_str(self.attachment.name)
attachment_prop = (b"attachment=" + qname.encode('ascii'))
properties += b' ' + attachment_prop

interfaces = serialize_str(
''.join(repr(ifc) for ifc in self.interfaces))
Expand Down Expand Up @@ -573,6 +579,13 @@ def _deserialize(
f"when expected devices from {expected_backend_domain.name}.")
properties['backend_domain'] = expected_backend_domain

if 'attachment' not in properties or not properties['attachment']:
properties['attachment'] = None
else:
app = expected_backend_domain.app
properties['attachment'] = app.domains.get_blind(
properties['attachment'])

if expected_devclass and properties['devclass'] != expected_devclass:
raise UnexpectedDeviceProperty(
f"Got {properties['devclass']} device "
Expand All @@ -597,10 +610,6 @@ def _deserialize(

return cls(**properties)

@property
def frontend_domain(self):
return self.data.get("frontend_domain", None)

@property
def full_identity(self) -> str:
"""
Expand Down Expand Up @@ -645,7 +654,6 @@ def sanitize_str(
"""
if replace_char is None:
if any(x not in allowed_chars for x in untrusted_value):
print(untrusted_value, file=sys.stderr) # TODO
raise qubes.api.ProtocolError(error_message)
return untrusted_value
result = ""
Expand Down Expand Up @@ -678,8 +686,9 @@ class DeviceAssignment(Device):
3. (True, True, True) -> domain is running, device is attached
and couldn't be detached.
4. (False, Ture, False) -> device is assigned to domain, but not attached
because either domain is halted
or device manually detached.
because either (i) domain is halted,
device (ii) manually detached or
(iii) attach to different domain.
5. (False, True, True) -> domain is halted, device assigned to domain
and required to start domain.
"""
Expand Down Expand Up @@ -725,10 +734,11 @@ def frontend_domain(
@property
def attached(self) -> bool:
"""
Is the device already attached to the fronted domain?
Is the device attached to the fronted domain?
Returns False if device is attached to different domain
"""
return (self.frontend_domain is not None
and self.frontend_domain.is_running())
return self.device.attachment == self.frontend_domain

@property
def required(self) -> bool:
Expand All @@ -738,6 +748,10 @@ def required(self) -> bool:
"""
return self.__required

@required.setter
def required(self, required: bool):
self.__required = required

@property
def attach_automatically(self) -> bool:
"""
Expand All @@ -746,6 +760,10 @@ def attach_automatically(self) -> bool:
"""
return self.__attach_automatically

@attach_automatically.setter
def attach_automatically(self, attach_automatically: bool):
self.__attach_automatically = attach_automatically

@property
def options(self) -> Dict[str, Any]:
""" Device options (same as in the legacy API). """
Expand Down Expand Up @@ -773,10 +791,7 @@ def serialize(self) -> bytes:
properties += b' ' + backend_domain_prop

if self.frontend_domain is not None:
if isinstance(self.frontend_domain, str):
front_name = serialize_str(self.frontend_domain) # TODO
else:
front_name = serialize_str(self.frontend_domain.name)
front_name = serialize_str(self.frontend_domain.name)
frontend_domain_prop = (
b"frontend_domain=" + front_name.encode('ascii'))
properties += b' ' + frontend_domain_prop
Expand Down Expand Up @@ -925,6 +940,23 @@ class DeviceCollection:
:param device: :py:class:`DeviceInfo` object to be attached
.. event:: device-assign:<class> (device, options)
Fired when device is assigned to a VM.
Handler for this event may be asynchronous.
:param device: :py:class:`DeviceInfo` object to be assigned
:param options: :py:class:`dict` of assignment options
.. event:: device-unassign:<class> (device)
Fired when device is unassigned from a VM.
Handler for this event can be asynchronous (a coroutine).
:param device: :py:class:`DeviceInfo` object to be unassigned
.. event:: device-list:<class>
Fired to get list of devices exposed by a VM. Handlers of this
Expand Down Expand Up @@ -1008,10 +1040,6 @@ async def assign(self, assignment: DeviceAssignment):
"Only pci and usb devices can be assigned "
"to be automatically attached.")

await self._vm.fire_event_async(
'device-pre-assign:' + self._bus,
pre_event=True, device=device, options=assignment.options)

self._set.add(assignment)

await self._vm.fire_event_async(
Expand Down Expand Up @@ -1112,12 +1140,9 @@ async def unassign(self, device_assignment: DeviceAssignment):
"Can not remove an required assignment from "
"a non halted qube.")

device = device_assignment.device
await self._vm.fire_event_async(
'device-pre-unassign:' + self._bus, pre_event=True, device=device)

self._set.discard(device_assignment)

device = device_assignment.device
await self._vm.fire_event_async(
'device-unassign:' + self._bus, device=device)

Expand Down Expand Up @@ -1145,7 +1170,7 @@ def get_attached_devices(self) -> Iterable[DeviceAssignment]:
backend_domain=dev.backend_domain,
ident=dev.ident,
options=options,
frontend_domain=dev.frontend_domain,
frontend_domain=self._vm,
devclass=dev.devclass,
attach_automatically=False,
required=False,
Expand Down
4 changes: 2 additions & 2 deletions qubes/ext/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def on_domain_init_load(self, vm, event):
# avoid building a cache on domain-init, as it isn't fully set yet,
# and definitely isn't running yet
current_devices = {
dev.ident: dev.frontend_domain
dev.ident: dev.attachment
for dev in self.on_device_list_block(vm, None)
}
self.devices_cache[vm.name] = current_devices
Expand All @@ -181,7 +181,7 @@ def on_qdb_change(self, vm, event, path):
"""A change in QubesDB means a change in device list."""
# pylint: disable=unused-argument
vm.fire_event('device-list-change:block')
current_devices = dict((dev.ident, dev.frontend_domain)
current_devices = dict((dev.ident, dev.attachment)
for dev in self.on_device_list_block(vm, None))

# send events about devices detached/attached outside by themselves
Expand Down

0 comments on commit 4a426e6

Please sign in to comment.