storage.py 11 KB

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