dom0_update.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. #
  2. # The Qubes OS Project, http://www.qubes-os.org
  3. #
  4. # Copyright (C) 2015 Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
  5. #
  6. # This program is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU General Public License
  8. # as published by the Free Software Foundation; either version 2
  9. # of the License, or (at your option) any later version.
  10. #
  11. # This program 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
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program; if not, write to the Free Software
  18. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
  19. # USA.
  20. #
  21. import os
  22. import shutil
  23. import subprocess
  24. import tempfile
  25. import unittest
  26. import qubes
  27. import qubes.tests
  28. VM_PREFIX = "test-"
  29. @unittest.skipUnless(os.path.exists('/usr/bin/rpmsign') and
  30. os.path.exists('/usr/bin/rpmbuild'),
  31. 'rpm-sign and/or rpm-build not installed')
  32. class TC_00_Dom0UpgradeMixin(object):
  33. """
  34. Tests for downloading dom0 updates using VMs based on different templates
  35. """
  36. pkg_name = 'qubes-test-pkg'
  37. dom0_update_common_opts = ['--disablerepo=*', '--enablerepo=test']
  38. update_flag_path = '/var/lib/qubes/updates/dom0-updates-available'
  39. @classmethod
  40. def generate_key(cls, keydir):
  41. gpg_opts = ['gpg', '--quiet', '--no-default-keyring',
  42. '--homedir', keydir]
  43. p = subprocess.Popen(gpg_opts + ['--gen-key', '--batch'],
  44. stdin=subprocess.PIPE,
  45. stderr=open(os.devnull, 'w'))
  46. p.stdin.write('''
  47. Key-Type: RSA
  48. Key-Length: 1024
  49. Key-Usage: sign
  50. Name-Real: Qubes test
  51. Expire-Date: 0
  52. %commit
  53. '''.format(keydir=keydir).encode())
  54. p.stdin.close()
  55. p.wait()
  56. subprocess.check_call(gpg_opts + ['-a', '--export',
  57. '--output', os.path.join(keydir, 'pubkey.asc')])
  58. p = subprocess.Popen(gpg_opts + ['--with-colons', '--list-keys'],
  59. stdout=subprocess.PIPE)
  60. for line in p.stdout.readlines():
  61. fields = line.decode().split(':')
  62. if fields[0] == 'pub':
  63. return fields[4][-8:].lower()
  64. raise RuntimeError
  65. @classmethod
  66. def setUpClass(cls):
  67. super(TC_00_Dom0UpgradeMixin, cls).setUpClass()
  68. cls.tmpdir = tempfile.mkdtemp()
  69. cls.keyid = cls.generate_key(cls.tmpdir)
  70. p = subprocess.Popen(['sudo', 'dd',
  71. 'status=none', 'of=/etc/yum.repos.d/test.repo'],
  72. stdin=subprocess.PIPE)
  73. p.stdin.write(b'''
  74. [test]
  75. name = Test
  76. baseurl = http://localhost:8080/
  77. enabled = 1
  78. ''')
  79. p.stdin.close()
  80. p.wait()
  81. @classmethod
  82. def tearDownClass(cls):
  83. subprocess.check_call(['sudo', 'rm', '-f',
  84. '/etc/yum.repos.d/test.repo'])
  85. shutil.rmtree(cls.tmpdir)
  86. def setUp(self):
  87. super(TC_00_Dom0UpgradeMixin, self).setUp()
  88. if self.template.startswith('whonix-'):
  89. # Whonix redirect all the traffic through tor, so repository
  90. # on http://localhost:8080/ is unavailable
  91. self.skipTest("Test not supported for this template")
  92. self.init_default_template(self.template)
  93. self.updatevm = self.app.add_new_vm(
  94. qubes.vm.appvm.AppVM,
  95. name=self.make_vm_name("updatevm"),
  96. label='red'
  97. )
  98. self.loop.run_until_complete(self.updatevm.create_on_disk())
  99. self.app.updatevm = self.updatevm
  100. self.app.save()
  101. subprocess.call(['sudo', 'rpm', '-e', self.pkg_name],
  102. stderr=open(os.devnull, 'w'))
  103. subprocess.check_call(['sudo', 'rpm', '--import',
  104. os.path.join(self.tmpdir, 'pubkey.asc')])
  105. self.loop.run_until_complete(self.updatevm.start())
  106. self.repo_running = False
  107. def tearDown(self):
  108. super(TC_00_Dom0UpgradeMixin, self).tearDown()
  109. subprocess.call(['sudo', 'rpm', '-e', self.pkg_name], stderr=open(
  110. os.devnull, 'w'))
  111. subprocess.call(['sudo', 'rpm', '-e', 'gpg-pubkey-{}'.format(
  112. self.keyid)], stderr=open(os.devnull, 'w'))
  113. for pkg in os.listdir(self.tmpdir):
  114. if pkg.endswith('.rpm'):
  115. os.unlink(pkg)
  116. def create_pkg(self, dir, name, version):
  117. spec_path = os.path.join(dir, name+'.spec')
  118. spec = open(spec_path, 'w')
  119. spec.write(
  120. '''
  121. Name: {name}
  122. Summary: Test Package
  123. Version: {version}
  124. Release: 1
  125. Vendor: Invisible Things Lab
  126. License: GPL
  127. Group: Qubes
  128. URL: http://www.qubes-os.org
  129. %description
  130. Test package
  131. %install
  132. %files
  133. '''.format(name=name, version=version)
  134. )
  135. spec.close()
  136. subprocess.check_call(
  137. ['rpmbuild', '--quiet', '-bb', '--define', '_rpmdir {}'.format(dir),
  138. spec_path])
  139. pkg_path = os.path.join(dir, 'x86_64',
  140. '{}-{}-1.x86_64.rpm'.format(name, version))
  141. subprocess.check_call(['sudo', 'chmod', 'go-rw', '/dev/tty'])
  142. subprocess.check_call(
  143. ['rpm', '--quiet', '--define=_gpg_path {}'.format(dir),
  144. '--define=_gpg_name {}'.format("Qubes test"),
  145. '--addsign', pkg_path],
  146. stdin=open(os.devnull),
  147. stdout=open(os.devnull, 'w'),
  148. stderr=subprocess.STDOUT)
  149. subprocess.check_call(['sudo', 'chmod', 'go+rw', '/dev/tty'])
  150. return pkg_path
  151. def send_pkg(self, filename):
  152. self.loop.run_until_complete(self.updatevm.run_for_stdio(
  153. 'mkdir -p /tmp/repo; cat > /tmp/repo/{}'.format(
  154. os.path.basename(filename)),
  155. input=open(filename, 'rb').read()))
  156. try:
  157. self.loop.run_until_complete(
  158. self.updatevm.run_for_stdio('cd /tmp/repo; createrepo .'))
  159. except subprocess.CalledProcessError as e:
  160. if e.returncode == 127:
  161. self.skipTest('createrepo not installed in template {}'.format(
  162. self.template))
  163. else:
  164. self.skipTest('createrepo failed with code {}, '
  165. 'cannot perform the test'.format(retcode))
  166. self.start_repo()
  167. def start_repo(self):
  168. if self.repo_running:
  169. return
  170. self.loop.run_until_complete(self.updatevm.run(
  171. 'cd /tmp/repo && python -m SimpleHTTPServer 8080'))
  172. self.repo_running = True
  173. def test_000_update(self):
  174. """Dom0 update tests
  175. Check if package update is:
  176. - detected
  177. - installed
  178. - "updates pending" flag is cleared
  179. """
  180. filename = self.create_pkg(self.tmpdir, self.pkg_name, '1.0')
  181. subprocess.check_call(['sudo', 'rpm', '-i', filename])
  182. filename = self.create_pkg(self.tmpdir, self.pkg_name, '2.0')
  183. self.send_pkg(filename)
  184. open(self.update_flag_path, 'a').close()
  185. logpath = os.path.join(self.tmpdir, 'dom0-update-output.txt')
  186. try:
  187. subprocess.check_call(['sudo', '-E', 'qubes-dom0-update', '-y'] +
  188. self.dom0_update_common_opts,
  189. stdout=open(logpath, 'w'),
  190. stderr=subprocess.STDOUT)
  191. except subprocess.CalledProcessError:
  192. self.fail("qubes-dom0-update failed: " + open(
  193. logpath).read())
  194. retcode = subprocess.call(['rpm', '-q', '{}-1.0'.format(
  195. self.pkg_name)], stdout=open(os.devnull, 'w'))
  196. self.assertEqual(retcode, 1, 'Package {}-1.0 still installed after '
  197. 'update'.format(self.pkg_name))
  198. retcode = subprocess.call(['rpm', '-q', '{}-2.0'.format(
  199. self.pkg_name)], stdout=open(os.devnull, 'w'))
  200. self.assertEqual(retcode, 0, 'Package {}-2.0 not installed after '
  201. 'update'.format(self.pkg_name))
  202. self.assertFalse(os.path.exists(self.update_flag_path),
  203. "'updates pending' flag not cleared")
  204. def test_005_update_flag_clear(self):
  205. """Check if 'updates pending' flag is creared"""
  206. # create any pkg (but not install it) to initialize repo in the VM
  207. filename = self.create_pkg(self.tmpdir, self.pkg_name, '1.0')
  208. self.send_pkg(filename)
  209. open(self.update_flag_path, 'a').close()
  210. logpath = os.path.join(self.tmpdir, 'dom0-update-output.txt')
  211. try:
  212. subprocess.check_call(['sudo', '-E', 'qubes-dom0-update', '-y'] +
  213. self.dom0_update_common_opts,
  214. stdout=open(logpath, 'w'),
  215. stderr=subprocess.STDOUT)
  216. except subprocess.CalledProcessError:
  217. self.fail("qubes-dom0-update failed: " + open(
  218. logpath).read())
  219. with open(logpath) as f:
  220. dom0_update_output = f.read()
  221. self.assertFalse('Errno' in dom0_update_output or
  222. 'Couldn\'t' in dom0_update_output,
  223. "qubes-dom0-update reported an error: {}".
  224. format(dom0_update_output))
  225. self.assertFalse(os.path.exists(self.update_flag_path),
  226. "'updates pending' flag not cleared")
  227. def test_006_update_flag_clear(self):
  228. """Check if 'updates pending' flag is creared, using --clean"""
  229. # create any pkg (but not install it) to initialize repo in the VM
  230. filename = self.create_pkg(self.tmpdir, self.pkg_name, '1.0')
  231. self.send_pkg(filename)
  232. open(self.update_flag_path, 'a').close()
  233. # remove also repodata to test #1685
  234. if os.path.exists('/var/lib/qubes/updates/repodata'):
  235. shutil.rmtree('/var/lib/qubes/updates/repodata')
  236. logpath = os.path.join(self.tmpdir, 'dom0-update-output.txt')
  237. try:
  238. subprocess.check_call(['sudo', '-E', 'qubes-dom0-update', '-y',
  239. '--clean'] +
  240. self.dom0_update_common_opts,
  241. stdout=open(logpath, 'w'),
  242. stderr=subprocess.STDOUT)
  243. except subprocess.CalledProcessError:
  244. self.fail("qubes-dom0-update failed: " + open(
  245. logpath).read())
  246. with open(logpath) as f:
  247. dom0_update_output = f.read()
  248. self.assertFalse('Errno' in dom0_update_output or
  249. 'Couldn\'t' in dom0_update_output,
  250. "qubes-dom0-update reported an error: {}".
  251. format(dom0_update_output))
  252. self.assertFalse(os.path.exists(self.update_flag_path),
  253. "'updates pending' flag not cleared")
  254. def test_010_instal(self):
  255. filename = self.create_pkg(self.tmpdir, self.pkg_name, '1.0')
  256. self.send_pkg(filename)
  257. logpath = os.path.join(self.tmpdir, 'dom0-update-output.txt')
  258. try:
  259. subprocess.check_call(['sudo', '-E', 'qubes-dom0-update', '-y'] +
  260. self.dom0_update_common_opts + [
  261. self.pkg_name],
  262. stdout=open(logpath, 'w'),
  263. stderr=subprocess.STDOUT)
  264. except subprocess.CalledProcessError:
  265. self.fail("qubes-dom0-update failed: " + open(
  266. logpath).read())
  267. retcode = subprocess.call(['rpm', '-q', '{}-1.0'.format(
  268. self.pkg_name)], stdout=open('/dev/null', 'w'))
  269. self.assertEqual(retcode, 0, 'Package {}-1.0 not installed'.format(
  270. self.pkg_name))
  271. def test_020_install_wrong_sign(self):
  272. subprocess.call(['sudo', 'rpm', '-e', 'gpg-pubkey-{}'.format(
  273. self.keyid)])
  274. filename = self.create_pkg(self.tmpdir, self.pkg_name, '1.0')
  275. self.send_pkg(filename)
  276. logpath = os.path.join(self.tmpdir, 'dom0-update-output.txt')
  277. try:
  278. subprocess.check_call(['sudo', '-E', 'qubes-dom0-update', '-y'] +
  279. self.dom0_update_common_opts + [
  280. self.pkg_name],
  281. stdout=open(logpath, 'w'),
  282. stderr=subprocess.STDOUT)
  283. self.fail("qubes-dom0-update unexpectedly succeeded: " + open(
  284. logpath).read())
  285. except subprocess.CalledProcessError:
  286. pass
  287. retcode = subprocess.call(['rpm', '-q', '{}-1.0'.format(
  288. self.pkg_name)], stdout=open('/dev/null', 'w'))
  289. self.assertEqual(retcode, 1,
  290. 'Package {}-1.0 installed although '
  291. 'signature is invalid'.format(self.pkg_name))
  292. def test_030_install_unsigned(self):
  293. filename = self.create_pkg(self.tmpdir, self.pkg_name, '1.0')
  294. subprocess.check_call(['rpm', '--delsign', filename],
  295. stdout=open(os.devnull, 'w'),
  296. stderr=subprocess.STDOUT)
  297. self.send_pkg(filename)
  298. logpath = os.path.join(self.tmpdir, 'dom0-update-output.txt')
  299. try:
  300. subprocess.check_call(['sudo', '-E', 'qubes-dom0-update', '-y'] +
  301. self.dom0_update_common_opts +
  302. [self.pkg_name],
  303. stdout=open(logpath, 'w'),
  304. stderr=subprocess.STDOUT
  305. )
  306. self.fail("qubes-dom0-update unexpectedly succeeded: " + open(
  307. logpath).read())
  308. except subprocess.CalledProcessError:
  309. pass
  310. retcode = subprocess.call(['rpm', '-q', '{}-1.0'.format(
  311. self.pkg_name)], stdout=open('/dev/null', 'w'))
  312. self.assertEqual(retcode, 1,
  313. 'UNSIGNED package {}-1.0 installed'.format(self.pkg_name))
  314. def load_tests(loader, tests, pattern):
  315. for template in qubes.tests.list_templates():
  316. tests.addTests(loader.loadTestsFromTestCase(
  317. type(
  318. 'TC_00_Dom0Upgrade_' + template,
  319. (TC_00_Dom0UpgradeMixin, qubes.tests.SystemTestCase),
  320. {'template': template})))
  321. return tests