storage_lvm.py 49 KB

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