Skip to content

Commit 6dec58d

Browse files
committed
q-dev: wait for attaching devices during startup and update tests
1 parent bee1797 commit 6dec58d

File tree

2 files changed

+104
-72
lines changed

2 files changed

+104
-72
lines changed

qubes/ext/block.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -562,8 +562,12 @@ async def on_domain_start(self, vm, _event, **_kwargs):
562562
if device not in to_attach:
563563
# make it unique
564564
to_attach[device] = assignment.clone(device=device)
565+
in_progress = set()
565566
for assignment in to_attach.values():
566-
asyncio.ensure_future(self.attach_and_notify(vm, assignment))
567+
in_progress.add(
568+
asyncio.ensure_future(self.attach_and_notify(vm, assignment)))
569+
if in_progress:
570+
await asyncio.wait(in_progress)
567571

568572
async def attach_and_notify(self, vm, assignment):
569573
# bypass DeviceCollection logic preventing double attach

qubes/tests/devices_block.py

+99-71
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import asyncio
2121
import unittest
2222
from unittest import mock
23-
from unittest.mock import Mock
23+
from unittest.mock import Mock, AsyncMock
2424

2525
import jinja2
2626

@@ -675,11 +675,12 @@ def test_061_on_qdb_change_required(self):
675675
back.devices['block']._exposed.append(
676676
qubes.ext.block.BlockDevice(back, 'sda'))
677677

678-
self.ext.attach_and_notify = Mock()
679-
with mock.patch('asyncio.ensure_future'):
680-
self.ext.on_qdb_change(back, None, None)
681-
self.ext.attach_and_notify.assert_called_once_with(
682-
front, assignment)
678+
resolver_path = 'qubes.ext.utils.resolve_conflicts_and_attach'
679+
with mock.patch(resolver_path, new_callable=Mock) as resolver:
680+
with mock.patch('asyncio.ensure_future'):
681+
self.ext.on_qdb_change(back, None, None)
682+
resolver.assert_called_once_with(
683+
self.ext, {'sda': {front: assignment}})
683684

684685
def test_062_on_qdb_change_auto_attached(self):
685686
back, front = self.added_assign_setup()
@@ -690,11 +691,12 @@ def test_062_on_qdb_change_auto_attached(self):
690691
back.devices['block']._exposed.append(
691692
qubes.ext.block.BlockDevice(back, 'sda'))
692693

693-
self.ext.attach_and_notify = Mock()
694-
with mock.patch('asyncio.ensure_future'):
695-
self.ext.on_qdb_change(back, None, None)
696-
self.ext.attach_and_notify.assert_called_once_with(
697-
front, assignment)
694+
resolver_path = 'qubes.ext.utils.resolve_conflicts_and_attach'
695+
with mock.patch(resolver_path, new_callable=Mock) as resolver:
696+
with mock.patch('asyncio.ensure_future'):
697+
self.ext.on_qdb_change(back, None, None)
698+
resolver.assert_called_once_with(
699+
self.ext, {'sda': {front: assignment}})
698700

699701
def test_063_on_qdb_change_ask_to_attached(self):
700702
back, front = self.added_assign_setup()
@@ -705,11 +707,12 @@ def test_063_on_qdb_change_ask_to_attached(self):
705707
back.devices['block']._exposed.append(
706708
qubes.ext.block.BlockDevice(back, 'sda'))
707709

708-
self.ext.attach_and_notify = Mock()
709-
with mock.patch('asyncio.ensure_future'):
710-
self.ext.on_qdb_change(back, None, None)
711-
self.ext.attach_and_notify.assert_called_once_with(
712-
front, assignment)
710+
resolver_path = 'qubes.ext.utils.resolve_conflicts_and_attach'
711+
with mock.patch(resolver_path, new_callable=Mock) as resolver:
712+
with mock.patch('asyncio.ensure_future'):
713+
self.ext.on_qdb_change(back, None, None)
714+
resolver.assert_called_once_with(
715+
self.ext, {'sda': {front: assignment}})
713716

714717
def test_064_on_qdb_change_multiple_assignments_including_full(self):
715718
back, front = self.added_assign_setup()
@@ -732,11 +735,12 @@ def test_064_on_qdb_change_multiple_assignments_including_full(self):
732735
back.devices['block']._exposed.append(
733736
qubes.ext.block.BlockDevice(back, 'sda'))
734737

