storage.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  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. import qubes.storage.lvm
  25. import qubes.tests
  26. import qubes.tests.storage_lvm
  27. import qubes.vm.appvm
  28. class StorageTestMixin(object):
  29. def setUp(self):
  30. super(StorageTestMixin, self).setUp()
  31. self.init_default_template()
  32. self.vm1 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  33. name=self.make_vm_name('vm1'),
  34. label='red')
  35. self.loop.run_until_complete(self.vm1.create_on_disk())
  36. self.vm2 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
  37. name=self.make_vm_name('vm2'),
  38. label='red')
  39. self.loop.run_until_complete(self.vm2.create_on_disk())
  40. self.pool = None
  41. self.init_pool()
  42. self.app.save()
  43. def tearDown(self):
  44. del self.vm1
  45. del self.vm2
  46. del self.pool
  47. super(StorageTestMixin, self).tearDown()
  48. def init_pool(self):
  49. ''' Initialize storage pool to be tested, store it in self.pool'''
  50. raise NotImplementedError
  51. def test_000_volatile(self):
  52. '''Test if volatile volume is really volatile'''
  53. return self.loop.run_until_complete(self._test_000_volatile())
  54. @asyncio.coroutine
  55. def _test_000_volatile(self):
  56. size = 32*1024*1024
  57. volume_config = {
  58. 'pool': self.pool.name,
  59. 'size': size,
  60. 'save_on_stop': False,
  61. 'rw': True,
  62. }
  63. testvol = self.vm1.storage.init_volume('testvol', volume_config)
  64. coro_maybe = testvol.create()
  65. del testvol
  66. if asyncio.iscoroutine(coro_maybe):
  67. yield from coro_maybe
  68. del coro_maybe
  69. self.app.save()
  70. yield from (self.vm1.start())
  71. # volatile image not clean
  72. yield from (self.vm1.run_for_stdio(
  73. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.format(size),
  74. user='root'))
  75. # volatile image not volatile
  76. yield from (
  77. self.vm1.run_for_stdio('echo test123 > /dev/xvde', user='root'))
  78. yield from (self.vm1.shutdown(wait=True))
  79. yield from (self.vm1.start())
  80. yield from (self.vm1.run_for_stdio(
  81. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.format(size),
  82. user='root'))
  83. def test_001_non_volatile(self):
  84. '''Test if non-volatile volume is really non-volatile'''
  85. return self.loop.run_until_complete(self._test_001_non_volatile())
  86. @asyncio.coroutine
  87. def _test_001_non_volatile(self):
  88. size = 32*1024*1024
  89. volume_config = {
  90. 'pool': self.pool.name,
  91. 'size': size,
  92. 'save_on_stop': True,
  93. 'rw': True,
  94. }
  95. testvol = self.vm1.storage.init_volume('testvol', volume_config)
  96. coro_maybe = testvol.create()
  97. del testvol
  98. if asyncio.iscoroutine(coro_maybe):
  99. yield from coro_maybe
  100. del coro_maybe
  101. self.app.save()
  102. yield from self.vm1.start()
  103. # non-volatile image not clean
  104. yield from self.vm1.run_for_stdio(
  105. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.format(size),
  106. user='root')
  107. yield from self.vm1.run_for_stdio('echo test123 > /dev/xvde',
  108. user='root')
  109. yield from self.vm1.shutdown(wait=True)
  110. yield from self.vm1.start()
  111. # non-volatile image volatile
  112. with self.assertRaises(subprocess.CalledProcessError):
  113. yield from self.vm1.run_for_stdio(
  114. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.format(
  115. size),
  116. user='root')
  117. def test_002_read_only(self):
  118. '''Test read-only volume'''
  119. self.loop.run_until_complete(self._test_002_read_only())
  120. @asyncio.coroutine
  121. def _test_002_read_only(self):
  122. size = 32 * 1024 * 1024
  123. volume_config = {
  124. 'pool': self.pool.name,
  125. 'size': size,
  126. 'save_on_stop': False,
  127. 'rw': False,
  128. }
  129. testvol = self.vm1.storage.init_volume('testvol', volume_config)
  130. coro_maybe = testvol.create()
  131. del testvol
  132. if asyncio.iscoroutine(coro_maybe):
  133. yield from coro_maybe
  134. del coro_maybe
  135. self.app.save()
  136. yield from self.vm1.start()
  137. # non-volatile image not clean
  138. yield from self.vm1.run_for_stdio(
  139. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.format(size),
  140. user='root')
  141. # Write to read-only volume unexpectedly succeeded
  142. with self.assertRaises(subprocess.CalledProcessError):
  143. yield from self.vm1.run_for_stdio('echo test123 > /dev/xvde',
  144. user='root')
  145. # read-only volume modified
  146. yield from self.vm1.run_for_stdio(
  147. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.format(size),
  148. user='root')
  149. def test_003_snapshot(self):
  150. '''Test snapshot volume data propagation'''
  151. self.loop.run_until_complete(self._test_003_snapshot())
  152. @asyncio.coroutine
  153. def _test_003_snapshot(self):
  154. size = 128 * 1024 * 1024
  155. volume_config = {
  156. 'pool': self.pool.name,
  157. 'size': size,
  158. 'save_on_stop': True,
  159. 'rw': True,
  160. }
  161. testvol = self.vm1.storage.init_volume('testvol', volume_config)
  162. coro_maybe = testvol.create()
  163. if asyncio.iscoroutine(coro_maybe):
  164. yield from coro_maybe
  165. del coro_maybe
  166. volume_config = {
  167. 'pool': self.pool.name,
  168. 'size': size,
  169. 'snap_on_start': True,
  170. 'source': testvol.vid,
  171. 'rw': True,
  172. }
  173. del testvol
  174. testvol_snap = self.vm2.storage.init_volume('testvol', volume_config)
  175. coro_maybe = testvol_snap.create()
  176. del testvol_snap
  177. if asyncio.iscoroutine(coro_maybe):
  178. yield from coro_maybe
  179. del coro_maybe
  180. self.app.save()
  181. yield from self.vm1.start()
  182. yield from self.vm2.start()
  183. try:
  184. yield from self.vm1.run_for_stdio(
  185. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.
  186. format(size),
  187. user='root')
  188. except subprocess.CalledProcessError:
  189. self.fail('origin image not clean')
  190. try:
  191. yield from self.vm2.run_for_stdio(
  192. 'head -c {} /dev/zero | diff -q /dev/xvde -'.format(size),
  193. user='root')
  194. except subprocess.CalledProcessError:
  195. self.fail('snapshot image not clean')
  196. try:
  197. yield from self.vm1.run_for_stdio(
  198. 'echo test123 > /dev/xvde && sync',
  199. user='root')
  200. except subprocess.CalledProcessError:
  201. self.fail('Write to read-write volume failed')
  202. try:
  203. yield from self.vm2.run_for_stdio(
  204. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.
  205. format(size),
  206. user='root')
  207. except subprocess.CalledProcessError:
  208. self.fail('origin changes propagated to snapshot too early')
  209. yield from self.vm1.shutdown(wait=True)
  210. # after origin shutdown there should be still no change
  211. try:
  212. yield from self.vm2.run_for_stdio(
  213. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.
  214. format(size),
  215. user='root')
  216. except subprocess.CalledProcessError:
  217. self.fail('origin changes propagated to snapshot too early2')
  218. yield from self.vm2.shutdown(wait=True)
  219. yield from self.vm2.start()
  220. # only after target VM restart changes should be visible
  221. with self.assertRaises(subprocess.CalledProcessError,
  222. msg='origin changes not visible in snapshot'):
  223. yield from self.vm2.run_for_stdio(
  224. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.format(
  225. size),
  226. user='root')
  227. def test_004_snapshot_non_persistent(self):
  228. '''Test snapshot volume non-persistence'''
  229. return self.loop.run_until_complete(
  230. self._test_004_snapshot_non_persistent())
  231. @asyncio.coroutine
  232. def _test_004_snapshot_non_persistent(self):
  233. size = 128 * 1024 * 1024
  234. volume_config = {
  235. 'pool': self.pool.name,
  236. 'size': size,
  237. 'save_on_stop': True,
  238. 'rw': True,
  239. }
  240. testvol = self.vm1.storage.init_volume('testvol', volume_config)
  241. coro_maybe = testvol.create()
  242. if asyncio.iscoroutine(coro_maybe):
  243. yield from coro_maybe
  244. del coro_maybe
  245. volume_config = {
  246. 'pool': self.pool.name,
  247. 'size': size,
  248. 'snap_on_start': True,
  249. 'source': testvol.vid,
  250. 'rw': True,
  251. }
  252. del testvol
  253. testvol_snap = self.vm2.storage.init_volume('testvol', volume_config)
  254. coro_maybe = testvol_snap.create()
  255. del testvol_snap
  256. if asyncio.iscoroutine(coro_maybe):
  257. yield from coro_maybe
  258. del coro_maybe
  259. self.app.save()
  260. yield from self.vm2.start()
  261. # snapshot image not clean
  262. yield from self.vm2.run_for_stdio(
  263. 'head -c {} /dev/zero | diff -q /dev/xvde -'.format(size),
  264. user='root')
  265. # Write to read-write snapshot volume failed
  266. yield from self.vm2.run_for_stdio('echo test123 > /dev/xvde && sync',
  267. user='root')
  268. yield from self.vm2.shutdown(wait=True)
  269. yield from self.vm2.start()
  270. # changes on snapshot survived VM restart
  271. yield from self.vm2.run_for_stdio(
  272. 'head -c {} /dev/zero 2>&1 | diff -q /dev/xvde - 2>&1'.format(size),
  273. user='root')
  274. class StorageFile(StorageTestMixin, qubes.tests.SystemTestCase):
  275. def init_pool(self):
  276. self.dir_path = '/var/tmp/test-pool'
  277. self.pool = self.app.add_pool(dir_path=self.dir_path,
  278. name='test-pool', driver='file')
  279. os.makedirs(os.path.join(self.dir_path, 'appvms', self.vm1.name),
  280. exist_ok=True)
  281. os.makedirs(os.path.join(self.dir_path, 'appvms', self.vm2.name),
  282. exist_ok=True)
  283. def tearDown(self):
  284. self.app.remove_pool('test-pool')
  285. shutil.rmtree(self.dir_path)
  286. super(StorageFile, self).tearDown()
  287. @qubes.tests.storage_lvm.skipUnlessLvmPoolExists
  288. class StorageLVM(StorageTestMixin, qubes.tests.SystemTestCase):
  289. def init_pool(self):
  290. # check if the default LVM Thin pool qubes_dom0/pool00 exists
  291. volume_group, thin_pool = \
  292. qubes.tests.storage_lvm.DEFAULT_LVM_POOL.split('/', 1)
  293. self.pool = self._find_pool(volume_group, thin_pool)
  294. if not self.pool:
  295. self.pool = self.app.add_pool(**qubes.tests.storage_lvm.POOL_CONF)
  296. self.created_pool = True
  297. def tearDown(self):
  298. ''' Remove the default lvm pool if it was created only for this test '''
  299. if self.created_pool:
  300. self.app.remove_pool(self.pool.name)
  301. super(StorageLVM, self).tearDown()
  302. def _find_pool(self, volume_group, thin_pool):
  303. ''' Returns the pool matching the specified ``volume_group`` &
  304. ``thin_pool``, or None.
  305. '''
  306. pools = [p for p in self.app.pools
  307. if issubclass(p.__class__, qubes.storage.lvm.ThinPool)]
  308. for pool in pools:
  309. if pool.volume_group == volume_group \
  310. and pool.thin_pool == thin_pool:
  311. return pool
  312. return None