dom0_update.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  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. 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-buid not installed')
  33. class TC_00_Dom0Upgrade(unittest.TestCase):
  34. cleanup_paths = []
  35. pkg_name = 'qubes-test-pkg'
  36. dom0_update_common_opts = ['--disablerepo=*', '--enablerepo=test',
  37. '--setopt=test.copy_local=1']
  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))
  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.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_Dom0Upgrade, cls).setUpClass()
  67. cls.tmpdir = tempfile.mkdtemp()
  68. cls.cleanup_paths += [cls.tmpdir]
  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('''
  74. [test]
  75. name = Test
  76. baseurl = file:///tmp/repo
  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. for dir in cls.cleanup_paths:
  86. shutil.rmtree(dir)
  87. cls.cleanup_paths = []
  88. def setUp(self):
  89. self.qc = QubesVmCollection()
  90. self.qc.lock_db_for_writing()
  91. self.qc.load()
  92. self.updatevm = self.qc.add_new_vm("QubesProxyVm",
  93. name="%supdatevm" % VM_PREFIX,
  94. template=self.qc.get_default_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 remove_vms(self, vms):
  106. self.qc.lock_db_for_writing()
  107. self.qc.load()
  108. self.qc.set_updatevm_vm(self.qc[self.saved_updatevm.qid])
  109. for vm in vms:
  110. if isinstance(vm, str):
  111. vm = self.qc.get_vm_by_name(vm)
  112. else:
  113. vm = self.qc[vm.qid]
  114. if vm.is_running():
  115. try:
  116. vm.force_shutdown()
  117. except:
  118. pass
  119. try:
  120. vm.remove_from_disk()
  121. except OSError:
  122. pass
  123. self.qc.pop(vm.qid)
  124. self.qc.save()
  125. self.qc.unlock_db()
  126. def tearDown(self):
  127. vmlist = [vm for vm in self.qc.values() if vm.name.startswith(
  128. VM_PREFIX)]
  129. self.remove_vms(vmlist)
  130. subprocess.call(['sudo', 'rpm', '-e', self.pkg_name], stderr=open(
  131. os.devnull, 'w'))
  132. subprocess.call(['sudo', 'rpm', '-e', 'gpg-pubkey-{}'.format(
  133. self.keyid)], stderr=open(os.devnull, 'w'))
  134. for pkg in os.listdir(self.tmpdir):
  135. if pkg.endswith('.rpm'):
  136. os.unlink(pkg)
  137. def create_pkg(self, dir, name, version):
  138. spec_path = os.path.join(dir, name+'.spec')
  139. spec = open(spec_path, 'w')
  140. spec.write(
  141. '''
  142. Name: {name}
  143. Summary: Test Package
  144. Version: {version}
  145. Release: 1
  146. Vendor: Invisible Things Lab
  147. License: GPL
  148. Group: Qubes
  149. URL: http://www.qubes-os.org
  150. %description
  151. Test package
  152. %install
  153. %files
  154. '''.format(name=name, version=version)
  155. )
  156. spec.close()
  157. subprocess.check_call(
  158. ['rpmbuild', '--quiet', '-bb', '--define', '_rpmdir {}'.format(dir),
  159. spec_path])
  160. pkg_path = os.path.join(dir, 'x86_64',
  161. '{}-{}-1.x86_64.rpm'.format(name, version))
  162. subprocess.check_call(['sudo', 'chmod', 'go-rw', '/dev/tty'])
  163. subprocess.check_call(
  164. ['rpm', '--quiet', '--define=_gpg_path {}'.format(dir),
  165. '--define=_gpg_name {}'.format("Qubes test"),
  166. '--addsign', pkg_path],
  167. stdin=open(os.devnull),
  168. stdout=open(os.devnull, 'w'),
  169. stderr=subprocess.STDOUT)
  170. subprocess.check_call(['sudo', 'chmod', 'go+rw', '/dev/tty'])
  171. return pkg_path
  172. def send_pkg(self, filename):
  173. p = self.updatevm.run('mkdir -p /tmp/repo; cat > /tmp/repo/{}'.format(
  174. os.path.basename(
  175. filename)), passio_popen=True)
  176. p.stdin.write(open(filename).read())
  177. p.stdin.close()
  178. p.wait()
  179. self.updatevm.run('cd /tmp/repo; createrepo .', wait=True)
  180. def test_000_update(self):
  181. filename = self.create_pkg(self.tmpdir, self.pkg_name, '1.0')
  182. subprocess.check_call(['sudo', 'rpm', '-i', filename])
  183. filename = self.create_pkg(self.tmpdir, self.pkg_name, '2.0')
  184. self.send_pkg(filename)
  185. logpath = os.path.join(self.tmpdir, 'dom0-update-output.txt')
  186. try:
  187. subprocess.check_call(['sudo', '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. def test_010_instal(self):
  203. filename = self.create_pkg(self.tmpdir, self.pkg_name, '1.0')
  204. self.send_pkg(filename)
  205. logpath = os.path.join(self.tmpdir, 'dom0-update-output.txt')
  206. try:
  207. subprocess.check_call(['sudo', 'qubes-dom0-update', '-y'] +
  208. self.dom0_update_common_opts + [
  209. self.pkg_name],
  210. stdout=open(logpath, 'w'),
  211. stderr=subprocess.STDOUT)
  212. except subprocess.CalledProcessError:
  213. self.fail("qubes-dom0-update failed: " + open(
  214. logpath).read())
  215. retcode = subprocess.call(['rpm', '-q', '{}-1.0'.format(
  216. self.pkg_name)], stdout=open('/dev/null', 'w'))
  217. self.assertEqual(retcode, 0, 'Package {}-1.0 not installed'.format(
  218. self.pkg_name))
  219. def test_020_install_wrong_sign(self):
  220. subprocess.call(['sudo', 'rpm', '-e', 'gpg-pubkey-{}'.format(
  221. self.keyid)])
  222. filename = self.create_pkg(self.tmpdir, self.pkg_name, '1.0')
  223. self.send_pkg(filename)
  224. logpath = os.path.join(self.tmpdir, 'dom0-update-output.txt')
  225. try:
  226. subprocess.check_call(['sudo', 'qubes-dom0-update', '-y'] +
  227. self.dom0_update_common_opts + [
  228. self.pkg_name],
  229. stdout=open(logpath, 'w'),
  230. stderr=subprocess.STDOUT)
  231. self.fail("qubes-dom0-update unexpectedly succeeded: " + open(
  232. logpath).read())
  233. except subprocess.CalledProcessError:
  234. pass
  235. retcode = subprocess.call(['rpm', '-q', '{}-1.0'.format(
  236. self.pkg_name)], stdout=open('/dev/null', 'w'))
  237. self.assertEqual(retcode, 1,
  238. 'Package {}-1.0 installed although '
  239. 'signature is invalid'.format(self.pkg_name))
  240. def test_030_install_unsigned(self):
  241. filename = self.create_pkg(self.tmpdir, self.pkg_name, '1.0')
  242. subprocess.check_call(['rpm', '--delsign', filename],
  243. stdout=open(os.devnull, 'w'),
  244. stderr=subprocess.STDOUT)
  245. self.send_pkg(filename)
  246. logpath = os.path.join(self.tmpdir, 'dom0-update-output.txt')
  247. try:
  248. subprocess.check_call(['sudo', 'qubes-dom0-update', '-y'] +
  249. self.dom0_update_common_opts +
  250. [self.pkg_name],
  251. stdout=open(logpath, 'w'),
  252. stderr=subprocess.STDOUT
  253. )
  254. self.fail("qubes-dom0-update unexpectedly succeeded: " + open(
  255. logpath).read())
  256. except subprocess.CalledProcessError:
  257. pass
  258. retcode = subprocess.call(['rpm', '-q', '{}-1.0'.format(
  259. self.pkg_name)], stdout=open('/dev/null', 'w'))
  260. self.assertEqual(retcode, 1,
  261. 'UNSIGNED package {}-1.0 installed'.format(self.pkg_name))