storage.py 12 KB

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