@@ -222,3 +222,185 @@ def test_015_wait_all_kill_timeout(self):
222222 qubesadmin .tools .qvm_shutdown .main (
223223 ['--wait' , '--all' , '--timeout=1' ], app = self .app )
224224 self .assertAllCalled ()
225+
226+ def test_005_force (self ):
227+ '''test --force sends force flag to shutdown call'''
228+ self .app .expected_calls [
229+ ('dom0' , 'admin.vm.List' , None , None )] = \
230+ b'0\x00 some-vm class=AppVM state=Running\n '
231+ self .app .expected_calls [
232+ ('some-vm' , 'admin.vm.Shutdown' , 'force' , None )] = b'0\x00 '
233+ qubesadmin .tools .qvm_shutdown .main (
234+ ['--force' , 'some-vm' ], app = self .app )
235+ self .assertAllCalled ()
236+
237+ def test_006_dry_run (self ):
238+ '''test --dry-run skips shutdown calls'''
239+ self .app .expected_calls [
240+ ('dom0' , 'admin.vm.List' , None , None )] = \
241+ b'0\x00 some-vm class=AppVM state=Running\n '
242+ qubesadmin .tools .qvm_shutdown .main (
243+ ['--dry-run' , 'some-vm' ], app = self .app )
244+ self .assertAllCalled ()
245+
246+ @unittest .skipUnless (qubesadmin .tools .qvm_shutdown .have_events ,
247+ 'Events not present' )
248+ def test_011_wait_retry (self ):
249+ '''test --wait retries VMs whose shutdown request failed'''
250+ loop = asyncio .new_event_loop ()
251+ asyncio .set_event_loop (loop )
252+
253+ mock_events = unittest .mock .AsyncMock ()
254+ patch = unittest .mock .patch (
255+ 'qubesadmin.events.EventsDispatcher._get_events_reader' ,
256+ mock_events )
257+ patch .start ()
258+ self .addCleanup (patch .stop )
259+ mock_events .side_effect = qubesadmin .tests .tools .MockEventsReader ([
260+ # round 1: wait for some-vm
261+ b'1\0 \0 connection-established\0 \0 ' ,
262+ b'1\0 some-vm\0 domain-shutdown\0 \0 ' ,
263+ # round 2: wait for other-vm
264+ b'1\0 \0 connection-established\0 \0 ' ,
265+ b'1\0 other-vm\0 domain-shutdown\0 \0 ' ,
266+ ])
267+
268+ self .app .expected_calls [
269+ ('dom0' , 'admin.vm.List' , None , None )] = \
270+ b'0\x00 ' \
271+ b'some-vm class=AppVM state=Running\n ' \
272+ b'other-vm class=AppVM state=Running\n '
273+ self .app .expected_calls [
274+ ('some-vm' , 'admin.vm.Shutdown' , None , None )] = \
275+ b'0\x00 '
276+ # other-vm fails first attempt, succeeds on retry
277+ self .app .expected_calls [
278+ ('other-vm' , 'admin.vm.Shutdown' , None , None )] = [
279+ b'2\x00 QubesException\x00 \x00 Shutdown refused\x00 ' ,
280+ b'0\x00 ' ,
281+ ]
282+ self .app .expected_calls [
283+ ('some-vm' , 'admin.vm.CurrentState' , None , None )] = [
284+ b'0\x00 power_state=Running' ,
285+ b'0\x00 power_state=Halted' ,
286+ ]
287+ self .app .expected_calls [
288+ ('other-vm' , 'admin.vm.CurrentState' , None , None )] = [
289+ b'0\x00 power_state=Running' ,
290+ b'0\x00 power_state=Halted' ,
291+ ]
292+ qubesadmin .tools .qvm_shutdown .main (
293+ ['--wait' , 'some-vm' , 'other-vm' ], app = self .app )
294+ self .assertAllCalled ()
295+
296+ @unittest .skipUnless (qubesadmin .tools .qvm_shutdown .have_events ,
297+ 'Events not present' )
298+ def test_013_wait_all_shutdown_fail (self ):
299+ '''test --wait exits with error when all shutdown requests fail'''
300+ self .app .expected_calls [
301+ ('dom0' , 'admin.vm.List' , None , None )] = \
302+ b'0\x00 some-vm class=AppVM state=Running\n '
303+ self .app .expected_calls [
304+ ('some-vm' , 'admin.vm.Shutdown' , None , None )] = \
305+ b'2\x00 QubesException\x00 \x00 Shutdown refused\x00 '
306+ self .app .expected_calls [
307+ ('some-vm' , 'admin.vm.CurrentState' , None , None )] = \
308+ b'0\x00 power_state=Running'
309+ with self .assertRaises (SystemExit ):
310+ qubesadmin .tools .qvm_shutdown .main (
311+ ['--wait' , 'some-vm' ], app = self .app )
312+ self .assertAllCalled ()
313+
314+ @unittest .skipUnless (qubesadmin .tools .qvm_shutdown .have_events ,
315+ 'Events not present' )
316+ def test_016_wait_kill_exception (self ):
317+ '''test --wait timeout where kill raises QubesException'''
318+ loop = asyncio .new_event_loop ()
319+ asyncio .set_event_loop (loop )
320+
321+ mock_events = unittest .mock .AsyncMock ()
322+ patch = unittest .mock .patch (
323+ 'qubesadmin.events.EventsDispatcher._get_events_reader' ,
324+ mock_events )
325+ patch .start ()
326+ self .addCleanup (patch .stop )
327+ mock_events .side_effect = qubesadmin .tests .tools .MockEventsReader ([
328+ b'1\0 \0 connection-established\0 \0 ' ,
329+ ])
330+
331+ self .app .expected_calls [
332+ ('dom0' , 'admin.vm.List' , None , None )] = \
333+ b'0\x00 some-vm class=AppVM state=Running\n '
334+ self .app .expected_calls [
335+ ('some-vm' , 'admin.vm.Shutdown' , None , None )] = \
336+ b'0\x00 '
337+ self .app .expected_calls [
338+ ('some-vm' , 'admin.vm.Kill' , None , None )] = \
339+ b'2\x00 QubesException\x00 \x00 Kill failed\x00 '
340+ self .app .expected_calls [
341+ ('some-vm' , 'admin.vm.CurrentState' , None , None )] = [
342+ b'0\x00 power_state=Running' ,
343+ b'0\x00 power_state=Running' ,
344+ ]
345+ with self .assertRaises (SystemExit ):
346+ qubesadmin .tools .qvm_shutdown .main (
347+ ['--wait' , '--timeout=1' , 'some-vm' ], app = self .app )
348+ self .assertAllCalled ()
349+
350+ @unittest .skipUnless (qubesadmin .tools .qvm_shutdown .have_events ,
351+ 'Events not present' )
352+ def test_017_wait_dispvm_na (self ):
353+ '''test --wait treats DispVM with NA power state as shut down'''
354+ loop = asyncio .new_event_loop ()
355+ asyncio .set_event_loop (loop )
356+
357+ mock_events = unittest .mock .AsyncMock ()
358+ patch = unittest .mock .patch (
359+ 'qubesadmin.events.EventsDispatcher._get_events_reader' ,
360+ mock_events )
361+ patch .start ()
362+ self .addCleanup (patch .stop )
363+ mock_events .side_effect = qubesadmin .tests .tools .MockEventsReader ([
364+ b'1\0 \0 connection-established\0 \0 ' ,
365+ b'1\0 disp123\0 domain-shutdown\0 \0 ' ,
366+ ])
367+
368+ self .app .expected_calls [
369+ ('dom0' , 'admin.vm.List' , None , None )] = \
370+ b'0\x00 disp123 class=DispVM state=Running\n '
371+ self .app .expected_calls [
372+ ('disp123' , 'admin.vm.Shutdown' , None , None )] = \
373+ b'0\x00 '
374+ self .app .expected_calls [
375+ ('disp123' , 'admin.vm.CurrentState' , None , None )] = [
376+ b'0\x00 power_state=Running' ,
377+ # failed_domains: first get_power_state() != 'Halted',
378+ # then klass == 'DispVM' triggers second get_power_state()
379+ b'0\x00 power_state=NA' ,
380+ b'0\x00 power_state=NA' ,
381+ ]
382+ qubesadmin .tools .qvm_shutdown .main (
383+ ['--wait' , 'disp123' ], app = self .app )
384+ self .assertAllCalled ()
385+
386+ def test_018_wait_polling_fallback (self ):
387+ '''test --wait uses polling when events are unavailable'''
388+ loop = asyncio .new_event_loop ()
389+ asyncio .set_event_loop (loop )
390+
391+ self .app .expected_calls [
392+ ('dom0' , 'admin.vm.List' , None , None )] = \
393+ b'0\x00 some-vm class=AppVM state=Running\n '
394+ self .app .expected_calls [
395+ ('some-vm' , 'admin.vm.Shutdown' , None , None )] = \
396+ b'0\x00 '
397+ self .app .expected_calls [
398+ ('some-vm' , 'admin.vm.CurrentState' , None , None )] = [
399+ b'0\x00 power_state=Halted' ,
400+ b'0\x00 power_state=Halted' ,
401+ ]
402+ with unittest .mock .patch .object (
403+ qubesadmin .tools .qvm_shutdown , 'have_events' , False ):
404+ qubesadmin .tools .qvm_shutdown .main (
405+ ['--wait' , 'some-vm' ], app = self .app )
406+ self .assertAllCalled ()
0 commit comments