dom0_update.py 14 KB

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