735-
self.ext.attach_and_notify = Mock()
736-
with mock.patch('asyncio.ensure_future'):
737-
self.ext.on_qdb_change(back, None, None)
738-
self.assertEqual(self.ext.attach_and_notify.call_args[0][1].options,
739-
{'pid': 'did'})
738+
resolver_path = 'qubes.ext.utils.resolve_conflicts_and_attach'
739+
with mock.patch(resolver_path, new_callable=Mock) as resolver:
740+
with mock.patch('asyncio.ensure_future'):
741+
self.ext.on_qdb_change(back, None, None)
742+
self.assertEqual(
743+
resolver.call_args[0][1]['sda'][front].options,{'pid': 'did'})
740744

741745
def test_065_on_qdb_change_multiple_assignments_port_vs_dev(self):
742746
back, front = self.added_assign_setup()
@@ -755,11 +759,12 @@ def test_065_on_qdb_change_multiple_assignments_port_vs_dev(self):
755759
back.devices['block']._exposed.append(
756760
qubes.ext.block.BlockDevice(back, 'sda'))
757761

758-
self.ext.attach_and_notify = Mock()
759-
with mock.patch('asyncio.ensure_future'):
760-
self.ext.on_qdb_change(back, None, None)
761-
self.assertEqual(self.ext.attach_and_notify.call_args[0][1].options,
762-
{'pid': 'any'})
762+
resolver_path = 'qubes.ext.utils.resolve_conflicts_and_attach'
763+
with mock.patch(resolver_path, new_callable=Mock) as resolver:
764+
with mock.patch('asyncio.ensure_future'):
765+
self.ext.on_qdb_change(back, None, None)
766+
self.assertEqual(
767+
resolver.call_args[0][1]['sda'][front].options, {'pid': 'any'})
763768

764769
def test_066_on_qdb_change_multiple_assignments_dev(self):
765770
back, front = self.added_assign_setup()
@@ -780,13 +785,16 @@ def test_066_on_qdb_change_multiple_assignments_dev(self):
780785
back.devices['block']._exposed.append(
781786
qubes.ext.block.BlockDevice(back, 'other'))
782787

783-
self.ext.attach_and_notify = Mock()
784-
with mock.patch('asyncio.ensure_future'):
785-
self.ext.on_qdb_change(back, None, None)
786-
self.assertEqual(self.ext.attach_and_notify.call_args[0][1].options,
787-
{'any': 'did'})
788+
resolver_path = 'qubes.ext.utils.resolve_conflicts_and_attach'
789+
with mock.patch(resolver_path, new_callable=Mock) as resolver:
790+
with mock.patch('asyncio.ensure_future'):
791+
self.ext.on_qdb_change(back, None, None)
792+
self.assertEqual(
793+
resolver.call_args[0][1]['sda'][front].options, {'any': 'did'})
788794

789-
def test_067_on_qdb_change_attached(self):
795+
@unittest.mock.patch(
796+
'qubes.ext.utils.resolve_conflicts_and_attach', new_callable=Mock)
797+
def test_067_on_qdb_change_attached(self, _confirm):
790798
# added
791799
back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='r'), domain_xml=domain_xml_template.format(""))
792800
exp_dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
@@ -830,7 +838,9 @@ def test_067_on_qdb_change_attached(self):
830838
fire_event_async.assert_called_once_with(
831839
'device-attach:block', device=exp_dev, options={})
832840

833-
def test_068_on_qdb_change_changed(self):
841+
@unittest.mock.patch(
842+
'qubes.ext.utils.resolve_conflicts_and_attach', new_callable=Mock)
843+
def test_068_on_qdb_change_changed(self, _confirm):
834844
# attached to front-vm
835845
back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='r'), domain_xml=domain_xml_template.format(""))
836846
exp_dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
@@ -890,7 +900,9 @@ def test_068_on_qdb_change_changed(self):
890900
fire_event_async_2.assert_called_once_with(
891901
'device-attach:block', device=exp_dev, options={})
892902

