dom0_update.py 14 KB

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