2020
2121""" A disposable vm implementation """
2222
23+ import asyncio
2324import copy
25+ import psutil
26+ import subprocess
2427
2528import qubes .vm .qubesvm
2629import qubes .vm .appvm
@@ -187,11 +190,170 @@ def __init__(self, app, xml, *args, **kwargs):
187190 self .features .update (template .features )
188191 self .tags .update (template .tags )
189192
193+ def get_feat_preload (self , feature ):
194+ if feature not in ["preload-dispvm" , "preload-dispvm-max" ]:
195+ raise qubes .exc .QubesException ("Invalid feature provided" )
196+
197+ if feature == "preload-dispvm" :
198+ default = ""
199+ elif feature == "preload-dispvm-max" :
200+ default = 0
201+
202+ value = self .features .check_with_template (feature , default )
203+
204+ if feature == "preload-dispvm" :
205+ return value .split (" " )
206+ if feature == "preload-dispvm-max" :
207+ return int (value )
208+ return None
209+
210+ def is_preloaded (self ):
211+ preload_dispvm = self .get_feat_preload ("preload-dispvm" )
212+ if not preload_dispvm :
213+ return False
214+ if self .name not in preload_dispvm :
215+ return False
216+ return True
217+
218+ async def mark_preloaded (self ):
219+ """
220+ Create preloaded DispVM.
221+
222+ Template from which the VM should be created.
223+
224+ :return:
225+ """
226+ preload_dispvm = self .get_feat_preload ("preload-dispvm" )
227+ if preload_dispvm :
228+ preload_dispvm .append (self .name )
229+ else :
230+ preload_dispvm = [self .name ]
231+
232+ appvm = getattr (self , "template" )
233+ appvm .features ["preload-dispvm" ] = " " .join (preload_dispvm )
234+ self .features ["internal" ] = True
235+
236+ async def use_preloaded (self ):
237+ """
238+ Mark preloaded DispVM as used.
239+
240+ :return:
241+ """
242+ appvm = getattr (self , "template" )
243+
244+ preload_dispvm = self .get_feat_preload ("preload-dispvm" )
245+ if self .name not in preload_dispvm :
246+ raise qubes .exc .QubesException ("DispVM is not preloaded" )
247+
248+ preload_dispvm = " " .join (preload_dispvm .remove (self .name ))
249+ appvm .features ["preload-dispvm" ] = preload_dispvm
250+ self .features ["internal" ] = False
251+ await appvm .fire_event_async (
252+ "domain-preloaded-dispvm-used" , dispvm = self
253+ )
254+
255+ @qubes .events .handler (
256+ "domain-preloaded-dispvm-used" , "domain-preloaded-dispvm-autostart"
257+ )
258+ async def on_domain_preloaded_dispvm_used (self , event , delay = 5 , ** kwargs ): # pylint: disable=unused-argument
259+ """When preloaded DispVM is used or after boot, preload another one.
260+
261+ :param event: event which was fired
262+ :param delay: delay between trials
263+ :returns:
264+ """
265+ await asyncio .sleep (delay )
266+ while True :
267+ # TODO: Is there existing Qubes code that checks available memory
268+ # before starting a qube?
269+ memory = getattr (self , "memory" , 0 )
270+ available_memory = (
271+ psutil .virtual_memory ().available / (1024 * 1024 )
272+ )
273+ threshold = 1024 * 5
274+ if memory >= (available_memory - threshold ):
275+ ## TODO: how to pass arg?
276+ await qubes .vm .dispvm .DispVM .from_appvm (
277+ self , preload = True
278+ ).start ()
279+ #await qubes.api.admin.QubesAdminAPI.create_disposable(
280+ # self.app, b"dom0", "admin.vm.CreateDisposable", b"dom0", b"preload"
281+ #)
282+ # TODO: what to do if the maximum is never reached on autostart
283+ # as there is not enough memory, and then a preloaded DispVM is
284+ # used, calling for the creation of another one, while the
285+ # autostart will also try to create one. Is this a race
286+ # condition?
287+ # TODO: fire event after start of all qubes that are set to
288+ # autostart.
289+ if event == "domain-preloaded-dispvm-autostart" :
290+ preload_dispvm_max = self .get_feat_preload (
291+ "preload-dispvm-max"
292+ )
293+ preload_dispvm = self .get_feat_preload ("preload-dispvm" )
294+ if (
295+ preload_dispvm
296+ and len (preload_dispvm ) < preload_dispvm_max
297+ ):
298+ continue
299+ break
300+ await asyncio .sleep (delay )
301+
190302 @qubes .events .handler ("domain-load" )
191303 def on_domain_loaded (self , event ):
192304 """When domain is loaded assert that this vm has a template.""" # pylint: disable=unused-argument
193305 assert self .template
194306
307+ @qubes .events .handler ("domain-start" )
308+ # W0236 (invalid-overridden-method) Method 'on_domain_started' was expected
309+ # to be 'non-async', found it instead as 'async'
310+ # TODO: Seems to conflict with qubes.vm.mix.net, which is pretty strange.
311+ # Larger bug? qubes.vm.qubesvm.QubesVM has NetVMMixin... which conflicts...
312+ async def on_domain_started (self , event , ** kwargs ):
313+ """Pause preloaded domains as soon as they start."""
314+ # TODO:
315+ # Marek: Test if pause isn't too early. Some services (especially:
316+ # gui-agent) may still be starting. qubes.WaitForSession service may
317+ # help (ensure to use async handler to not block qubesd while waiting
318+ # on it).
319+ no_gui_sleep = 15
320+ gui_timeout = 30
321+ if self .is_preloaded ():
322+ gui = self .features .get ("gui" , None )
323+ if not gui :
324+ asyncio .sleep (no_gui_sleep )
325+ self .pause ()
326+ return
327+
328+ proc = None
329+ try :
330+ proc = await asyncio .wait_for (
331+ self .run_service (
332+ "qubes.WaitForSession" ,
333+ user = self .default_user ,
334+ stdout = subprocess .DEVNULL ,
335+ stderr = subprocess .DEVNULL ,
336+ ),
337+ timeout = gui_timeout ,
338+ )
339+ except asyncio .TimeoutError :
340+ ## TODO: should timeout be treated as an error/qubes.exc?
341+ return
342+ except (subprocess .CalledProcessError ,qubes .exc .QubesException ):
343+ raise qubes .exc .QubesException (
344+ "Failed to run QUBESRPC qubes.WaitForSession"
345+ )
346+ finally :
347+ if proc is not None :
348+ proc .terminate ()
349+ self .pause ()
350+
351+ @qubes .events .handler ("domain-unpaused" )
352+ async def on_domain_unpaused (self ):
353+ """Mark unpaused preloaded domains as used."""
354+ if self .is_preloaded ():
355+ await self .use_preloaded ()
356+
195357 @qubes .events .handler ("property-pre-reset:template" )
196358 def on_property_pre_reset_template (self , event , name , oldvalue = None ):
197359 """Forbid deleting template of VM""" # pylint: disable=unused-argument
@@ -228,11 +390,12 @@ async def _auto_cleanup(self):
228390 self .app .save ()
229391
230392 @classmethod
231- async def from_appvm (cls , appvm , ** kwargs ):
393+ async def from_appvm (cls , appvm , preload = False , ** kwargs ):
232394 """Create a new instance from given AppVM
233395
234396 :param qubes.vm.appvm.AppVM appvm: template from which the VM should \
235397 be created
398+ :param bool preload: Whether to preload a disposable
236399 :returns: new disposable vm
237400
238401 *kwargs* are passed to the newly created VM
@@ -251,10 +414,32 @@ async def from_appvm(cls, appvm, **kwargs):
251414 "template_for_dispvms=False"
252415 )
253416 app = appvm .app
417+
418+ if preload :
419+ preload_dispvm_max = appvm .get_feat_preload ("preload-dispvm-max" )
420+ if preload_dispvm_max == 0 :
421+ return
422+ preload_dispvm = appvm .get_feat_preload ("preload-dispvm" )
423+ if preload_dispvm and len (preload_dispvm ) >= preload_dispvm_max :
424+ raise qubes .exc .QubesException (
425+ "Failed to create preloaded disposable, limit of "
426+ "preloaded DispVMs reached"
427+ )
428+ else :
429+ preload_dispvm = appvm .get_feat_preload ("preload-dispvm" )
430+ if preload_dispvm :
431+ dispvm = app .domains [preload_dispvm [0 ]]
432+ await dispvm .use_preloaded ()
433+ return dispvm
434+
254435 dispvm = app .add_new_vm (
255436 cls , template = appvm , auto_cleanup = True , ** kwargs
256437 )
257438 await dispvm .create_on_disk ()
439+
440+ if preload :
441+ await dispvm .mark_preloaded ()
442+
258443 app .save ()
259444 return dispvm
260445
0 commit comments