893-
def test_069_on_qdb_change_removed_attached(self):
903+
@unittest.mock.patch(
904+
'qubes.ext.utils.resolve_conflicts_and_attach', new_callable=Mock)
905+
def test_069_on_qdb_change_removed_attached(self, _confirm):
894906
# attached to front-vm
895907
back_vm = TestVM(name='sys-usb', qdb=get_qdb(mode='r'), domain_xml=domain_xml_template.format(""))
896908
dom0 = TestVM({}, name='dom0',
@@ -943,10 +955,7 @@ def test_069_on_qdb_change_removed_attached(self):
943955
('device-removed:block', frozenset({('port', exp_dev.port)}))],
944956
1)
945957

946-
# with `new_callable=Mock` we override async function with synchronous Mock
947-
@unittest.mock.patch(
948-
'qubes.ext.utils.confirm_device_attachment', new_callable=Mock)
949-
def test_070_on_qdb_change_two_fronts_failed(self, _mock_confirm):
958+
def test_070_on_qdb_change_two_fronts(self):
950959
back, front = self.added_assign_setup()
951960

952961
exp_dev = qubes.ext.block.BlockDevice(back, 'sda')
@@ -956,43 +965,58 @@ def test_070_on_qdb_change_two_fronts_failed(self, _mock_confirm):
956965
back.devices['block']._assigned.append(assign)
957966
back.devices['block']._exposed.append(exp_dev)
958967

959-
self.ext.attach_and_notify = Mock()
968+
resolver_path = 'qubes.ext.utils.resolve_conflicts_and_attach'
969+
with mock.patch(resolver_path, new_callable=Mock) as resolver:
970+
with mock.patch('asyncio.ensure_future'):
971+
self.ext.on_qdb_change(back, None, None)
972+
resolver.assert_called_once_with(
973+
self.ext, {'sda': {front: assign, back: assign}})
960974

961-
with mock.patch('qubes.ext.utils.asyncio.ensure_future') as future:
962-
future.return_value = Mock()
963-
future.return_value.result = Mock()
964-
future.return_value.result.return_value = "nonsense"
965-
self.ext.on_qdb_change(back, None, None)
975+
@unittest.mock.patch('asyncio.create_subprocess_shell')
976+
def test_071_failed_confirmation(self, shell):
977+
back, front = self.added_assign_setup()
966978

979+
exp_dev = qubes.ext.block.BlockDevice(back, 'sda')
980+
assign = DeviceAssignment(exp_dev, mode='auto-attach')
981+
982+
front.devices['block']._assigned.append(assign)
983+
back.devices['block']._assigned.append(assign)
984+
back.devices['block']._exposed.append(exp_dev)
985+
986+
proc = AsyncMock()
987+
shell.return_value = proc
988+
proc.communicate = AsyncMock()
989+
proc.communicate.return_value = (b'nonsense', b'')
990+
991+
loop = asyncio.get_event_loop()
992+
self.ext.attach_and_notify = AsyncMock()
993+
loop.run_until_complete(qubes.ext.utils.resolve_conflicts_and_attach(
994+
self.ext, {'sda': {front: assign, back: assign}}))
967995
self.ext.attach_and_notify.assert_not_called()
968996

969-
# with `new_callable=Mock` we override async function with synchronous Mock
970-
@unittest.mock.patch(
971-
'qubes.ext.utils.confirm_device_attachment', new_callable=Mock)
972-
def test_071_on_qdb_change_two_fronts(self, _mock_confirm):
997+
@unittest.mock.patch('asyncio.create_subprocess_shell')
998+
def test_072_successful_confirmation(self, shell):
973999
back, front = self.added_assign_setup()
9741000

9751001
exp_dev = qubes.ext.block.BlockDevice(back, 'sda')
976-
assign = DeviceAssignment(exp_dev, mode='ask-to-attach')
1002+
assign = DeviceAssignment(exp_dev, mode='auto-attach')
9771003

9781004
front.devices['block']._assigned.append(assign)
9791005
back.devices['block']._assigned.append(assign)
9801006
back.devices['block']._exposed.append(exp_dev)
9811007

