Skip to content

Commit a80383d

Browse files
committed
Merge branch 'device-cache'
* device-cache: Enable cache for qvm-device Cache device assignments/attachments
2 parents 1510d25 + 5370803 commit a80383d

File tree

5 files changed

+117
-14
lines changed

5 files changed

+117
-14
lines changed

qubesadmin/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,8 +797,10 @@ def _invalidate_cache_all(self):
797797
# pylint: disable=protected-access
798798
self.domains.clear_cache()
799799
for vm in self.domains._vm_objects.values():
800+
assert isinstance(vm, qubesadmin.vm.QubesVM)
800801
vm._power_state_cache = None
801802
vm._properties_cache = {}
803+
vm.devices.clear_cache()
802804
self._properties_cache = {}
803805

804806

qubesadmin/devices.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ def __init__(self, vm, class_):
6262
self._vm = vm
6363
self._class = class_
6464
self._dev_cache = {}
65+
#: attachments cache, `None` means "not cached (yet)",
66+
#: in contrast to empty list which means "cached empty list"
67+
self._attachment_cache = None
68+
#: assignments cache, `None` means "not cached (yet)",
69+
#: in contrast to empty list which means "cached empty list"
70+
self._assignment_cache = None
6571

6672
def attach(self, assignment: DeviceAssignment) -> None:
6773
"""
@@ -75,6 +81,9 @@ def attach(self, assignment: DeviceAssignment) -> None:
7581
"did you mean `qvm-pci assign --required ...`"
7682
)
7783
self._add(assignment, "attach")
84+
# clear the whole cache instead of saving provided assignment, it might
85+
# get modified before actually attaching
86+
self._attachment_cache = None
7887

7988
def detach(self, assignment: DeviceAssignment) -> None:
8089
"""
@@ -84,6 +93,7 @@ def detach(self, assignment: DeviceAssignment) -> None:
8493
(obtained from :py:meth:`assignments`)
8594
"""
8695
self._remove(assignment, "detach")
96+
self._assignment_cache = None
8797

8898
def assign(self, assignment: DeviceAssignment) -> None:
8999
"""
@@ -113,6 +123,9 @@ def assign(self, assignment: DeviceAssignment) -> None:
113123
)
114124

115125
self._add(assignment, "assign")
126+
# clear the whole cache instead of saving provided assignment, it might
127+
# get modified before actually assigning
128+
self._assignment_cache = None
116129

117130
def unassign(self, assignment: DeviceAssignment) -> None:
118131
"""
@@ -122,6 +135,7 @@ def unassign(self, assignment: DeviceAssignment) -> None:
122135
(obtained from :py:meth:`assignments`)
123136
"""
124137
self._remove(assignment, "unassign")
138+
self._assignment_cache = None
125139

