storage.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. #
  2. # The Qubes OS Project, http://www.qubes-os.org
  3. #
  4. # Copyright (C) 2016 Marek Marczykowski-Górecki
  5. # <marmarek@invisiblethingslab.com>
  6. #
  7. # This library is free software; you can redistribute it and/or
  8. # modify it under the terms of the GNU Lesser General Public
  9. # License as published by the Free Software Foundation; either
  10. # version 2.1 of the License, or (at your option) any later version.
  11. #
  12. # This library is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  15. # Lesser General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Lesser General Public
  18. # License along with this library; if not, see <https://www.gnu.org/licenses/>.
  19. #
  20. import asyncio
  21. import os
  22. import shutil
  23. import subprocess
  24. from contextlib import suppress
  25. import qubes.storage.lvm
  26. import qubes.tests
  27. import qubes.tests.storage_lvm
  28. import qubes.tests.storage_reflink
  29. import qubes.vm.appvm
  30. class StorageTestMixin(object):
  31. def setUp(self):
  32. super(StorageTestMixin, self).setUp()
  33. self.init_default_template()
  34. self.old_default_pool = self.app.default_pool
  35. self.vm1 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  36. name=self.make_vm_name('vm1'),
  37. label='red')
  38. self.loop.run_until_complete(self.vm1.create_on_disk())
  39. self.vm2 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  40. name=self.make_vm_name('vm2'),
  41. label='red')
  42. self.loop.run_until_complete(self.vm2.create_on_disk())
  43. self.pool = None
  44. self.init_pool()
  45. self.app.save()
  46. def tearDown(self):
  47. with suppress(qubes.exc.QubesException):
  48. self.loop.run_until_complete(self.vm1.kill())
  49. with suppress(qubes.exc.QubesException):
  50. self.loop.run_until_complete(self.vm2.kill())
  51. del self.app.domains[self.vm1]
  52. del self.app.domains[self.vm2]
  53. del self.vm1
  54. del self.vm2
  55. self.app.default_pool = self.old_default_pool
  56. self.cleanup_pool()
  57. del self.pool
  58. super(StorageTestMixin, self).tearDown()
  59. def init_pool(self):
  60. ''' Initialize storage pool to be tested, store it in self.pool'''
  61. raise NotImplementedError
  62. def cleanup_pool(self):
  63. ''' Remove tested storage pool'''
  64. raise NotImplementedError
  65. def test_000_volatile(self):
  66. '''Test if volatile volume is really volatile'''
  67. return self.loop.run_until_complete(self._test_000_volatile())
  68. @asyncio.coroutine
  69. def _test_000_volatile(self):
  70. size = 32*1024*1024
  71. volume_config = {
  72. 'pool': self.pool.name,
  73. 'size': size,
  74. 'save_on_stop': False,
  75. 'rw': True,
  76. }
  77. testvol = self.vm1.storage.init_volume('testvol', volume_config)
  78. coro_maybe = testvol.create()
  79. del testvol
  80. if asyncio.iscoroutine(coro_maybe):
  81. yield from coro_maybe
  82. del coro_maybe
  83. self.app.save()
  84. yield from (self.vm1.start())
  85. yield from self.wait_for_session(self.vm1)
  86. # volatile image not clean
  87. yield from (self.vm1.run_for_stdio(
  88. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.format(size),
  89. user='root'))
  90. # volatile image not volatile
  91. yield from (
  92. self.vm1.run_for_stdio('echo test123 > /dev/xvde', user='root'))
  93. yield from (self.vm1.shutdown(wait=True))
  94. yield from (self.vm1.start())
  95. yield from (self.vm1.run_for_stdio(
  96. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.format(size),
  97. user='root'))
  98. def test_001_non_volatile(self):
  99. '''Test if non-volatile volume is really non-volatile'''
  100. return self.loop.run_until_complete(self._test_001_non_volatile())
  101. @asyncio.coroutine
  102. def _test_001_non_volatile(self):
  103. size = 32*1024*1024
  104. volume_config = {
  105. 'pool': self.pool.name,
  106. 'size': size,
  107. 'save_on_stop': True,
  108. 'rw': True,
  109. }
  110. testvol = self.vm1.storage.init_volume('testvol', volume_config)
  111. coro_maybe = testvol.create()
  112. del testvol
  113. if asyncio.iscoroutine(coro_maybe):
  114. yield from coro_maybe
  115. del coro_maybe
  116. self.app.save()
  117. yield from self.vm1.start()
  118. yield from self.wait_for_session(self.vm1)
  119. # non-volatile image not clean
  120. yield from self.vm1.run_for_stdio(
  121. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.format(size),
  122. user='root')
  123. yield from self.vm1.run_for_stdio('echo test123 > /dev/xvde',
  124. user='root')
  125. yield from self.vm1.shutdown(wait=True)
  126. yield from self.vm1.start()
  127. # non-volatile image volatile
  128. with self.assertRaises(subprocess.CalledProcessError):
  129. yield from self.vm1.run_for_stdio(
  130. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.format(
  131. size),
  132. user='root')
  133. def test_002_read_only(self):
  134. '''Test read-only volume'''
  135. self.loop.run_until_complete(self._test_002_read_only())
  136. @asyncio.coroutine
  137. def _test_002_read_only(self):
  138. size = 32 * 1024 * 1024
  139. volume_config = {
  140. 'pool': self.pool.name,
  141. 'size': size,
  142. 'save_on_stop': False,
  143. 'rw': False,
  144. }
  145. testvol = self.vm1.storage.init_volume('testvol', volume_config)
  146. coro_maybe = testvol.create()
  147. del testvol
  148. if asyncio.iscoroutine(coro_maybe):
  149. yield from coro_maybe
  150. del coro_maybe
  151. self.app.save()
  152. yield from self.vm1.start()
  153. # non-volatile image not clean
  154. yield from self.vm1.run_for_stdio(
  155. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.format(size),
  156. user='root')
  157. # Write to read-only volume unexpectedly succeeded
  158. with self.assertRaises(subprocess.CalledProcessError):
  159. yield from self.vm1.run_for_stdio('echo test123 > /dev/xvde',
  160. user='root')
  161. # read-only volume modified
  162. yield from self.vm1.run_for_stdio(
  163. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.format(size),
  164. user='root')
  165. def test_003_snapshot(self):
  166. '''Test snapshot volume data propagation'''
  167. self.loop.run_until_complete(self._test_003_snapshot())
  168. @asyncio.coroutine
  169. def _test_003_snapshot(self):
  170. size = 128 * 1024 * 1024
  171. volume_config = {
  172. 'pool': self.pool.name,
  173. 'size': size,
  174. 'save_on_stop': True,
  175. 'rw': True,
  176. }
  177. testvol = self.vm1.storage.init_volume('testvol', volume_config)
  178. coro_maybe = testvol.create()
  179. if asyncio.iscoroutine(coro_maybe):
  180. yield from coro_maybe
  181. del coro_maybe
  182. volume_config = {
  183. 'pool': self.pool.name,
  184. 'size': size,
  185. 'snap_on_start': True,
  186. 'source': testvol.vid,
  187. 'rw': True,
  188. }
  189. del testvol
  190. testvol_snap = self.vm2.storage.init_volume('testvol', volume_config)
  191. coro_maybe = testvol_snap.create()
  192. del testvol_snap
  193. if asyncio.iscoroutine(coro_maybe):
  194. yield from coro_maybe
  195. del coro_maybe
  196. self.app.save()
  197. yield from self.vm1.start()
  198. yield from self.vm2.start()
  199. yield from asyncio.wait(
  200. [self.wait_for_session(self.vm1), self.wait_for_session(self.vm2)])
  201. try:
  202. yield from self.vm1.run_for_stdio(
  203. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.
  204. format(size),
  205. user='root')
  206. except subprocess.CalledProcessError:
  207. self.fail('origin image not clean')
  208. try:
  209. yield from self.vm2.run_for_stdio(
  210. 'head -c {} /dev/zero | diff -q /dev/xvde -'.format(size),
  211. user='root')
  212. except subprocess.CalledProcessError:
  213. self.fail('snapshot image not clean')
  214. try:
  215. yield from self.vm1.run_for_stdio(
  216. 'echo test123 > /dev/xvde && sync',
  217. user='root')
  218. except subprocess.CalledProcessError:
  219. self.fail('Write to read-write volume failed')
  220. try:
  221. yield from self.vm2.run_for_stdio(
  222. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.
  223. format(size),
  224. user='root')
  225. except subprocess.CalledProcessError:
  226. self.fail('origin changes propagated to snapshot too early')
  227. yield from self.vm1.shutdown(wait=True)
  228. # after origin shutdown there should be still no change
  229. try:
  230. yield from self.vm2.run_for_stdio(
  231. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.
  232. format(size),
  233. user='root')
  234. except subprocess.CalledProcessError:
  235. self.fail('origin changes propagated to snapshot too early2')
  236. yield from self.vm2.shutdown(wait=True)
  237. yield from self.vm2.start()
  238. # only after target VM restart changes should be visible
  239. with self.assertRaises(subprocess.CalledProcessError,
  240. msg='origin changes not visible in snapshot'):
  241. yield from self.vm2.run_for_stdio(
  242. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.format(
  243. size),
  244. user='root')
  245. def test_004_snapshot_non_persistent(self):
  246. '''Test snapshot volume non-persistence'''
  247. return self.loop.run_until_complete(
  248. self._test_004_snapshot_non_persistent())
  249. @asyncio.coroutine
  250. def _test_004_snapshot_non_persistent(self):
  251. size = 128 * 1024 * 1024
  252. volume_config = {
  253. 'pool': self.pool.name,
  254. 'size': size,
  255. 'save_on_stop': True,
  256. 'rw': True,
  257. }
  258. testvol = self.vm1.storage.init_volume('testvol', volume_config)
  259. coro_maybe = testvol.create()
  260. if asyncio.iscoroutine(coro_maybe):
  261. yield from coro_maybe
  262. del coro_maybe
  263. volume_config = {
  264. 'pool': self.pool.name,
  265. 'size': size,
  266. 'snap_on_start': True,
  267. 'source': testvol.vid,
  268. 'rw': True,
  269. }
  270. del testvol
  271. testvol_snap = self.vm2.storage.init_volume('testvol', volume_config)
  272. coro_maybe = testvol_snap.create()
  273. del testvol_snap
  274. if asyncio.iscoroutine(coro_maybe):
  275. yield from coro_maybe
  276. del coro_maybe
  277. self.app.save()
  278. yield from self.vm2.start()
  279. yield from self.wait_for_session(self.vm2)
  280. # snapshot image not clean
  281. yield from self.vm2.run_for_stdio(
  282. 'head -c {} /dev/zero | diff -q /dev/xvde -'.format(size),
  283. user='root')
  284. # Write to read-write snapshot volume failed
  285. yield from self.vm2.run_for_stdio('echo test123 > /dev/xvde && sync',
  286. user='root')
  287. yield from self.vm2.shutdown(wait=True)
  288. yield from self.vm2.start()
  289. # changes on snapshot survived VM restart
  290. yield from self.vm2.run_for_stdio(
  291. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.format(size),
  292. user='root')
  293. class StorageFile(StorageTestMixin, qubes.tests.SystemTestCase):
  294. def init_pool(self):
  295. self.dir_path = '/var/tmp/test-pool'
  296. self.pool = self.loop.run_until_complete(
  297. self.app.add_pool(dir_path=self.dir_path,
  298. name='test-pool', driver='file'))
  299. os.makedirs(os.path.join(self.dir_path, 'appvms', self.vm1.name),
  300. exist_ok=True)
  301. os.makedirs(os.path.join(self.dir_path, 'appvms', self.vm2.name),
  302. exist_ok=True)
  303. def cleanup_pool(self):
  304. self.loop.run_until_complete(self.app.remove_pool('test-pool'))
  305. shutil.rmtree(self.dir_path)
  306. class StorageReflinkMixin(StorageTestMixin):
  307. def cleanup_pool(self):
  308. self.loop.run_until_complete(self.app.remove_pool(self.pool.name))
  309. def init_pool(self, fs_type, **kwargs):
  310. name = 'test-reflink-integration-on-' + fs_type
  311. dir_path = os.path.join('/var/tmp', name)
  312. qubes.tests.storage_reflink.mkdir_fs(dir_path, fs_type,
  313. cleanup_via=self.addCleanup)
  314. self.pool = self.loop.run_until_complete(
  315. self.app.add_pool(name=name, dir_path=dir_path,
  316. driver='file-reflink', **kwargs))
  317. class StorageReflinkOnBtrfs(StorageReflinkMixin, qubes.tests.SystemTestCase):
  318. def init_pool(self):
  319. super().init_pool('btrfs')
  320. class StorageReflinkOnExt4(StorageReflinkMixin, qubes.tests.SystemTestCase):
  321. def init_pool(self):
  322. super().init_pool('ext4', setup_check='no')
  323. @qubes.tests.storage_lvm.skipUnlessLvmPoolExists
  324. class StorageLVM(StorageTestMixin, qubes.tests.SystemTestCase):
  325. def init_pool(self):
  326. self.created_pool = False
  327. # check if the default LVM Thin pool qubes_dom0/pool00 exists
  328. volume_group, thin_pool = \
  329. qubes.tests.storage_lvm.DEFAULT_LVM_POOL.split('/', 1)
  330. self.pool = self._find_pool(volume_group, thin_pool)
  331. if not self.pool:
  332. self.pool = self.loop.run_until_complete(
  333. self.app.add_pool(**qubes.tests.storage_lvm.POOL_CONF))
  334. self.created_pool = True
  335. def cleanup_pool(self):
  336. ''' Remove the default lvm pool if it was created only for this test '''
  337. if self.created_pool:
  338. self.loop.run_until_complete(self.app.remove_pool(self.pool.name))
  339. def _find_pool(self, volume_group, thin_pool):
  340. ''' Returns the pool matching the specified ``volume_group`` &
  341. ``thin_pool``, or None.
  342. '''
  343. pools = [p for p in self.app.pools
  344. if issubclass(p.__class__, qubes.storage.lvm.ThinPool)]
  345. for pool in pools:
  346. if pool.volume_group == volume_group \
  347. and pool.thin_pool == thin_pool:
  348. return pool
  349. return None