devices_block.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. # -*- encoding: utf8 -*-
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2017 Marek Marczykowski-Górecki
  6. # <marmarek@invisiblethingslab.com>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 2 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License along
  19. # with this program; if not, see <http://www.gnu.org/licenses/>.
  20. from unittest import mock
  21. import jinja2
  22. import qubes.tests
  23. import qubes.ext.block
  24. domain_xml_template = '''
  25. <domain type='xen' id='9'>
  26. <name>test-vm</name>
  27. <uuid>00000000-0000-0000-0000-0000000000ae</uuid>
  28. <memory unit='KiB'>4096000</memory>
  29. <currentMemory unit='KiB'>409600</currentMemory>
  30. <vcpu placement='static'>8</vcpu>
  31. <os>
  32. <type arch='x86_64' machine='xenpv'>linux</type>
  33. <kernel>/var/lib/qubes/vm-kernels/4.4.55-11/vmlinuz</kernel>
  34. <initrd>/var/lib/qubes/vm-kernels/4.4.55-11/initramfs</initrd>
  35. <cmdline>root=/dev/mapper/dmroot ro nomodeset console=hvc0 rd_NO_PLYMOUTH rd.plymouth.enable=0 plymouth.enable=0 dyndbg=&quot;file drivers/xen/gntdev.c +p&quot; printk=8</cmdline>
  36. </os>
  37. <clock offset='utc' adjustment='reset'>
  38. <timer name='tsc' mode='native'/>
  39. </clock>
  40. <on_poweroff>destroy</on_poweroff>
  41. <on_reboot>destroy</on_reboot>
  42. <on_crash>destroy</on_crash>
  43. <devices>
  44. <disk type='block' device='disk'>
  45. <driver name='phy'/>
  46. <source dev='/var/lib/qubes/vm-templates/fedora-25/root.img:/var/lib/qubes/vm-templates/fedora-25/root-cow.img'/>
  47. <backingStore/>
  48. <script path='block-snapshot'/>
  49. <target dev='xvda' bus='xen'/>
  50. <readonly/>
  51. </disk>
  52. <disk type='block' device='disk'>
  53. <driver name='phy'/>
  54. <source dev='/var/lib/qubes/appvms/test-vm/private.img'/>
  55. <backingStore/>
  56. <target dev='xvdb' bus='xen'/>
  57. </disk>
  58. <disk type='block' device='disk'>
  59. <driver name='phy'/>
  60. <source dev='/var/lib/qubes/appvms/test-vm/volatile.img'/>
  61. <backingStore/>
  62. <target dev='xvdc' bus='xen'/>
  63. </disk>
  64. <disk type='block' device='disk'>
  65. <driver name='phy'/>
  66. <source dev='/var/lib/qubes/vm-kernels/4.4.55-11/modules.img'/>
  67. <backingStore/>
  68. <target dev='xvdd' bus='xen'/>
  69. <readonly/>
  70. </disk>
  71. {}
  72. <interface type='ethernet'>
  73. <mac address='00:16:3e:5e:6c:06'/>
  74. <ip address='10.137.1.8' family='ipv4'/>
  75. <script path='vif-route-qubes'/>
  76. <backenddomain name='sys-firewall'/>
  77. </interface>
  78. <console type='pty' tty='/dev/pts/0'>
  79. <source path='/dev/pts/0'/>
  80. <target type='xen' port='0'/>
  81. </console>
  82. </devices>
  83. </domain>
  84. '''
  85. class TestQubesDB(object):
  86. def __init__(self, data):
  87. self._data = data
  88. def read(self, key):
  89. return self._data.get(key, None)
  90. def list(self, prefix):
  91. return [key for key in self._data if key.startswith(prefix)]
  92. class TestApp(object):
  93. def __init__(self):
  94. #: jinja2 environment for libvirt XML templates
  95. self.env = jinja2.Environment(
  96. loader=jinja2.FileSystemLoader([
  97. 'templates',
  98. '/etc/qubes/templates',
  99. '/usr/share/qubes/templates',
  100. ]),
  101. undefined=jinja2.StrictUndefined)
  102. self.domains = {}
  103. class TestVM(object):
  104. def __init__(self, qdb, domain_xml=None, running=True, name='test-vm'):
  105. self.name = name
  106. self.untrusted_qdb = TestQubesDB(qdb)
  107. self.libvirt_domain = mock.Mock()
  108. self.is_running = lambda: running
  109. self.log = mock.Mock()
  110. self.app = TestApp()
  111. if domain_xml:
  112. self.libvirt_domain.configure_mock(**{
  113. 'XMLDesc.return_value': domain_xml
  114. })
  115. def __eq__(self, other):
  116. if isinstance(other, TestVM):
  117. return self.name == other.name
  118. class TC_00_Block(qubes.tests.QubesTestCase):
  119. def setUp(self):
  120. super().setUp()
  121. self.ext = qubes.ext.block.BlockDeviceExtension()
  122. def test_000_device_get(self):
  123. vm = TestVM({
  124. '/qubes-block-devices/sda': b'',
  125. '/qubes-block-devices/sda/desc': b'Test device',
  126. '/qubes-block-devices/sda/size': b'1024000',
  127. '/qubes-block-devices/sda/mode': b'w',
  128. })
  129. device_info = self.ext.device_get(vm, 'sda')
  130. self.assertIsInstance(device_info, qubes.ext.block.BlockDevice)
  131. self.assertEqual(device_info.backend_domain, vm)
  132. self.assertEqual(device_info.ident, 'sda')
  133. self.assertEqual(device_info.description, 'Test device')
  134. self.assertEqual(device_info._description, 'Test device')
  135. self.assertEqual(device_info.size, 1024000)
  136. self.assertEqual(device_info.mode, 'w')
  137. self.assertEqual(device_info.frontend_domain, None)
  138. self.assertEqual(device_info.device_node, '/dev/sda')
  139. def test_001_device_get_other_node(self):
  140. vm = TestVM({
  141. '/qubes-block-devices/mapper_dmroot': b'',
  142. '/qubes-block-devices/mapper_dmroot/desc': b'Test device',
  143. '/qubes-block-devices/mapper_dmroot/size': b'1024000',
  144. '/qubes-block-devices/mapper_dmroot/mode': b'w',
  145. })
  146. device_info = self.ext.device_get(vm, 'mapper_dmroot')
  147. self.assertIsInstance(device_info, qubes.ext.block.BlockDevice)
  148. self.assertEqual(device_info.backend_domain, vm)
  149. self.assertEqual(device_info.ident, 'mapper_dmroot')
  150. self.assertEqual(device_info.description, 'Test device')
  151. self.assertEqual(device_info._description, 'Test device')
  152. self.assertEqual(device_info.size, 1024000)
  153. self.assertEqual(device_info.mode, 'w')
  154. self.assertEqual(device_info.frontend_domain, None)
  155. self.assertEqual(device_info.device_node, '/dev/mapper/dmroot')
  156. def test_002_device_get_invalid_desc(self):
  157. vm = TestVM({
  158. '/qubes-block-devices/sda': b'',
  159. '/qubes-block-devices/sda/desc': b'Test device<>za\xc4\x87abc',
  160. '/qubes-block-devices/sda/size': b'1024000',
  161. '/qubes-block-devices/sda/mode': b'w',
  162. })
  163. device_info = self.ext.device_get(vm, 'sda')
  164. self.assertEqual(device_info.description, 'Test device__za__abc')
  165. def test_003_device_get_invalid_size(self):
  166. vm = TestVM({
  167. '/qubes-block-devices/sda': b'',
  168. '/qubes-block-devices/sda/desc': b'Test device',
  169. '/qubes-block-devices/sda/size': b'1024000abc',
  170. '/qubes-block-devices/sda/mode': b'w',
  171. })
  172. device_info = self.ext.device_get(vm, 'sda')
  173. self.assertEqual(device_info.size, 0)
  174. vm.log.warning.assert_called_once_with('Device sda has invalid size')
  175. def test_004_device_get_invalid_mode(self):
  176. vm = TestVM({
  177. '/qubes-block-devices/sda': b'',
  178. '/qubes-block-devices/sda/desc': b'Test device',
  179. '/qubes-block-devices/sda/size': b'1024000',
  180. '/qubes-block-devices/sda/mode': b'abc',
  181. })
  182. device_info = self.ext.device_get(vm, 'sda')
  183. self.assertEqual(device_info.mode, 'w')
  184. vm.log.warning.assert_called_once_with('Device sda has invalid mode')
  185. def test_005_device_get_none(self):
  186. vm = TestVM({
  187. '/qubes-block-devices/sda': b'',
  188. '/qubes-block-devices/sda/desc': b'Test device',
  189. '/qubes-block-devices/sda/size': b'1024000',
  190. '/qubes-block-devices/sda/mode': b'w',
  191. })
  192. device_info = self.ext.device_get(vm, 'sdb')
  193. self.assertIsNone(device_info)
  194. def test_010_devices_list(self):
  195. vm = TestVM({
  196. '/qubes-block-devices/sda': b'',
  197. '/qubes-block-devices/sda/desc': b'Test device',
  198. '/qubes-block-devices/sda/size': b'1024000',
  199. '/qubes-block-devices/sda/mode': b'w',
  200. '/qubes-block-devices/sdb': b'',
  201. '/qubes-block-devices/sdb/desc': b'Test device2',
  202. '/qubes-block-devices/sdb/size': b'2048000',
  203. '/qubes-block-devices/sdb/mode': b'r',
  204. })
  205. devices = sorted(list(self.ext.on_device_list_block(vm, '')))
  206. self.assertEqual(len(devices), 2)
  207. self.assertEqual(devices[0].backend_domain, vm)
  208. self.assertEqual(devices[0].ident, 'sda')
  209. self.assertEqual(devices[0].description, 'Test device')
  210. self.assertEqual(devices[0].size, 1024000)
  211. self.assertEqual(devices[0].mode, 'w')
  212. self.assertEqual(devices[1].backend_domain, vm)
  213. self.assertEqual(devices[1].ident, 'sdb')
  214. self.assertEqual(devices[1].description, 'Test device2')
  215. self.assertEqual(devices[1].size, 2048000)
  216. self.assertEqual(devices[1].mode, 'r')
  217. def test_011_devices_list_empty(self):
  218. vm = TestVM({})
  219. devices = sorted(list(self.ext.on_device_list_block(vm, '')))
  220. self.assertEqual(len(devices), 0)
  221. def test_012_devices_list_invalid_ident(self):
  222. vm = TestVM({
  223. '/qubes-block-devices/invalid ident': b'',
  224. '/qubes-block-devices/invalid+ident': b'',
  225. '/qubes-block-devices/invalid#': b'',
  226. })
  227. devices = sorted(list(self.ext.on_device_list_block(vm, '')))
  228. self.assertEqual(len(devices), 0)
  229. msg = 'test-vm vm\'s device path name contains unsafe characters. '\
  230. 'Skipping it.'
  231. self.assertEqual(vm.log.warning.mock_calls, [
  232. mock.call(msg),
  233. mock.call(msg),
  234. mock.call(msg),
  235. ])
  236. def test_020_find_unused_frontend(self):
  237. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  238. frontend = self.ext.find_unused_frontend(vm)
  239. self.assertEqual(frontend, 'xvdi')
  240. def test_022_find_unused_frontend2(self):
  241. disk = '''
  242. <disk type="block" device="disk">
  243. <driver name="phy" />
  244. <source dev="/dev/sda" />
  245. <target dev="xvdi" />
  246. <readonly />
  247. <backenddomain name="sys-usb" />
  248. </disk>
  249. '''
  250. vm = TestVM({}, domain_xml=domain_xml_template.format(disk))
  251. frontend = self.ext.find_unused_frontend(vm)
  252. self.assertEqual(frontend, 'xvdj')
  253. def test_030_list_attached_empty(self):
  254. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  255. devices = sorted(list(self.ext.on_device_list_attached(vm, '')))
  256. self.assertEqual(len(devices), 0)
  257. def test_031_list_attached(self):
  258. disk = '''
  259. <disk type="block" device="disk">
  260. <driver name="phy" />
  261. <source dev="/dev/sda" />
  262. <target dev="xvdi" />
  263. <readonly />
  264. <backenddomain name="sys-usb" />
  265. </disk>
  266. '''
  267. vm = TestVM({}, domain_xml=domain_xml_template.format(disk))
  268. vm.app.domains['test-vm'] = vm
  269. vm.app.domains['sys-usb'] = TestVM({}, name='sys-usb')
  270. devices = sorted(list(self.ext.on_device_list_attached(vm, '')))
  271. self.assertEqual(len(devices), 1)
  272. dev = devices[0][0]
  273. options = devices[0][1]
  274. self.assertEqual(dev.backend_domain, vm.app.domains['sys-usb'])
  275. self.assertEqual(dev.ident, 'sda')
  276. self.assertEqual(options['frontend-dev'], 'xvdi')
  277. self.assertEqual(options['read-only'], 'yes')
  278. def test_032_list_attached_dom0(self):
  279. disk = '''
  280. <disk type="block" device="disk">
  281. <driver name="phy" />
  282. <source dev="/dev/sda" />
  283. <target dev="xvdi" />
  284. </disk>
  285. '''
  286. vm = TestVM({}, domain_xml=domain_xml_template.format(disk))
  287. vm.app.domains['test-vm'] = vm
  288. vm.app.domains['sys-usb'] = TestVM({}, name='sys-usb')
  289. vm.app.domains['dom0'] = TestVM({}, name='dom0')
  290. vm.app.domains[0] = vm.app.domains['dom0']
  291. devices = sorted(list(self.ext.on_device_list_attached(vm, '')))
  292. self.assertEqual(len(devices), 1)
  293. dev = devices[0][0]
  294. options = devices[0][1]
  295. self.assertEqual(dev.backend_domain, vm.app.domains['dom0'])
  296. self.assertEqual(dev.ident, 'sda')
  297. self.assertEqual(options['frontend-dev'], 'xvdi')
  298. self.assertEqual(options['read-only'], 'no')
  299. def test_040_attach(self):
  300. back_vm = TestVM(name='sys-usb', qdb={
  301. '/qubes-block-devices/sda': b'',
  302. '/qubes-block-devices/sda/desc': b'Test device',
  303. '/qubes-block-devices/sda/size': b'1024000',
  304. '/qubes-block-devices/sda/mode': b'w',
  305. })
  306. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  307. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  308. self.ext.on_device_pre_attached_block(vm, '', dev, {})
  309. device_xml = (
  310. '<disk type="block" device="disk">\n'
  311. ' <driver name="phy" />\n'
  312. ' <source dev="/dev/sda" />\n'
  313. ' <target dev="xvdi" />\n'
  314. ' <backenddomain name="sys-usb" />\n'
  315. '</disk>')
  316. vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml)
  317. def test_041_attach_frontend(self):
  318. back_vm = TestVM(name='sys-usb', qdb={
  319. '/qubes-block-devices/sda': b'',
  320. '/qubes-block-devices/sda/desc': b'Test device',
  321. '/qubes-block-devices/sda/size': b'1024000',
  322. '/qubes-block-devices/sda/mode': b'w',
  323. })
  324. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  325. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  326. self.ext.on_device_pre_attached_block(vm, '', dev,
  327. {'frontend-dev': 'xvdj'})
  328. device_xml = (
  329. '<disk type="block" device="disk">\n'
  330. ' <driver name="phy" />\n'
  331. ' <source dev="/dev/sda" />\n'
  332. ' <target dev="xvdj" />\n'
  333. ' <backenddomain name="sys-usb" />\n'
  334. '</disk>')
  335. vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml)
  336. def test_042_attach_read_only(self):
  337. back_vm = TestVM(name='sys-usb', qdb={
  338. '/qubes-block-devices/sda': b'',
  339. '/qubes-block-devices/sda/desc': b'Test device',
  340. '/qubes-block-devices/sda/size': b'1024000',
  341. '/qubes-block-devices/sda/mode': b'w',
  342. })
  343. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  344. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  345. self.ext.on_device_pre_attached_block(vm, '', dev,
  346. {'read-only': 'yes'})
  347. device_xml = (
  348. '<disk type="block" device="disk">\n'
  349. ' <driver name="phy" />\n'
  350. ' <source dev="/dev/sda" />\n'
  351. ' <target dev="xvdi" />\n'
  352. ' <readonly />\n'
  353. ' <backenddomain name="sys-usb" />\n'
  354. '</disk>')
  355. vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml)
  356. def test_043_attach_invalid_option(self):
  357. back_vm = TestVM(name='sys-usb', qdb={
  358. '/qubes-block-devices/sda': b'',
  359. '/qubes-block-devices/sda/desc': b'Test device',
  360. '/qubes-block-devices/sda/size': b'1024000',
  361. '/qubes-block-devices/sda/mode': b'w',
  362. })
  363. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  364. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  365. with self.assertRaises(qubes.exc.QubesValueError):
  366. self.ext.on_device_pre_attached_block(vm, '', dev,
  367. {'no-such-option': '123'})
  368. self.assertFalse(vm.libvirt_domain.attachDevice.called)
  369. def test_044_attach_invalid_option2(self):
  370. back_vm = TestVM(name='sys-usb', qdb={
  371. '/qubes-block-devices/sda': b'',
  372. '/qubes-block-devices/sda/desc': b'Test device',
  373. '/qubes-block-devices/sda/size': b'1024000',
  374. '/qubes-block-devices/sda/mode': b'w',
  375. })
  376. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  377. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  378. with self.assertRaises(qubes.exc.QubesValueError):
  379. self.ext.on_device_pre_attached_block(vm, '', dev,
  380. {'read-only': 'maybe'})
  381. self.assertFalse(vm.libvirt_domain.attachDevice.called)
  382. def test_045_attach_backend_not_running(self):
  383. back_vm = TestVM(name='sys-usb', running=False, qdb={
  384. '/qubes-block-devices/sda': b'',
  385. '/qubes-block-devices/sda/desc': b'Test device',
  386. '/qubes-block-devices/sda/size': b'1024000',
  387. '/qubes-block-devices/sda/mode': b'w',
  388. })
  389. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  390. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  391. with self.assertRaises(qubes.exc.QubesVMNotRunningError):
  392. self.ext.on_device_pre_attached_block(vm, '', dev, {})
  393. self.assertFalse(vm.libvirt_domain.attachDevice.called)
  394. def test_046_attach_ro_dev_rw(self):
  395. back_vm = TestVM(name='sys-usb', qdb={
  396. '/qubes-block-devices/sda': b'',
  397. '/qubes-block-devices/sda/desc': b'Test device',
  398. '/qubes-block-devices/sda/size': b'1024000',
  399. '/qubes-block-devices/sda/mode': b'r',
  400. })
  401. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  402. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  403. with self.assertRaises(qubes.exc.QubesValueError):
  404. self.ext.on_device_pre_attached_block(vm, '', dev,
  405. {'read-only': 'no'})
  406. self.assertFalse(vm.libvirt_domain.attachDevice.called)
  407. def test_047_attach_read_only_auto(self):
  408. back_vm = TestVM(name='sys-usb', qdb={
  409. '/qubes-block-devices/sda': b'',
  410. '/qubes-block-devices/sda/desc': b'Test device',
  411. '/qubes-block-devices/sda/size': b'1024000',
  412. '/qubes-block-devices/sda/mode': b'r',
  413. })
  414. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  415. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  416. self.ext.on_device_pre_attached_block(vm, '', dev, {})
  417. device_xml = (
  418. '<disk type="block" device="disk">\n'
  419. ' <driver name="phy" />\n'
  420. ' <source dev="/dev/sda" />\n'
  421. ' <target dev="xvdi" />\n'
  422. ' <readonly />\n'
  423. ' <backenddomain name="sys-usb" />\n'
  424. '</disk>')
  425. vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml)
  426. def test_050_detach(self):
  427. back_vm = TestVM(name='sys-usb', qdb={
  428. '/qubes-block-devices/sda': b'',
  429. '/qubes-block-devices/sda/desc': b'Test device',
  430. '/qubes-block-devices/sda/size': b'1024000',
  431. '/qubes-block-devices/sda/mode': b'r',
  432. })
  433. device_xml = (
  434. '<disk type="block" device="disk">\n'
  435. ' <driver name="phy" />\n'
  436. ' <source dev="/dev/sda" />\n'
  437. ' <target dev="xvdi" />\n'
  438. ' <readonly />\n'
  439. ' <backenddomain name="sys-usb" />\n'
  440. '</disk>')
  441. vm = TestVM({}, domain_xml=domain_xml_template.format(device_xml))
  442. vm.app.domains['test-vm'] = vm
  443. vm.app.domains['sys-usb'] = TestVM({}, name='sys-usb')
  444. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  445. self.ext.on_device_pre_detached_block(vm, '', dev)
  446. vm.libvirt_domain.detachDevice.assert_called_once_with(device_xml)
  447. def test_051_detach_not_attached(self):
  448. back_vm = TestVM(name='sys-usb', qdb={
  449. '/qubes-block-devices/sda': b'',
  450. '/qubes-block-devices/sda/desc': b'Test device',
  451. '/qubes-block-devices/sda/size': b'1024000',
  452. '/qubes-block-devices/sda/mode': b'r',
  453. })
  454. device_xml = (
  455. '<disk type="block" device="disk">\n'
  456. ' <driver name="phy" />\n'
  457. ' <source dev="/dev/sda" />\n'
  458. ' <target dev="xvdi" />\n'
  459. ' <readonly />\n\n'
  460. ' <backenddomain name="sys-usb" />\n'
  461. '</disk>')
  462. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  463. vm.app.domains['test-vm'] = vm
  464. vm.app.domains['sys-usb'] = TestVM({}, name='sys-usb')
  465. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  466. self.ext.on_device_pre_detached_block(vm, '', dev)
  467. self.assertFalse(vm.libvirt_domain.detachDevice.called)