126140
def _add(self, assignment: DeviceAssignment, action: str) -> None:
127141
"""
@@ -186,6 +200,10 @@ def get_attached_devices(self) -> Iterable[DeviceAssignment]:
186200
"""
187201
List devices which are attached to this vm.
188202
"""
203+
if self._attachment_cache is not None:
204+
yield from self._attachment_cache
205+
return
206+
new_cache = []
189207
assignments_str = self._vm.qubesd_call(
190208
None, "admin.vm.device.{}.Attached".format(self._class)
191209
).decode()
@@ -195,9 +213,14 @@ def get_attached_devices(self) -> Iterable[DeviceAssignment]:
195213
head, self._class, self._vm.app.domains, blind=True
196214
)
197215

198-
yield DeviceAssignment.deserialize(
216+
assignment = DeviceAssignment.deserialize(
199217
untrusted_rest.encode("ascii"), expected_device=device
200218
)
219+
new_cache.append(assignment)
220+
yield assignment
221+
222+
if self._vm.app.cache_enabled:
223+
self._attachment_cache = new_cache
201224

202225
def get_assigned_devices(
203226
self, required_only: bool = False
@@ -207,6 +230,12 @@ def get_assigned_devices(
207230
208231
Safe to access before libvirt bootstrap.
209232
"""
233+
if self._assignment_cache is not None:
234+
for assignment in self._assignment_cache:
235+
if not required_only or assignment.required:
236+
yield assignment
237+
return
238+
new_cache = []
210239
assignments_str = self._vm.qubesd_call(
211240
None, "admin.vm.device.{}.Assigned".format(self._class)
212241
).decode()
@@ -219,9 +248,13 @@ def get_assigned_devices(
219248
assignment = DeviceAssignment.deserialize(
220249
untrusted_rest.encode("ascii"), expected_device=device
221250
)
251+
new_cache.append(assignment)
222252
if not required_only or assignment.required:
223253
yield assignment
224254

255+
if self._vm.app.cache_enabled:
256+
self._assignment_cache = new_cache
257+
225258
def get_exposed_devices(self) -> Iterable[DeviceInfo]:
226259
"""
227260
List devices exposed by this vm.
@@ -252,6 +285,7 @@ def update_assignment(self, device: Port, required: AssignmentMode):
252285
repr(device),
253286
required.value.encode("utf-8"),
254287
)
288+
self._assignment_cache = None
255289

256290
__iter__ = get_exposed_devices
257291

@@ -260,6 +294,8 @@ def clear_cache(self):
260294
Clear cache of available devices.
261295
"""
262296
self._dev_cache.clear()
297+
self._assignment_cache = None
298+
self._attachment_cache = None
263299

264300
def __getitem__(self, item):
265301
"""Get device object with given port_id.
@@ -325,3 +361,8 @@ def allow(self, *interfaces: Iterable[DeviceInterface]):
325361
None,
326362
"".join(repr(ifc) for ifc in interfaces).encode('ascii'),
327363
)
364+
365+
def clear_cache(self):
366+
"""Clear cache of all available device classes"""
367+
for devclass in self.values():
368+
devclass.clear_cache()

qubesadmin/events/__init__.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,35 @@ def handle(self, subject, event, **kwargs):
229229
vm = kwargs['vm']
230230
self.app.domains.clear_cache(invalidate_name=str(vm))
231231
subject = None
232+
# invalidate cache if needed; call it before other handlers
233+
# as those may want to use cached value
234+
if event.startswith('property-set:') or \
235+
event.startswith('property-reset:'):
236+
self.app._invalidate_cache(subject, event, **kwargs)
237+
elif event in ('domain-pre-start', 'domain-start', 'domain-shutdown',
238+
'domain-paused', 'domain-unpaused',
239+
'domain-start-failed'):
240+
self.app._update_power_state_cache(subject, event, **kwargs)
241+
subject.devices.clear_cache()
242+
elif event == 'connection-established':
243+
# on (re)connection, clear cache completely - we don't have
244+
# guarantee about not missing any events before this point
245+
self.app._invalidate_cache_all()
246+
elif event.split(":")[0] in (
247+
"device-assign",
248+
"device-unassign",
249+
"device-assignment-changed"
250+
):
251+
devclass = event.split(":")[1]
252+
subject.devices[devclass]._assignment_cache = None
253+
elif event.split(":")[0] in (
254+
"device-attach",
255+
"device-detach",
256+
"device-removed"
257+
):
258+
devclass = event.split(":")[1]
259+
subject.devices[devclass]._attachment_cache = None
260+
232261
# deserialize known attributes
233262
if event.startswith('device-'):
234263
try:
@@ -256,19 +285,6 @@ def handle(self, subject, event, **kwargs):
256285
kwargs['port'], devclass, self.app.domains, blind=True)
257286
except (KeyError, ValueError):
258287
pass
259-
# invalidate cache if needed; call it before other handlers
260-
# as those may want to use cached value
261-
if event.startswith('property-set:') or \
262-
event.startswith('property-reset:'):
263-
self.app._invalidate_cache(subject, event, **kwargs)
264-
elif event in ('domain-pre-start', 'domain-start', 'domain-shutdown',
265-
'domain-paused', 'domain-unpaused',
266-
'domain-start-failed'):
267-
self.app._update_power_state_cache(subject, event, **kwargs)
268-
elif event == 'connection-established':
269-
# on (re)connection, clear cache completely - we don't have
270-
# guarantee about not missing any events before this point
271-
self.app._invalidate_cache_all()
272288

273289
handlers = [h_func for h_name, h_func_set in self.handlers.items()
274290
for h_func in h_func_set

qubesadmin/tests/devices.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,49 @@ def test_041_assignments_options(self):
255255

256256
self.assertAllCalled()
257257

258+
def test_042_assignments_cache(self):
259+
self.app.cache_enabled = True
260+
self.app.expected_calls[
261+
('test-vm', 'admin.vm.device.test.Attached', None, None)] = \
262+
(b"0\0test-vm2+dev1 backend_domain='test-vm2' port_id='dev1' "
263+
b"mode='manual' devclass='test' "
264+
b"frontend_domain='test-vm' _ro='True'\n")
265+
self.app.expected_calls[
266+
('test-vm', 'admin.vm.device.test.Assigned', None, None)] = \
267+
(b"0\0test-vm3+dev2 backend_domain='test-vm3' devclass='test' "
268+
b"port_id='dev2' mode='required' "
269+
b"frontend_domain='test-vm' _ro='False'\n")
270+
# populate cache
271+
list(self.vm.devices['test'].get_dedicated_devices())
272+
273+
self.assertAllCalled()
274+
self.app.expected_calls.clear()
275+
276+
# get again, should be cached now
277+
assigns = sorted(list(
278+
self.vm.devices['test'].get_dedicated_devices()))
279+
280+
self.assertEqual(len(assigns), 2)
281+
self.assertIsInstance(assigns[0], DeviceAssignment)
282+
self.assertEqual(assigns[0].backend_domain,
283+
self.app.domains['test-vm2'])
284+
self.assertEqual(assigns[0].port_id, 'dev1')
285+
self.assertEqual(assigns[0].frontend_domain,
286+
self.app.domains['test-vm'])
287+
self.assertEqual(assigns[0].options, {'ro': 'True'})
288+
self.assertEqual(assigns[0].required, False)
289+
self.assertEqual(assigns[0].devclass, 'test')
290+
291+
self.assertIsInstance(assigns[1], DeviceAssignment)
292+
self.assertEqual(assigns[1].backend_domain,
293+
self.app.domains['test-vm3'])
294+
self.assertEqual(assigns[1].port_id, 'dev2')
295+
self.assertEqual(assigns[1].frontend_domain,
296+
self.app.domains['test-vm'])
297+
self.assertEqual(assigns[1].options, {'ro': 'False'})
298+
self.assertEqual(assigns[1].required, True)
299+
self.assertEqual(assigns[1].devclass, 'test')
300+
258301
def test_050_required(self):
259302
self.app.expected_calls[
260303
('test-vm', 'admin.vm.device.test.Assigned', None, None)] = \

qubesadmin/tools/qvm_device.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,7 @@ def get_parser(device_class=None):
757757
def main(args=None, app=None):
758758
"""Main routine of :program:`qvm-block`."""
759759
app = app or qubesadmin.Qubes()
760+
app.cache_enabled = True
760761

761762
basename = os.path.basename(sys.argv[0])
762763
devclass = None

0 commit comments

Comments
 (0)