dom0_update.py 14 KB

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