devices_block.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  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. if isinstance(key, str):
  90. key = key.encode()
  91. return self._data.get(key, None)
  92. def list(self, prefix):
  93. if isinstance(prefix, str):
  94. prefix = prefix.encode()
  95. return [key for key in self._data if key.startswith(prefix)]
  96. class TestApp(object):
  97. def __init__(self):
  98. #: jinja2 environment for libvirt XML templates
  99. self.env = jinja2.Environment(
  100. loader=jinja2.FileSystemLoader([
  101. 'templates',
  102. ]),
  103. undefined=jinja2.StrictUndefined)
  104. self.domains = {}
  105. class TestVM(object):
  106. def __init__(self, qdb, domain_xml=None, running=True, name='test-vm'):
  107. self.name = name
  108. self.qdb = TestQubesDB(qdb)
  109. self.libvirt_domain = mock.Mock()
  110. self.is_running = lambda: running
  111. self.log = mock.Mock()
  112. self.app = TestApp()
  113. if domain_xml:
  114. self.libvirt_domain.configure_mock(**{
  115. 'XMLDesc.return_value': domain_xml
  116. })
  117. def __eq__(self, other):
  118. if isinstance(other, TestVM):
  119. return self.name == other.name
  120. class TC_00_Block(qubes.tests.QubesTestCase):
  121. def setUp(self):
  122. super().setUp()
  123. self.ext = qubes.ext.block.BlockDeviceExtension()
  124. def test_000_device_get(self):
  125. vm = TestVM({
  126. b'/qubes-block-devices/sda': b'',
  127. b'/qubes-block-devices/sda/desc': b'Test device',
  128. b'/qubes-block-devices/sda/size': b'1024000',
  129. b'/qubes-block-devices/sda/mode': b'w',
  130. })
  131. device_info = self.ext.device_get(vm, 'sda')
  132. self.assertIsInstance(device_info, qubes.ext.block.BlockDevice)
  133. self.assertEqual(device_info.backend_domain, vm)
  134. self.assertEqual(device_info.ident, 'sda')
  135. self.assertEqual(device_info.description, 'Test device')
  136. self.assertEqual(device_info._description, 'Test device')
  137. self.assertEqual(device_info.size, 1024000)
  138. self.assertEqual(device_info.mode, 'w')
  139. self.assertEqual(device_info.frontend_domain, None)
  140. self.assertEqual(device_info.device_node, '/dev/sda')
  141. def test_001_device_get_other_node(self):
  142. vm = TestVM({
  143. b'/qubes-block-devices/mapper_dmroot': b'',
  144. b'/qubes-block-devices/mapper_dmroot/desc': b'Test device',
  145. b'/qubes-block-devices/mapper_dmroot/size': b'1024000',
  146. b'/qubes-block-devices/mapper_dmroot/mode': b'w',
  147. })
  148. device_info = self.ext.device_get(vm, 'mapper_dmroot')
  149. self.assertIsInstance(device_info, qubes.ext.block.BlockDevice)
  150. self.assertEqual(device_info.backend_domain, vm)
  151. self.assertEqual(device_info.ident, 'mapper_dmroot')
  152. self.assertEqual(device_info.description, 'Test device')
  153. self.assertEqual(device_info._description, 'Test device')
  154. self.assertEqual(device_info.size, 1024000)
  155. self.assertEqual(device_info.mode, 'w')
  156. self.assertEqual(device_info.frontend_domain, None)
  157. self.assertEqual(device_info.device_node, '/dev/mapper/dmroot')
  158. def test_002_device_get_invalid_desc(self):
  159. vm = TestVM({
  160. b'/qubes-block-devices/sda': b'',
  161. b'/qubes-block-devices/sda/desc': b'Test device<>za\xc4\x87abc',
  162. b'/qubes-block-devices/sda/size': b'1024000',
  163. b'/qubes-block-devices/sda/mode': b'w',
  164. })
  165. device_info = self.ext.device_get(vm, 'sda')
  166. self.assertEqual(device_info.description, 'Test device__za__abc')
  167. def test_003_device_get_invalid_size(self):
  168. vm = TestVM({
  169. b'/qubes-block-devices/sda': b'',
  170. b'/qubes-block-devices/sda/desc': b'Test device',
  171. b'/qubes-block-devices/sda/size': b'1024000abc',
  172. b'/qubes-block-devices/sda/mode': b'w',
  173. })
  174. device_info = self.ext.device_get(vm, 'sda')
  175. self.assertEqual(device_info.size, 0)
  176. vm.log.warning.assert_called_once_with('Device sda has invalid size')
  177. def test_004_device_get_invalid_mode(self):
  178. vm = TestVM({
  179. b'/qubes-block-devices/sda': b'',
  180. b'/qubes-block-devices/sda/desc': b'Test device',
  181. b'/qubes-block-devices/sda/size': b'1024000',
  182. b'/qubes-block-devices/sda/mode': b'abc',
  183. })
  184. device_info = self.ext.device_get(vm, 'sda')
  185. self.assertEqual(device_info.mode, 'w')
  186. vm.log.warning.assert_called_once_with('Device sda has invalid mode')
  187. def test_005_device_get_none(self):
  188. vm = TestVM({
  189. b'/qubes-block-devices/sda': b'',
  190. b'/qubes-block-devices/sda/desc': b'Test device',
  191. b'/qubes-block-devices/sda/size': b'1024000',
  192. b'/qubes-block-devices/sda/mode': b'w',
  193. })
  194. device_info = self.ext.device_get(vm, 'sdb')
  195. self.assertIsNone(device_info)
  196. def test_010_devices_list(self):
  197. vm = TestVM({
  198. b'/qubes-block-devices/sda': b'',
  199. b'/qubes-block-devices/sda/desc': b'Test device',
  200. b'/qubes-block-devices/sda/size': b'1024000',
  201. b'/qubes-block-devices/sda/mode': b'w',
  202. b'/qubes-block-devices/sdb': b'',
  203. b'/qubes-block-devices/sdb/desc': b'Test device2',
  204. b'/qubes-block-devices/sdb/size': b'2048000',
  205. b'/qubes-block-devices/sdb/mode': b'r',
  206. })
  207. devices = sorted(list(self.ext.on_device_list_block(vm, '')))
  208. self.assertEqual(len(devices), 2)
  209. self.assertEqual(devices[0].backend_domain, vm)
  210. self.assertEqual(devices[0].ident, 'sda')
  211. self.assertEqual(devices[0].description, 'Test device')
  212. self.assertEqual(devices[0].size, 1024000)
  213. self.assertEqual(devices[0].mode, 'w')
  214. self.assertEqual(devices[1].backend_domain, vm)
  215. self.assertEqual(devices[1].ident, 'sdb')
  216. self.assertEqual(devices[1].description, 'Test device2')
  217. self.assertEqual(devices[1].size, 2048000)
  218. self.assertEqual(devices[1].mode, 'r')
  219. def test_011_devices_list_empty(self):
  220. vm = TestVM({})
  221. devices = sorted(list(self.ext.on_device_list_block(vm, '')))
  222. self.assertEqual(len(devices), 0)
  223. def test_012_devices_list_invalid_ident(self):
  224. vm = TestVM({
  225. b'/qubes-block-devices/invalid ident': b'',
  226. b'/qubes-block-devices/invalid+ident': b'',
  227. b'/qubes-block-devices/invalid#': b'',
  228. })
  229. devices = sorted(list(self.ext.on_device_list_block(vm, '')))
  230. self.assertEqual(len(devices), 0)
  231. msg = 'test-vm vm\'s device path name contains unsafe characters. '\
  232. 'Skipping it.'
  233. self.assertEqual(vm.log.warning.mock_calls, [
  234. mock.call(msg),
  235. mock.call(msg),
  236. mock.call(msg),
  237. ])
  238. def test_020_find_unused_frontend(self):
  239. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  240. frontend = self.ext.find_unused_frontend(vm)
  241. self.assertEqual(frontend, 'xvdi')
  242. def test_022_find_unused_frontend2(self):
  243. disk = '''
  244. <disk type="block" device="disk">
  245. <driver name="phy" />
  246. <source dev="/dev/sda" />
  247. <target dev="xvdi" />
  248. <readonly />
  249. <backenddomain name="sys-usb" />
  250. </disk>
  251. '''
  252. vm = TestVM({}, domain_xml=domain_xml_template.format(disk))
  253. frontend = self.ext.find_unused_frontend(vm)
  254. self.assertEqual(frontend, 'xvdj')
  255. def test_030_list_attached_empty(self):
  256. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  257. devices = sorted(list(self.ext.on_device_list_attached(vm, '')))
  258. self.assertEqual(len(devices), 0)
  259. def test_031_list_attached(self):
  260. disk = '''
  261. <disk type="block" device="disk">
  262. <driver name="phy" />
  263. <source dev="/dev/sda" />
  264. <target dev="xvdi" />
  265. <readonly />
  266. <backenddomain name="sys-usb" />
  267. </disk>
  268. '''
  269. vm = TestVM({}, domain_xml=domain_xml_template.format(disk))
  270. vm.app.domains['test-vm'] = vm
  271. vm.app.domains['sys-usb'] = TestVM({}, name='sys-usb')
  272. devices = sorted(list(self.ext.on_device_list_attached(vm, '')))
  273. self.assertEqual(len(devices), 1)
  274. dev = devices[0][0]
  275. options = devices[0][1]
  276. self.assertEqual(dev.backend_domain, vm.app.domains['sys-usb'])
  277. self.assertEqual(dev.ident, 'sda')
  278. self.assertEqual(options['frontend-dev'], 'xvdi')
  279. self.assertEqual(options['read-only'], 'yes')
  280. def test_032_list_attached_dom0(self):
  281. disk = '''
  282. <disk type="block" device="disk">
  283. <driver name="phy" />
  284. <source dev="/dev/sda" />
  285. <target dev="xvdi" />
  286. </disk>
  287. '''
  288. vm = TestVM({}, domain_xml=domain_xml_template.format(disk))
  289. vm.app.domains['test-vm'] = vm
  290. vm.app.domains['sys-usb'] = TestVM({}, name='sys-usb')
  291. vm.app.domains['dom0'] = TestVM({}, name='dom0')
  292. vm.app.domains[0] = vm.app.domains['dom0']
  293. devices = sorted(list(self.ext.on_device_list_attached(vm, '')))
  294. self.assertEqual(len(devices), 1)
  295. dev = devices[0][0]
  296. options = devices[0][1]
  297. self.assertEqual(dev.backend_domain, vm.app.domains['dom0'])
  298. self.assertEqual(dev.ident, 'sda')
  299. self.assertEqual(options['frontend-dev'], 'xvdi')
  300. self.assertEqual(options['read-only'], 'no')
  301. def test_040_attach(self):
  302. back_vm = TestVM(name='sys-usb', qdb={
  303. b'/qubes-block-devices/sda': b'',
  304. b'/qubes-block-devices/sda/desc': b'Test device',
  305. b'/qubes-block-devices/sda/size': b'1024000',
  306. b'/qubes-block-devices/sda/mode': b'w',
  307. })
  308. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  309. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  310. self.ext.on_device_pre_attached_block(vm, '', dev, {})
  311. device_xml = (
  312. '<disk type="block" device="disk">\n'
  313. ' <driver name="phy" />\n'
  314. ' <source dev="/dev/sda" />\n'
  315. ' <target dev="xvdi" />\n'
  316. '\n'
  317. ' <backenddomain name="sys-usb" />\n'
  318. '</disk>')
  319. vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml)
  320. def test_041_attach_frontend(self):
  321. back_vm = TestVM(name='sys-usb', qdb={
  322. b'/qubes-block-devices/sda': b'',
  323. b'/qubes-block-devices/sda/desc': b'Test device',
  324. b'/qubes-block-devices/sda/size': b'1024000',
  325. b'/qubes-block-devices/sda/mode': b'w',
  326. })
  327. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  328. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  329. self.ext.on_device_pre_attached_block(vm, '', dev,
  330. {'frontend-dev': 'xvdj'})
  331. device_xml = (
  332. '<disk type="block" device="disk">\n'
  333. ' <driver name="phy" />\n'
  334. ' <source dev="/dev/sda" />\n'
  335. ' <target dev="xvdj" />\n'
  336. '\n'
  337. ' <backenddomain name="sys-usb" />\n'
  338. '</disk>')
  339. vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml)
  340. def test_042_attach_read_only(self):
  341. back_vm = TestVM(name='sys-usb', qdb={
  342. b'/qubes-block-devices/sda': b'',
  343. b'/qubes-block-devices/sda/desc': b'Test device',
  344. b'/qubes-block-devices/sda/size': b'1024000',
  345. b'/qubes-block-devices/sda/mode': b'w',
  346. })
  347. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  348. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  349. self.ext.on_device_pre_attached_block(vm, '', dev,
  350. {'read-only': 'yes'})
  351. device_xml = (
  352. '<disk type="block" device="disk">\n'
  353. ' <driver name="phy" />\n'
  354. ' <source dev="/dev/sda" />\n'
  355. ' <target dev="xvdi" />\n'
  356. ' <readonly />\n\n'
  357. ' <backenddomain name="sys-usb" />\n'
  358. '</disk>')
  359. vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml)
  360. def test_043_attach_invalid_option(self):
  361. back_vm = TestVM(name='sys-usb', qdb={
  362. b'/qubes-block-devices/sda': b'',
  363. b'/qubes-block-devices/sda/desc': b'Test device',
  364. b'/qubes-block-devices/sda/size': b'1024000',
  365. b'/qubes-block-devices/sda/mode': b'w',
  366. })
  367. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  368. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  369. with self.assertRaises(qubes.exc.QubesValueError):
  370. self.ext.on_device_pre_attached_block(vm, '', dev,
  371. {'no-such-option': '123'})
  372. self.assertFalse(vm.libvirt_domain.attachDevice.called)
  373. def test_044_attach_invalid_option2(self):
  374. back_vm = TestVM(name='sys-usb', qdb={
  375. b'/qubes-block-devices/sda': b'',
  376. b'/qubes-block-devices/sda/desc': b'Test device',
  377. b'/qubes-block-devices/sda/size': b'1024000',
  378. b'/qubes-block-devices/sda/mode': b'w',
  379. })
  380. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  381. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  382. with self.assertRaises(qubes.exc.QubesValueError):
  383. self.ext.on_device_pre_attached_block(vm, '', dev,
  384. {'read-only': 'maybe'})
  385. self.assertFalse(vm.libvirt_domain.attachDevice.called)
  386. def test_045_attach_backend_not_running(self):
  387. back_vm = TestVM(name='sys-usb', running=False, qdb={
  388. b'/qubes-block-devices/sda': b'',
  389. b'/qubes-block-devices/sda/desc': b'Test device',
  390. b'/qubes-block-devices/sda/size': b'1024000',
  391. b'/qubes-block-devices/sda/mode': b'w',
  392. })
  393. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  394. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  395. with self.assertRaises(qubes.exc.QubesVMNotRunningError):
  396. self.ext.on_device_pre_attached_block(vm, '', dev, {})
  397. self.assertFalse(vm.libvirt_domain.attachDevice.called)
  398. def test_046_attach_ro_dev_rw(self):
  399. back_vm = TestVM(name='sys-usb', qdb={
  400. b'/qubes-block-devices/sda': b'',
  401. b'/qubes-block-devices/sda/desc': b'Test device',
  402. b'/qubes-block-devices/sda/size': b'1024000',
  403. b'/qubes-block-devices/sda/mode': b'r',
  404. })
  405. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  406. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  407. with self.assertRaises(qubes.exc.QubesValueError):
  408. self.ext.on_device_pre_attached_block(vm, '', dev,
  409. {'read-only': 'no'})
  410. self.assertFalse(vm.libvirt_domain.attachDevice.called)
  411. def test_047_attach_read_only_auto(self):
  412. back_vm = TestVM(name='sys-usb', qdb={
  413. b'/qubes-block-devices/sda': b'',
  414. b'/qubes-block-devices/sda/desc': b'Test device',
  415. b'/qubes-block-devices/sda/size': b'1024000',
  416. b'/qubes-block-devices/sda/mode': b'r',
  417. })
  418. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  419. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  420. self.ext.on_device_pre_attached_block(vm, '', dev, {})
  421. device_xml = (
  422. '<disk type="block" device="disk">\n'
  423. ' <driver name="phy" />\n'
  424. ' <source dev="/dev/sda" />\n'
  425. ' <target dev="xvdi" />\n'
  426. ' <readonly />\n\n'
  427. ' <backenddomain name="sys-usb" />\n'
  428. '</disk>')
  429. vm.libvirt_domain.attachDevice.assert_called_once_with(device_xml)
  430. def test_050_detach(self):
  431. back_vm = TestVM(name='sys-usb', qdb={
  432. b'/qubes-block-devices/sda': b'',
  433. b'/qubes-block-devices/sda/desc': b'Test device',
  434. b'/qubes-block-devices/sda/size': b'1024000',
  435. b'/qubes-block-devices/sda/mode': b'r',
  436. })
  437. device_xml = (
  438. '<disk type="block" device="disk">\n'
  439. ' <driver name="phy" />\n'
  440. ' <source dev="/dev/sda" />\n'
  441. ' <target dev="xvdi" />\n'
  442. ' <readonly />\n\n'
  443. ' <backenddomain name="sys-usb" />\n'
  444. '</disk>')
  445. vm = TestVM({}, domain_xml=domain_xml_template.format(device_xml))
  446. vm.app.domains['test-vm'] = vm
  447. vm.app.domains['sys-usb'] = TestVM({}, name='sys-usb')
  448. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  449. self.ext.on_device_pre_detached_block(vm, '', dev)
  450. vm.libvirt_domain.detachDevice.assert_called_once_with(device_xml)
  451. def test_051_detach_not_attached(self):
  452. back_vm = TestVM(name='sys-usb', qdb={
  453. b'/qubes-block-devices/sda': b'',
  454. b'/qubes-block-devices/sda/desc': b'Test device',
  455. b'/qubes-block-devices/sda/size': b'1024000',
  456. b'/qubes-block-devices/sda/mode': b'r',
  457. })
  458. device_xml = (
  459. '<disk type="block" device="disk">\n'
  460. ' <driver name="phy" />\n'
  461. ' <source dev="/dev/sda" />\n'
  462. ' <target dev="xvdi" />\n'
  463. ' <readonly />\n\n'
  464. ' <backenddomain name="sys-usb" />\n'
  465. '</disk>')
  466. vm = TestVM({}, domain_xml=domain_xml_template.format(''))
  467. vm.app.domains['test-vm'] = vm
  468. vm.app.domains['sys-usb'] = TestVM({}, name='sys-usb')
  469. dev = qubes.ext.block.BlockDevice(back_vm, 'sda')
  470. self.ext.on_device_pre_detached_block(vm, '', dev)
  471. self.assertFalse(vm.libvirt_domain.detachDevice.called)