storage_lvm.py 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042
  1. #
  2. # The Qubes OS Project, http://www.qubes-os.org
  3. #
  4. # Copyright (C) 2016 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
  5. #
  6. # This library is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU Lesser General Public
  8. # License as published by the Free Software Foundation; either
  9. # version 2.1 of the License, or (at your option) any later version.
  10. #
  11. # This library is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. # Lesser General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Lesser General Public
  17. # License along with this library; if not, see <https://www.gnu.org/licenses/>.
  18. #
  19. ''' Tests for lvm storage driver. By default tests are going to use the
  20. 'qubes_dom0/pool00'. An alternative LVM thin pool may be provided via
  21. :envvar:`DEFAULT_LVM_POOL` shell variable.
  22. Any pool variables prefixed with 'LVM_' or 'lvm_' represent a LVM
  23. 'volume_group/thin_pool' combination. Pool variables without a prefix
  24. represent a :py:class:`qubes.storage.lvm.ThinPool`.
  25. '''
  26. import os
  27. import subprocess
  28. import tempfile
  29. import unittest
  30. import unittest.mock
  31. import qubes.tests
  32. import qubes.storage
  33. from qubes.storage.lvm import ThinPool, ThinVolume, qubes_lvm
  34. if 'DEFAULT_LVM_POOL' in os.environ.keys():
  35. DEFAULT_LVM_POOL = os.environ['DEFAULT_LVM_POOL']
  36. else:
  37. DEFAULT_LVM_POOL = 'qubes_dom0/pool00'
  38. def lvm_pool_exists(volume_group, thin_pool):
  39. ''' Returns ``True`` if thin pool exists in the volume group. '''
  40. path = "/dev/{!s}/{!s}".format(volume_group, thin_pool)
  41. return os.path.exists(path)
  42. def skipUnlessLvmPoolExists(test_item): # pylint: disable=invalid-name
  43. ''' Decorator that skips LVM tests if the default pool is missing. '''
  44. volume_group, thin_pool = DEFAULT_LVM_POOL.split('/', 1)
  45. result = lvm_pool_exists(volume_group, thin_pool)
  46. msg = 'LVM thin pool {!r} does not exist'.format(DEFAULT_LVM_POOL)
  47. return unittest.skipUnless(result, msg)(test_item)
  48. POOL_CONF = {'name': 'test-lvm',
  49. 'driver': 'lvm_thin',
  50. 'volume_group': DEFAULT_LVM_POOL.split('/')[0],
  51. 'thin_pool': DEFAULT_LVM_POOL.split('/')[1]}
  52. class ThinPoolBase(qubes.tests.QubesTestCase):
  53. ''' Sanity tests for :py:class:`qubes.storage.lvm.ThinPool` '''
  54. created_pool = False
  55. def setUp(self):
  56. super(ThinPoolBase, self).setUp()
  57. volume_group, thin_pool = DEFAULT_LVM_POOL.split('/', 1)
  58. self.pool = self._find_pool(volume_group, thin_pool)
  59. if not self.pool:
  60. self.pool = self.app.add_pool(**POOL_CONF)
  61. self.created_pool = True
  62. def tearDown(self):
  63. ''' Remove the default lvm pool if it was created only for this test '''
  64. if self.created_pool:
  65. self.app.remove_pool(self.pool.name)
  66. super(ThinPoolBase, self).tearDown()
  67. def _find_pool(self, volume_group, thin_pool):
  68. ''' Returns the pool matching the specified ``volume_group`` &
  69. ``thin_pool``, or None.
  70. '''
  71. pools = [p for p in self.app.pools.values()
  72. if issubclass(p.__class__, ThinPool)]
  73. for pool in pools:
  74. if pool.volume_group == volume_group \
  75. and pool.thin_pool == thin_pool:
  76. return pool
  77. return None
  78. @skipUnlessLvmPoolExists
  79. class TC_00_ThinPool(ThinPoolBase):
  80. ''' Sanity tests for :py:class:`qubes.storage.lvm.ThinPool` '''
  81. def setUp(self):
  82. xml_path = '/tmp/qubes-test.xml'
  83. self.app = qubes.Qubes.create_empty_store(store=xml_path,
  84. clockvm=None,
  85. updatevm=None,
  86. offline_mode=True,
  87. )
  88. os.environ['QUBES_XML_PATH'] = xml_path
  89. super(TC_00_ThinPool, self).setUp()
  90. def tearDown(self):
  91. super(TC_00_ThinPool, self).tearDown()
  92. os.unlink(self.app.store)
  93. del self.app
  94. for attr in dir(self):
  95. if isinstance(getattr(self, attr), qubes.vm.BaseVM):
  96. delattr(self, attr)
  97. def test_000_default_thin_pool(self):
  98. ''' Check whether :py:data`DEFAULT_LVM_POOL` exists. This pool is
  99. created by default, if at installation time LVM + Thin was chosen.
  100. '''
  101. msg = 'Thin pool {!r} does not exist'.format(DEFAULT_LVM_POOL)
  102. self.assertTrue(self.pool, msg)
  103. def test_001_origin_volume(self):
  104. ''' Test origin volume creation '''
  105. config = {
  106. 'name': 'root',
  107. 'pool': self.pool.name,
  108. 'save_on_stop': True,
  109. 'rw': True,
  110. 'size': qubes.config.defaults['root_img_size'],
  111. }
  112. vm = qubes.tests.storage.TestVM(self)
  113. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  114. self.assertIsInstance(volume, ThinVolume)
  115. self.assertEqual(volume.name, 'root')
  116. self.assertEqual(volume.pool, self.pool.name)
  117. self.assertEqual(volume.size, qubes.config.defaults['root_img_size'])
  118. self.loop.run_until_complete(volume.create())
  119. path = "/dev/%s" % volume.vid
  120. self.assertTrue(os.path.exists(path), path)
  121. self.loop.run_until_complete(volume.remove())
  122. def test_003_read_write_volume(self):
  123. ''' Test read-write volume creation '''
  124. config = {
  125. 'name': 'root',
  126. 'pool': self.pool.name,
  127. 'rw': True,
  128. 'save_on_stop': True,
  129. 'size': qubes.config.defaults['root_img_size'],
  130. }
  131. vm = qubes.tests.storage.TestVM(self)
  132. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  133. self.assertIsInstance(volume, ThinVolume)
  134. self.assertEqual(volume.name, 'root')
  135. self.assertEqual(volume.pool, self.pool.name)
  136. self.assertEqual(volume.size, qubes.config.defaults['root_img_size'])
  137. self.loop.run_until_complete(volume.create())
  138. path = "/dev/%s" % volume.vid
  139. self.assertTrue(os.path.exists(path), path)
  140. self.loop.run_until_complete(volume.remove())
  141. def test_004_size(self):
  142. with self.assertNotRaises(NotImplementedError):
  143. size = self.pool.size
  144. environ = os.environ.copy()
  145. environ['LC_ALL'] = 'C.utf8'
  146. pool_size = subprocess.check_output(['sudo', 'lvs', '--noheadings',
  147. '-o', 'lv_size',
  148. '--units', 'b', self.pool.volume_group + '/' + self.pool.thin_pool],
  149. env=environ)
  150. self.assertEqual(size, int(pool_size.strip()[:-1]))
  151. def test_005_usage(self):
  152. with self.assertNotRaises(NotImplementedError):
  153. usage = self.pool.usage
  154. environ = os.environ.copy()
  155. environ['LC_ALL'] = 'C.utf8'
  156. pool_info = subprocess.check_output(['sudo', 'lvs', '--noheadings',
  157. '-o', 'lv_size,data_percent',
  158. '--units', 'b', self.pool.volume_group + '/' + self.pool.thin_pool],
  159. env=environ)
  160. pool_size, pool_usage = pool_info.strip().split()
  161. pool_size = int(pool_size[:-1])
  162. pool_usage = float(pool_usage)
  163. self.assertEqual(usage, int(pool_size * pool_usage / 100))
  164. def _get_size(self, path):
  165. if os.getuid() != 0:
  166. return int(
  167. subprocess.check_output(
  168. ['sudo', 'blockdev', '--getsize64', path]))
  169. fd = os.open(path, os.O_RDONLY)
  170. try:
  171. return os.lseek(fd, 0, os.SEEK_END)
  172. finally:
  173. os.close(fd)
  174. def test_006_resize(self):
  175. config = {
  176. 'name': 'root',
  177. 'pool': self.pool.name,
  178. 'rw': True,
  179. 'save_on_stop': True,
  180. 'size': 32 * 1024**2,
  181. }
  182. vm = qubes.tests.storage.TestVM(self)
  183. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  184. self.loop.run_until_complete(volume.create())
  185. self.addCleanup(self.loop.run_until_complete, volume.remove())
  186. path = "/dev/%s" % volume.vid
  187. new_size = 64 * 1024 ** 2
  188. self.loop.run_until_complete(volume.resize(new_size))
  189. self.assertEqual(self._get_size(path), new_size)
  190. self.assertEqual(volume.size, new_size)
  191. def test_007_resize_running(self):
  192. old_size = 32 * 1024**2
  193. config = {
  194. 'name': 'root',
  195. 'pool': self.pool.name,
  196. 'rw': True,
  197. 'save_on_stop': True,
  198. 'size': old_size,
  199. }
  200. vm = qubes.tests.storage.TestVM(self)
  201. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  202. self.loop.run_until_complete(volume.create())
  203. self.addCleanup(self.loop.run_until_complete, volume.remove())
  204. self.loop.run_until_complete(volume.start())
  205. path = "/dev/%s" % volume.vid
  206. path2 = "/dev/%s" % volume._vid_snap
  207. new_size = 64 * 1024 ** 2
  208. self.loop.run_until_complete(volume.resize(new_size))
  209. self.assertEqual(self._get_size(path), old_size)
  210. self.assertEqual(self._get_size(path2), new_size)
  211. self.assertEqual(volume.size, new_size)
  212. self.loop.run_until_complete(volume.stop())
  213. self.assertEqual(self._get_size(path), new_size)
  214. self.assertEqual(volume.size, new_size)
  215. def _get_lv_uuid(self, lv):
  216. sudo = [] if os.getuid() == 0 else ['sudo']
  217. lvs_output = subprocess.check_output(
  218. sudo + ['lvs', '--noheadings', '-o', 'lv_uuid', lv])
  219. return lvs_output.strip()
  220. def _get_lv_origin_uuid(self, lv):
  221. sudo = [] if os.getuid() == 0 else ['sudo']
  222. if qubes.storage.lvm.lvm_is_very_old:
  223. # no support for origin_uuid directly
  224. lvs_output = subprocess.check_output(
  225. sudo + ['lvs', '--noheadings', '-o', 'origin', lv])
  226. lvs_output = subprocess.check_output(
  227. sudo + ['lvs', '--noheadings', '-o', 'lv_uuid',
  228. lv.rsplit('/', 1)[0] + '/' + lvs_output.strip().decode()])
  229. else:
  230. lvs_output = subprocess.check_output(
  231. sudo + ['lvs', '--noheadings', '-o', 'origin_uuid', lv])
  232. return lvs_output.strip()
  233. def test_008_commit(self):
  234. ''' Test volume changes commit'''
  235. config = {
  236. 'name': 'root',
  237. 'pool': self.pool.name,
  238. 'save_on_stop': True,
  239. 'rw': True,
  240. 'size': qubes.config.defaults['root_img_size'],
  241. }
  242. vm = qubes.tests.storage.TestVM(self)
  243. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  244. self.loop.run_until_complete(volume.create())
  245. path_snap = '/dev/' + volume._vid_snap
  246. self.assertFalse(os.path.exists(path_snap), path_snap)
  247. origin_uuid = self._get_lv_uuid(volume.path)
  248. self.loop.run_until_complete(volume.start())
  249. snap_uuid = self._get_lv_uuid(path_snap)
  250. self.assertNotEqual(origin_uuid, snap_uuid)
  251. path = volume.path
  252. self.assertTrue(path.startswith('/dev/' + volume.vid),
  253. '{} does not start with /dev/{}'.format(path, volume.vid))
  254. self.assertTrue(os.path.exists(path), path)
  255. self.loop.run_until_complete(volume.remove())
  256. def test_009_interrupted_commit(self):
  257. ''' Test volume changes commit'''
  258. config = {
  259. 'name': 'root',
  260. 'pool': self.pool.name,
  261. 'save_on_stop': True,
  262. 'rw': True,
  263. 'revisions_to_keep': 2,
  264. 'size': qubes.config.defaults['root_img_size'],
  265. }
  266. vm = qubes.tests.storage.TestVM(self)
  267. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  268. # mock logging, to not interfere with time.time() mock
  269. volume.log = unittest.mock.Mock()
  270. # do not call volume.create(), do it manually to simulate
  271. # interrupted commit
  272. revisions = ['-1521065904-back', '-1521065905-back', '-snap']
  273. orig_uuids = {}
  274. for rev in revisions:
  275. cmd = ['create', self.pool._pool_id,
  276. volume.vid.split('/')[1] + rev, str(config['size'])]
  277. qubes_lvm(cmd)
  278. orig_uuids[rev] = self._get_lv_uuid(volume.vid + rev)
  279. qubes.storage.lvm.reset_cache()
  280. path_snap = '/dev/' + volume._vid_snap
  281. self.assertTrue(volume.is_dirty())
  282. self.assertEqual(volume.path,
  283. '/dev/' + volume.vid + revisions[1])
  284. expected_revisions = {
  285. revisions[0].lstrip('-'): '2018-03-14T22:18:24',
  286. revisions[1].lstrip('-'): '2018-03-14T22:18:25',
  287. }
  288. self.assertEqual(volume.revisions, expected_revisions)
  289. self.loop.run_until_complete(volume.start())
  290. self.assertEqual(volume.revisions, expected_revisions)
  291. snap_uuid = self._get_lv_uuid(path_snap)
  292. self.assertEqual(orig_uuids['-snap'], snap_uuid)
  293. self.assertTrue(volume.is_dirty())
  294. self.assertEqual(volume.path,
  295. '/dev/' + volume.vid + revisions[1])
  296. with unittest.mock.patch('time.time') as mock_time:
  297. mock_time.side_effect = [521065906]
  298. self.loop.run_until_complete(volume.stop())
  299. expected_revisions = {
  300. revisions[0].lstrip('-'): '2018-03-14T22:18:24',
  301. revisions[1].lstrip('-'): '2018-03-14T22:18:25',
  302. }
  303. self.assertFalse(volume.is_dirty())
  304. self.assertEqual(volume.revisions, expected_revisions)
  305. self.assertEqual(volume.path, '/dev/' + volume.vid)
  306. self.assertEqual(snap_uuid, self._get_lv_uuid(volume.path))
  307. self.assertFalse(os.path.exists(path_snap), path_snap)
  308. self.loop.run_until_complete(volume.remove())
  309. def test_010_migration1(self):
  310. '''Start with old revisions, then start interacting using new code'''
  311. config = {
  312. 'name': 'root',
  313. 'pool': self.pool.name,
  314. 'save_on_stop': True,
  315. 'rw': True,
  316. 'revisions_to_keep': 2,
  317. 'size': qubes.config.defaults['root_img_size'],
  318. }
  319. vm = qubes.tests.storage.TestVM(self)
  320. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  321. # mock logging, to not interfere with time.time() mock
  322. volume.log = unittest.mock.Mock()
  323. # do not call volume.create(), do it manually to have old LV naming
  324. revisions = ['', '-1521065904-back', '-1521065905-back']
  325. orig_uuids = {}
  326. for rev in revisions:
  327. cmd = ['create', self.pool._pool_id,
  328. volume.vid.split('/')[1] + rev, str(config['size'])]
  329. qubes_lvm(cmd)
  330. orig_uuids[rev] = self._get_lv_uuid(volume.vid + rev)
  331. qubes.storage.lvm.reset_cache()
  332. path_snap = '/dev/' + volume._vid_snap
  333. self.assertFalse(os.path.exists(path_snap), path_snap)
  334. expected_revisions = {
  335. revisions[1].lstrip('-'): '2018-03-14T22:18:24',
  336. revisions[2].lstrip('-'): '2018-03-14T22:18:25',
  337. }
  338. self.assertEqual(volume.revisions, expected_revisions)
  339. self.assertEqual(volume.path, '/dev/' + volume.vid)
  340. self.loop.run_until_complete(volume.start())
  341. snap_uuid = self._get_lv_uuid(path_snap)
  342. self.assertNotEqual(orig_uuids[''], snap_uuid)
  343. snap_origin_uuid = self._get_lv_origin_uuid(path_snap)
  344. self.assertEqual(orig_uuids[''], snap_origin_uuid)
  345. path = volume.path
  346. self.assertEqual(path, '/dev/' + volume.vid)
  347. self.assertTrue(os.path.exists(path), path)
  348. with unittest.mock.patch('time.time') as mock_time:
  349. mock_time.side_effect = ('1521065906', '1521065907')
  350. self.loop.run_until_complete(volume.stop())
  351. revisions.extend(['-1521065906-back'])
  352. expected_revisions = {
  353. revisions[2].lstrip('-'): '2018-03-14T22:18:25',
  354. revisions[3].lstrip('-'): '2018-03-14T22:18:26',
  355. }
  356. self.assertEqual(volume.revisions, expected_revisions)
  357. self.assertEqual(volume.path, '/dev/' + volume.vid)
  358. path_snap = '/dev/' + volume._vid_snap
  359. self.assertFalse(os.path.exists(path_snap), path_snap)
  360. self.assertTrue(os.path.exists('/dev/' + volume.vid))
  361. self.assertEqual(self._get_lv_uuid(volume.path), snap_uuid)
  362. prev_path = '/dev/' + volume.vid + revisions[3]
  363. self.assertEqual(self._get_lv_uuid(prev_path), orig_uuids[''])
  364. self.loop.run_until_complete(volume.remove())
  365. for rev in revisions:
  366. path = '/dev/' + volume.vid + rev
  367. self.assertFalse(os.path.exists(path), path)
  368. def test_011_migration2(self):
  369. '''VM started with old code, stopped with new'''
  370. config = {
  371. 'name': 'root',
  372. 'pool': self.pool.name,
  373. 'save_on_stop': True,
  374. 'rw': True,
  375. 'revisions_to_keep': 1,
  376. 'size': qubes.config.defaults['root_img_size'],
  377. }
  378. vm = qubes.tests.storage.TestVM(self)
  379. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  380. # mock logging, to not interfere with time.time() mock
  381. volume.log = unittest.mock.Mock()
  382. # do not call volume.create(), do it manually to have old LV naming
  383. revisions = ['', '-snap']
  384. orig_uuids = {}
  385. for rev in revisions:
  386. cmd = ['create', self.pool._pool_id,
  387. volume.vid.split('/')[1] + rev, str(config['size'])]
  388. qubes_lvm(cmd)
  389. orig_uuids[rev] = self._get_lv_uuid(volume.vid + rev)
  390. qubes.storage.lvm.reset_cache()
  391. path_snap = '/dev/' + volume._vid_snap
  392. self.assertTrue(os.path.exists(path_snap), path_snap)
  393. expected_revisions = {}
  394. self.assertEqual(volume.revisions, expected_revisions)
  395. self.assertEqual(volume.path, '/dev/' + volume.vid)
  396. self.assertTrue(volume.is_dirty())
  397. path = volume.path
  398. self.assertEqual(path, '/dev/' + volume.vid)
  399. self.assertTrue(os.path.exists(path), path)
  400. with unittest.mock.patch('time.time') as mock_time:
  401. mock_time.side_effect = ('1521065906', '1521065907')
  402. self.loop.run_until_complete(volume.stop())
  403. revisions.extend(['-1521065906-back'])
  404. expected_revisions = {
  405. revisions[2].lstrip('-'): '2018-03-14T22:18:26',
  406. }
  407. self.assertEqual(volume.revisions, expected_revisions)
  408. self.assertEqual(volume.path, '/dev/' + volume.vid)
  409. path_snap = '/dev/' + volume._vid_snap
  410. self.assertFalse(os.path.exists(path_snap), path_snap)
  411. self.assertTrue(os.path.exists('/dev/' + volume.vid))
  412. self.assertEqual(self._get_lv_uuid(volume.path), orig_uuids['-snap'])
  413. prev_path = '/dev/' + volume.vid + revisions[2]
  414. self.assertEqual(self._get_lv_uuid(prev_path), orig_uuids[''])
  415. self.loop.run_until_complete(volume.remove())
  416. for rev in revisions:
  417. path = '/dev/' + volume.vid + rev
  418. self.assertFalse(os.path.exists(path), path)
  419. def test_012_migration3(self):
  420. '''VM started with old code, started again with new, stopped with new'''
  421. config = {
  422. 'name': 'root',
  423. 'pool': self.pool.name,
  424. 'save_on_stop': True,
  425. 'rw': True,
  426. 'revisions_to_keep': 1,
  427. 'size': qubes.config.defaults['root_img_size'],
  428. }
  429. vm = qubes.tests.storage.TestVM(self)
  430. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  431. # mock logging, to not interfere with time.time() mock
  432. volume.log = unittest.mock.Mock()
  433. # do not call volume.create(), do it manually to have old LV naming
  434. revisions = ['', '-snap']
  435. orig_uuids = {}
  436. for rev in revisions:
  437. cmd = ['create', self.pool._pool_id,
  438. volume.vid.split('/')[1] + rev, str(config['size'])]
  439. qubes_lvm(cmd)
  440. orig_uuids[rev] = self._get_lv_uuid(volume.vid + rev)
  441. qubes.storage.lvm.reset_cache()
  442. path_snap = '/dev/' + volume._vid_snap
  443. self.assertTrue(os.path.exists(path_snap), path_snap)
  444. expected_revisions = {}
  445. self.assertEqual(volume.revisions, expected_revisions)
  446. self.assertTrue(volume.path, '/dev/' + volume.vid)
  447. self.assertTrue(volume.is_dirty())
  448. self.loop.run_until_complete(volume.start())
  449. self.assertEqual(volume.revisions, expected_revisions)
  450. self.assertEqual(volume.path, '/dev/' + volume.vid)
  451. # -snap LV should be unchanged
  452. self.assertEqual(self._get_lv_uuid(volume._vid_snap),
  453. orig_uuids['-snap'])
  454. self.loop.run_until_complete(volume.remove())
  455. for rev in revisions:
  456. path = '/dev/' + volume.vid + rev
  457. self.assertFalse(os.path.exists(path), path)
  458. def test_013_migration4(self):
  459. '''revisions_to_keep=0, VM started with old code, stopped with new'''
  460. config = {
  461. 'name': 'root',
  462. 'pool': self.pool.name,
  463. 'save_on_stop': True,
  464. 'rw': True,
  465. 'revisions_to_keep': 0,
  466. 'size': qubes.config.defaults['root_img_size'],
  467. }
  468. vm = qubes.tests.storage.TestVM(self)
  469. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  470. # mock logging, to not interfere with time.time() mock
  471. volume.log = unittest.mock.Mock()
  472. # do not call volume.create(), do it manually to have old LV naming
  473. revisions = ['', '-snap']
  474. orig_uuids = {}
  475. for rev in revisions:
  476. cmd = ['create', self.pool._pool_id,
  477. volume.vid.split('/')[1] + rev, str(config['size'])]
  478. qubes_lvm(cmd)
  479. orig_uuids[rev] = self._get_lv_uuid(volume.vid + rev)
  480. qubes.storage.lvm.reset_cache()
  481. path_snap = '/dev/' + volume._vid_snap
  482. self.assertTrue(os.path.exists(path_snap), path_snap)
  483. expected_revisions = {}
  484. self.assertEqual(volume.revisions, expected_revisions)
  485. self.assertEqual(volume.path, '/dev/' + volume.vid)
  486. self.assertTrue(volume.is_dirty())
  487. with unittest.mock.patch('time.time') as mock_time:
  488. mock_time.side_effect = ('1521065906', '1521065907')
  489. self.loop.run_until_complete(volume.stop())
  490. expected_revisions = {}
  491. self.assertEqual(volume.revisions, expected_revisions)
  492. self.assertEqual(volume.path, '/dev/' + volume.vid)
  493. self.loop.run_until_complete(volume.remove())
  494. for rev in revisions:
  495. path = '/dev/' + volume.vid + rev
  496. self.assertFalse(os.path.exists(path), path)
  497. def test_014_commit_keep_0(self):
  498. ''' Test volume changes commit, with revisions_to_keep=0'''
  499. config = {
  500. 'name': 'root',
  501. 'pool': self.pool.name,
  502. 'save_on_stop': True,
  503. 'rw': True,
  504. 'revisions_to_keep': 0,
  505. 'size': qubes.config.defaults['root_img_size'],
  506. }
  507. vm = qubes.tests.storage.TestVM(self)
  508. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  509. # mock logging, to not interfere with time.time() mock
  510. volume.log = unittest.mock.Mock()
  511. self.loop.run_until_complete(volume.create())
  512. self.assertFalse(volume.is_dirty())
  513. path = volume.path
  514. expected_revisions = {}
  515. self.assertEqual(volume.revisions, expected_revisions)
  516. self.loop.run_until_complete(volume.start())
  517. self.assertEqual(volume.revisions, expected_revisions)
  518. path_snap = '/dev/' + volume._vid_snap
  519. snap_uuid = self._get_lv_uuid(path_snap)
  520. self.assertTrue(volume.is_dirty())
  521. self.assertEqual(volume.path, path)
  522. with unittest.mock.patch('time.time') as mock_time:
  523. mock_time.side_effect = [521065906]
  524. self.loop.run_until_complete(volume.stop())
  525. self.assertFalse(volume.is_dirty())
  526. self.assertEqual(volume.revisions, {})
  527. self.assertEqual(volume.path, '/dev/' + volume.vid)
  528. self.assertEqual(snap_uuid, self._get_lv_uuid(volume.path))
  529. self.assertFalse(os.path.exists(path_snap), path_snap)
  530. self.loop.run_until_complete(volume.remove())
  531. def test_020_revert_last(self):
  532. ''' Test volume revert'''
  533. config = {
  534. 'name': 'root',
  535. 'pool': self.pool.name,
  536. 'save_on_stop': True,
  537. 'rw': True,
  538. 'revisions_to_keep': 2,
  539. 'size': qubes.config.defaults['root_img_size'],
  540. }
  541. vm = qubes.tests.storage.TestVM(self)
  542. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  543. self.loop.run_until_complete(volume.create())
  544. self.loop.run_until_complete(volume.start())
  545. self.loop.run_until_complete(volume.stop())
  546. self.loop.run_until_complete(volume.start())
  547. self.loop.run_until_complete(volume.stop())
  548. self.assertEqual(len(volume.revisions), 2)
  549. revisions = volume.revisions
  550. revision_id = max(revisions.keys())
  551. current_path = volume.path
  552. current_uuid = self._get_lv_uuid(volume.path)
  553. rev_uuid = self._get_lv_uuid(volume.vid + '-' + revision_id)
  554. self.assertFalse(volume.is_dirty())
  555. self.assertNotEqual(current_uuid, rev_uuid)
  556. self.loop.run_until_complete(volume.revert())
  557. path_snap = '/dev/' + volume._vid_snap
  558. self.assertFalse(os.path.exists(path_snap), path_snap)
  559. self.assertEqual(current_path, volume.path)
  560. new_uuid = self._get_lv_origin_uuid(volume.path)
  561. self.assertEqual(new_uuid, rev_uuid)
  562. self.assertEqual(volume.revisions, revisions)
  563. self.loop.run_until_complete(volume.remove())
  564. def test_021_revert_earlier(self):
  565. ''' Test volume revert'''
  566. config = {
  567. 'name': 'root',
  568. 'pool': self.pool.name,
  569. 'save_on_stop': True,
  570. 'rw': True,
  571. 'revisions_to_keep': 2,
  572. 'size': qubes.config.defaults['root_img_size'],
  573. }
  574. vm = qubes.tests.storage.TestVM(self)
  575. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  576. self.loop.run_until_complete(volume.create())
  577. self.loop.run_until_complete(volume.start())
  578. self.loop.run_until_complete(volume.stop())
  579. self.loop.run_until_complete(volume.start())
  580. self.loop.run_until_complete(volume.stop())
  581. self.assertEqual(len(volume.revisions), 2)
  582. revisions = volume.revisions
  583. revision_id = min(revisions.keys())
  584. current_path = volume.path
  585. current_uuid = self._get_lv_uuid(volume.path)
  586. rev_uuid = self._get_lv_uuid(volume.vid + '-' + revision_id)
  587. self.assertFalse(volume.is_dirty())
  588. self.assertNotEqual(current_uuid, rev_uuid)
  589. self.loop.run_until_complete(volume.revert(revision_id))
  590. path_snap = '/dev/' + volume._vid_snap
  591. self.assertFalse(os.path.exists(path_snap), path_snap)
  592. self.assertEqual(current_path, volume.path)
  593. new_uuid = self._get_lv_origin_uuid(volume.path)
  594. self.assertEqual(new_uuid, rev_uuid)
  595. self.assertEqual(volume.revisions, revisions)
  596. self.loop.run_until_complete(volume.remove())
  597. def test_030_import_data(self):
  598. ''' Test volume import'''
  599. config = {
  600. 'name': 'root',
  601. 'pool': self.pool.name,
  602. 'save_on_stop': True,
  603. 'rw': True,
  604. 'revisions_to_keep': 2,
  605. 'size': qubes.config.defaults['root_img_size'],
  606. }
  607. vm = qubes.tests.storage.TestVM(self)
  608. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  609. self.loop.run_until_complete(volume.create())
  610. current_uuid = self._get_lv_uuid(volume.path)
  611. self.assertFalse(volume.is_dirty())
  612. import_path = self.loop.run_until_complete(volume.import_data())
  613. import_uuid = self._get_lv_uuid(import_path)
  614. self.assertNotEqual(current_uuid, import_uuid)
  615. # success - commit data
  616. self.loop.run_until_complete(volume.import_data_end(True))
  617. new_current_uuid = self._get_lv_uuid(volume.path)
  618. self.assertEqual(new_current_uuid, import_uuid)
  619. revisions = volume.revisions
  620. self.assertEqual(len(revisions), 1)
  621. revision = revisions.popitem()[0]
  622. self.assertEqual(current_uuid,
  623. self._get_lv_uuid(volume.vid + '-' + revision))
  624. self.assertFalse(os.path.exists(import_path), import_path)
  625. self.loop.run_until_complete(volume.remove())
  626. def test_031_import_data_fail(self):
  627. ''' Test volume import'''
  628. config = {
  629. 'name': 'root',
  630. 'pool': self.pool.name,
  631. 'save_on_stop': True,
  632. 'rw': True,
  633. 'revisions_to_keep': 2,
  634. 'size': qubes.config.defaults['root_img_size'],
  635. }
  636. vm = qubes.tests.storage.TestVM(self)
  637. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  638. self.loop.run_until_complete(volume.create())
  639. current_uuid = self._get_lv_uuid(volume.path)
  640. self.assertFalse(volume.is_dirty())
  641. import_path = self.loop.run_until_complete(volume.import_data())
  642. import_uuid = self._get_lv_uuid(import_path)
  643. self.assertNotEqual(current_uuid, import_uuid)
  644. # fail - discard data
  645. self.loop.run_until_complete(volume.import_data_end(False))
  646. new_current_uuid = self._get_lv_uuid(volume.path)
  647. self.assertEqual(new_current_uuid, current_uuid)
  648. revisions = volume.revisions
  649. self.assertEqual(len(revisions), 0)
  650. self.assertFalse(os.path.exists(import_path), import_path)
  651. self.loop.run_until_complete(volume.remove())
  652. def test_032_import_volume_same_pool(self):
  653. '''Import volume from the same pool'''
  654. # source volume
  655. config = {
  656. 'name': 'root',
  657. 'pool': self.pool.name,
  658. 'save_on_stop': True,
  659. 'rw': True,
  660. 'revisions_to_keep': 2,
  661. 'size': qubes.config.defaults['root_img_size'],
  662. }
  663. vm = qubes.tests.storage.TestVM(self)
  664. source_volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  665. self.loop.run_until_complete(source_volume.create())
  666. source_uuid = self._get_lv_uuid(source_volume.path)
  667. # destination volume
  668. config = {
  669. 'name': 'root2',
  670. 'pool': self.pool.name,
  671. 'save_on_stop': True,
  672. 'rw': True,
  673. 'revisions_to_keep': 2,
  674. 'size': qubes.config.defaults['root_img_size'],
  675. }
  676. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  677. volume.log = unittest.mock.Mock()
  678. with unittest.mock.patch('time.time') as mock_time:
  679. mock_time.side_effect = [1521065905]
  680. self.loop.run_until_complete(volume.create())
  681. self.assertEqual(volume.revisions, {})
  682. uuid_before = self._get_lv_uuid(volume.path)
  683. with unittest.mock.patch('time.time') as mock_time:
  684. mock_time.side_effect = [1521065906]
  685. self.loop.run_until_complete(
  686. volume.import_volume(source_volume))
  687. uuid_after = self._get_lv_uuid(volume.path)
  688. self.assertNotEqual(uuid_after, uuid_before)
  689. # also should be different than source volume (clone, not the same LV)
  690. self.assertNotEqual(uuid_after, source_uuid)
  691. self.assertEqual(self._get_lv_origin_uuid(volume.path), source_uuid)
  692. expected_revisions = {
  693. '1521065906-back': '2018-03-14T22:18:26',
  694. }
  695. self.assertEqual(volume.revisions, expected_revisions)
  696. self.loop.run_until_complete(volume.remove())
  697. self.loop.run_until_complete(source_volume.remove())
  698. def test_033_import_volume_different_pool(self):
  699. '''Import volume from a different pool'''
  700. source_volume = unittest.mock.Mock()
  701. # destination volume
  702. config = {
  703. 'name': 'root2',
  704. 'pool': self.pool.name,
  705. 'save_on_stop': True,
  706. 'rw': True,
  707. 'revisions_to_keep': 2,
  708. 'size': qubes.config.defaults['root_img_size'],
  709. }
  710. vm = qubes.tests.storage.TestVM(self)
  711. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  712. volume.log = unittest.mock.Mock()
  713. with unittest.mock.patch('time.time') as mock_time:
  714. mock_time.side_effect = [1521065905]
  715. self.loop.run_until_complete(volume.create())
  716. self.assertEqual(volume.revisions, {})
  717. uuid_before = self._get_lv_uuid(volume.path)
  718. with tempfile.NamedTemporaryFile() as source_volume_file:
  719. source_volume_file.write(b'test-content')
  720. source_volume_file.flush()
  721. source_volume.size = 16 * 1024 * 1024 # 16MiB
  722. source_volume.export.return_value = source_volume_file.name
  723. with unittest.mock.patch('time.time') as mock_time:
  724. mock_time.side_effect = [1521065906]
  725. self.loop.run_until_complete(
  726. volume.import_volume(source_volume))
  727. uuid_after = self._get_lv_uuid(volume.path)
  728. self.assertNotEqual(uuid_after, uuid_before)
  729. self.assertEqual(volume.size, 16 * 1024 * 1024)
  730. volume_content = subprocess.check_output(['sudo', 'cat', volume.path])
  731. self.assertEqual(volume_content.rstrip(b'\0'), b'test-content')
  732. expected_revisions = {
  733. '1521065906-back': '2018-03-14T22:18:26',
  734. }
  735. self.assertEqual(volume.revisions, expected_revisions)
  736. self.loop.run_until_complete(volume.remove())
  737. def test_040_volatile(self):
  738. '''Volatile volume test'''
  739. config = {
  740. 'name': 'volatile',
  741. 'pool': self.pool.name,
  742. 'rw': True,
  743. 'size': qubes.config.defaults['root_img_size'],
  744. }
  745. vm = qubes.tests.storage.TestVM(self)
  746. volume = self.app.get_pool(self.pool.name).init_volume(vm, config)
  747. # volatile volume don't need any file, verify should succeed
  748. self.assertTrue(volume.verify())
  749. self.loop.run_until_complete(volume.create())
  750. self.assertTrue(volume.verify())
  751. self.assertFalse(volume.save_on_stop)
  752. self.assertFalse(volume.snap_on_start)
  753. path = volume.path
  754. self.assertEqual(path, '/dev/' + volume.vid)
  755. self.assertFalse(os.path.exists(path))
  756. self.loop.run_until_complete(volume.start())
  757. self.assertTrue(os.path.exists(path))
  758. vol_uuid = self._get_lv_uuid(path)
  759. self.loop.run_until_complete(volume.start())
  760. self.assertTrue(os.path.exists(path))
  761. vol_uuid2 = self._get_lv_uuid(path)
  762. self.assertNotEqual(vol_uuid, vol_uuid2)
  763. self.loop.run_until_complete(volume.stop())
  764. self.assertFalse(os.path.exists(path))
  765. def test_050_snapshot_volume(self):
  766. ''' Test snapshot volume creation '''
  767. config_origin = {
  768. 'name': 'root',
  769. 'pool': self.pool.name,
  770. 'save_on_stop': True,
  771. 'rw': True,
  772. 'size': qubes.config.defaults['root_img_size'],
  773. }
  774. vm = qubes.tests.storage.TestVM(self)
  775. volume_origin = self.app.get_pool(self.pool.name).init_volume(
  776. vm, config_origin)
  777. self.loop.run_until_complete(volume_origin.create())
  778. config_snapshot = {
  779. 'name': 'root2',
  780. 'pool': self.pool.name,
  781. 'snap_on_start': True,
  782. 'source': volume_origin,
  783. 'rw': True,
  784. 'size': qubes.config.defaults['root_img_size'],
  785. }
  786. volume = self.app.get_pool(self.pool.name).init_volume(
  787. vm, config_snapshot)
  788. self.assertIsInstance(volume, ThinVolume)
  789. self.assertEqual(volume.name, 'root2')
  790. self.assertEqual(volume.pool, self.pool.name)
  791. self.assertEqual(volume.size, qubes.config.defaults['root_img_size'])
  792. # only origin volume really needs to exist, verify should succeed
  793. # even before create
  794. self.assertTrue(volume.verify())
  795. self.loop.run_until_complete(volume.create())
  796. path = volume.path
  797. self.assertEqual(path, '/dev/' + volume.vid)
  798. self.assertFalse(os.path.exists(path), path)
  799. self.loop.run_until_complete(volume.start())
  800. # snapshot volume isn't considered dirty at any time
  801. self.assertFalse(volume.is_dirty())
  802. # not outdated yet
  803. self.assertFalse(volume.is_outdated())
  804. origin_uuid = self._get_lv_uuid(volume_origin.path)
  805. snap_origin_uuid = self._get_lv_origin_uuid(volume._vid_snap)
  806. self.assertEqual(origin_uuid, snap_origin_uuid)
  807. # now make it outdated
  808. self.loop.run_until_complete(volume_origin.start())
  809. self.loop.run_until_complete(volume_origin.stop())
  810. self.assertTrue(volume.is_outdated())
  811. origin_uuid = self._get_lv_uuid(volume_origin.path)
  812. self.assertNotEqual(origin_uuid, snap_origin_uuid)
  813. self.loop.run_until_complete(volume.stop())
  814. # stopped volume is never outdated
  815. self.assertFalse(volume.is_outdated())
  816. path = volume.path
  817. self.assertFalse(os.path.exists(path), path)
  818. path = '/dev/' + volume._vid_snap
  819. self.assertFalse(os.path.exists(path), path)
  820. self.loop.run_until_complete(volume.remove())
  821. self.loop.run_until_complete(volume_origin.remove())
  822. def test_100_pool_list_volumes(self):
  823. config = {
  824. 'name': 'root',
  825. 'pool': self.pool.name,
  826. 'save_on_stop': True,
  827. 'rw': True,
  828. 'revisions_to_keep': 2,
  829. 'size': qubes.config.defaults['root_img_size'],
  830. }
  831. config2 = config.copy()
  832. vm = qubes.tests.storage.TestVM(self)
  833. volume1 = self.app.get_pool(self.pool.name).init_volume(vm, config)
  834. self.loop.run_until_complete(volume1.create())
  835. config2['name'] = 'private'
  836. volume2 = self.app.get_pool(self.pool.name).init_volume(vm, config2)
  837. self.loop.run_until_complete(volume2.create())
  838. # create some revisions
  839. self.loop.run_until_complete(volume1.start())
  840. self.loop.run_until_complete(volume1.stop())
  841. # and have one in dirty state
  842. self.loop.run_until_complete(volume2.start())
  843. self.assertIn(volume1, list(self.pool.volumes))
  844. self.assertIn(volume2, list(self.pool.volumes))
  845. self.loop.run_until_complete(volume1.remove())
  846. self.assertNotIn(volume1, list(self.pool.volumes))
  847. self.assertIn(volume2, list(self.pool.volumes))
  848. self.loop.run_until_complete(volume2.remove())
  849. self.assertNotIn(volume1, list(self.pool.volumes))
  850. self.assertNotIn(volume1, list(self.pool.volumes))
  851. @skipUnlessLvmPoolExists
  852. class TC_01_ThinPool(ThinPoolBase, qubes.tests.SystemTestCase):
  853. ''' Sanity tests for :py:class:`qubes.storage.lvm.ThinPool` '''
  854. def setUp(self):
  855. super(TC_01_ThinPool, self).setUp()
  856. self.init_default_template()
  857. def test_004_import(self):
  858. template_vm = self.app.default_template
  859. name = self.make_vm_name('import')
  860. vm = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM, name=name,
  861. label='red')
  862. vm.clone_properties(template_vm)
  863. self.loop.run_until_complete(
  864. vm.clone_disk_files(template_vm, pool=self.pool.name))
  865. for v_name, volume in vm.volumes.items():
  866. if volume.save_on_stop:
  867. expected = "/dev/{!s}/vm-{!s}-{!s}".format(
  868. DEFAULT_LVM_POOL.split('/')[0], vm.name, v_name)
  869. self.assertEqual(volume.path, expected)
  870. with self.assertNotRaises(qubes.exc.QubesException):
  871. self.loop.run_until_complete(vm.start())
  872. def test_005_create_appvm(self):
  873. vm = self.app.add_new_vm(cls=qubes.vm.appvm.AppVM,
  874. name=self.make_vm_name('appvm'), label='red')
  875. self.loop.run_until_complete(vm.create_on_disk(pool=self.pool.name))
  876. for v_name, volume in vm.volumes.items():
  877. if volume.save_on_stop:
  878. expected = "/dev/{!s}/vm-{!s}-{!s}".format(
  879. DEFAULT_LVM_POOL.split('/')[0], vm.name, v_name)
  880. self.assertEqual(volume.path, expected)
  881. with self.assertNotRaises(qubes.exc.QubesException):
  882. self.loop.run_until_complete(vm.start())
  883. @skipUnlessLvmPoolExists
  884. class TC_02_StorageHelpers(ThinPoolBase):
  885. def setUp(self):
  886. xml_path = '/tmp/qubes-test.xml'
  887. self.app = qubes.Qubes.create_empty_store(store=xml_path,
  888. clockvm=None,
  889. updatevm=None,
  890. offline_mode=True,
  891. )
  892. os.environ['QUBES_XML_PATH'] = xml_path
  893. super(TC_02_StorageHelpers, self).setUp()
  894. # reset cache
  895. qubes.storage.DirectoryThinPool._thin_pool = {}
  896. self.thin_dir = tempfile.TemporaryDirectory()
  897. subprocess.check_call(
  898. ['sudo', 'lvcreate', '-q', '-V', '32M',
  899. '-T', DEFAULT_LVM_POOL, '-n',
  900. 'test-file-pool'], stdout=subprocess.DEVNULL)
  901. self.thin_dev = '/dev/{}/test-file-pool'.format(
  902. DEFAULT_LVM_POOL.split('/')[0])
  903. subprocess.check_call(
  904. ['sudo', 'mkfs.ext4', '-q', self.thin_dev])
  905. subprocess.check_call(['sudo', 'mount', self.thin_dev,
  906. self.thin_dir.name])
  907. subprocess.check_call(['sudo', 'chmod', '777',
  908. self.thin_dir.name])
  909. def tearDown(self):
  910. subprocess.check_call(['sudo', 'umount', self.thin_dir.name])
  911. subprocess.check_call(
  912. ['sudo', 'lvremove', '-q', '-f', self.thin_dev],
  913. stdout = subprocess.DEVNULL)
  914. self.thin_dir.cleanup()
  915. super(TC_02_StorageHelpers, self).tearDown()
  916. os.unlink(self.app.store)
  917. del self.app
  918. for attr in dir(self):
  919. if isinstance(getattr(self, attr), qubes.vm.BaseVM):
  920. delattr(self, attr)
  921. def test_000_search_thin_pool(self):
  922. pool = qubes.storage.search_pool_containing_dir(
  923. self.app.pools.values(), self.thin_dir.name)
  924. self.assertEqual(pool, self.pool)
  925. def test_001_search_none(self):
  926. pool = qubes.storage.search_pool_containing_dir(
  927. self.app.pools.values(), '/tmp')
  928. self.assertIsNone(pool)
  929. def test_002_search_subdir(self):
  930. subdir = os.path.join(self.thin_dir.name, 'some-dir')
  931. os.mkdir(subdir)
  932. pool = qubes.storage.search_pool_containing_dir(
  933. self.app.pools.values(), subdir)
  934. self.assertEqual(pool, self.pool)
  935. def test_003_search_file_pool(self):
  936. subdir = os.path.join(self.thin_dir.name, 'some-dir')
  937. file_pool_config = {
  938. 'name': 'test-file-pool',
  939. 'driver': 'file',
  940. 'dir_path': subdir
  941. }
  942. pool2 = self.app.add_pool(**file_pool_config)
  943. pool = qubes.storage.search_pool_containing_dir(
  944. self.app.pools.values(), subdir)
  945. self.assertEqual(pool, pool2)
  946. pool = qubes.storage.search_pool_containing_dir(
  947. self.app.pools.values(), self.thin_dir.name)
  948. self.assertEqual(pool, self.pool)