982-
self.ext.attach_and_notify = Mock()
983-
984-
with mock.patch('asyncio.ensure_future') as future:
985-
future.return_value = Mock()
986-
future.return_value.result = Mock()
987-
future.return_value.result.return_value = "front-vm"
988-
self.ext.on_qdb_change(back, None, None)
1008+
proc = AsyncMock()
1009+
shell.return_value = proc
1010+
proc.communicate = AsyncMock()
1011+
proc.communicate.return_value = (b'front-vm', b'')
9891012

1013+
loop = asyncio.get_event_loop()
1014+
self.ext.attach_and_notify = AsyncMock()
1015+
loop.run_until_complete(qubes.ext.utils.resolve_conflicts_and_attach(
1016+
self.ext, {'sda': {front: assign, back: assign}}))
9901017
self.ext.attach_and_notify.assert_called_once_with(front, assign)
991-
# don't ask again
992-
self.assertEqual(self.ext.attach_and_notify.call_args[0][1].mode.value,
993-
'auto-attach')
9941018

995-
def test_072_on_qdb_change_ask(self):
1019+
def test_073_on_qdb_change_ask(self):
9961020
back, front = self.added_assign_setup()
9971021

9981022
exp_dev = qubes.ext.block.BlockDevice(back, 'sda')
@@ -1001,11 +1025,12 @@ def test_072_on_qdb_change_ask(self):
10011025
front.devices['block']._assigned.append(assign)
10021026
back.devices['block']._exposed.append(exp_dev)
10031027

1004-
self.ext.attach_and_notify = Mock()
1005-
with mock.patch('asyncio.ensure_future'):
1006-
self.ext.on_qdb_change(back, None, None)
1007-
self.assertEqual(self.ext.attach_and_notify.call_args[0][1].mode.value,
1008-
'ask-to-attach')
1028+
resolver_path = 'qubes.ext.utils.resolve_conflicts_and_attach'
1029+
with mock.patch(resolver_path, new_callable=Mock) as resolver:
1030+
with mock.patch('asyncio.ensure_future'):
1031+
self.ext.on_qdb_change(back, None, None)
1032+
resolver.assert_called_once_with(
1033+
self.ext, {'sda': {front: assign}})
10091034

10101035
def test_080_on_startup_multiple_assignments_including_full(self):
10111036
back, front = self.added_assign_setup()
@@ -1030,8 +1055,9 @@ def test_080_on_startup_multiple_assignments_including_full(self):
10301055

10311056
self.ext.attach_and_notify = Mock()
10321057
loop = asyncio.get_event_loop()
1033-
with mock.patch('asyncio.ensure_future'):
1034-
loop.run_until_complete(self.ext.on_domain_start(front, None))
1058+
with mock.patch('asyncio.wait'):
1059+
with mock.patch('asyncio.ensure_future'):
1060+
loop.run_until_complete(self.ext.on_domain_start(front, None))
10351061
self.assertEqual(self.ext.attach_and_notify.call_args[0][1].options,
10361062
{'pid': 'did'})
10371063

@@ -1054,8 +1080,9 @@ def test_081_on_startup_multiple_assignments_port_vs_dev(self):
10541080

10551081
self.ext.attach_and_notify = Mock()
10561082
loop = asyncio.get_event_loop()
1057-
with mock.patch('asyncio.ensure_future'):
1058-
loop.run_until_complete(self.ext.on_domain_start(front, None))
1083+
with mock.patch('asyncio.wait'):
1084+
with mock.patch('asyncio.ensure_future'):
1085+
loop.run_until_complete(self.ext.on_domain_start(front, None))
10591086
self.assertEqual(self.ext.attach_and_notify.call_args[0][1].options,
10601087
{'pid': 'any'})
10611088

@@ -1080,8 +1107,9 @@ def test_082_on_startup_multiple_assignments_dev(self):
10801107

10811108
self.ext.attach_and_notify = Mock()
10821109
loop = asyncio.get_event_loop()
1083-
with mock.patch('asyncio.ensure_future'):
1084-
loop.run_until_complete(self.ext.on_domain_start(front, None))
1110+
with mock.patch('asyncio.wait'):
1111+
with mock.patch('asyncio.ensure_future'):
1112+
loop.run_until_complete(self.ext.on_domain_start(front, None))
10851113
self.assertEqual(self.ext.attach_and_notify.call_args[0][1].options,
10861114
{'any': 'did'})
10871115

0 commit comments

Comments
 (0)