qvm_create.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. # -*- encoding: utf8 -*-
  2. #
  3. # The Qubes OS Project, http://www.qubes-os.org
  4. #
  5. # Copyright (C) 2017 Marek Marczykowski-Górecki
  6. # <marmarek@invisiblethingslab.com>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU Lesser General Public License as published by
  10. # the Free Software Foundation; either version 2.1 of the License, or
  11. # (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 Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public License along
  19. # with this program; if not, see <http://www.gnu.org/licenses/>.
  20. import os
  21. import tempfile
  22. import unittest.mock
  23. import subprocess
  24. import qubesadmin.tests
  25. import qubesadmin.tests.tools
  26. import qubesadmin.tools.qvm_create
  27. class TC_00_qvm_create(qubesadmin.tests.QubesTestCase):
  28. def test_000_just_appvm(self):
  29. self.app.expected_calls[('dom0', 'admin.vm.Create.AppVM', None,
  30. b'name=new-vm label=red')] = b'0\x00'
  31. self.app.expected_calls[('dom0', 'admin.label.List', None, None)] = \
  32. b'0\x00red\nblue\n'
  33. self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
  34. b'0\x00new-vm class=AppVM state=Halted\n'
  35. qubesadmin.tools.qvm_create.main(['-l', 'red', 'new-vm'], app=self.app)
  36. self.assertAllCalled()
  37. def test_001_missing_vm(self):
  38. with self.assertRaises(SystemExit):
  39. with qubesadmin.tests.tools.StderrBuffer() as stderr:
  40. qubesadmin.tools.qvm_create.main(['-l', 'red'], app=self.app)
  41. self.assertIn('NAME', stderr.getvalue())
  42. self.assertAllCalled()
  43. def test_002_custom_template(self):
  44. self.app.expected_calls[('dom0', 'admin.vm.Create.AppVM',
  45. 'some-template', b'name=new-vm label=red')] = b'0\x00'
  46. self.app.expected_calls[('dom0', 'admin.label.List', None, None)] = \
  47. b'0\x00red\nblue\n'
  48. self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
  49. b'0\x00new-vm class=AppVM state=Halted\n'
  50. qubesadmin.tools.qvm_create.main(['-l', 'red', '-t',
  51. 'some-template', 'new-vm'], app=self.app)
  52. self.assertAllCalled()
  53. def test_003_properties(self):
  54. self.app.expected_calls[('dom0', 'admin.vm.Create.AppVM',
  55. None, b'name=new-vm label=red')] = b'0\x00'
  56. self.app.expected_calls[('dom0', 'admin.label.List', None, None)] = \
  57. b'0\x00red\nblue\n'
  58. self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
  59. b'0\x00new-vm class=AppVM state=Halted\n'
  60. self.app.expected_calls[('new-vm', 'admin.vm.property.Set',
  61. 'netvm', b'sys-whonix')] = b'0\x00'
  62. qubesadmin.tools.qvm_create.main(['-l', 'red', '--prop',
  63. 'netvm=sys-whonix', 'new-vm'],
  64. app=self.app)
  65. self.assertAllCalled()
  66. def test_004_pool(self):
  67. self.app.expected_calls[('dom0', 'admin.vm.CreateInPool.AppVM',
  68. None, b'name=new-vm label=red pool=some-pool')] = b'0\x00'
  69. self.app.expected_calls[('dom0', 'admin.label.List', None, None)] = \
  70. b'0\x00red\nblue\n'
  71. self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
  72. b'0\x00new-vm class=AppVM state=Halted\n'
  73. qubesadmin.tools.qvm_create.main(['-l', 'red', '-P', 'some-pool',
  74. 'new-vm'],
  75. app=self.app)
  76. self.assertAllCalled()
  77. def test_005_pools(self):
  78. self.app.expected_calls[('dom0', 'admin.vm.CreateInPool.AppVM',
  79. None, b'name=new-vm label=red pool:private=some-pool '
  80. b'pool:volatile=other-pool')] = b'0\x00'
  81. self.app.expected_calls[('dom0', 'admin.label.List', None, None)] = \
  82. b'0\x00red\nblue\n'
  83. self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
  84. b'0\x00new-vm class=AppVM state=Halted\n'
  85. qubesadmin.tools.qvm_create.main(['-l', 'red', '--pool',
  86. 'private=some-pool', '--pool', 'volatile=other-pool', 'new-vm'],
  87. app=self.app)
  88. self.assertAllCalled()
  89. def test_005_root_copy_from(self):
  90. with tempfile.NamedTemporaryFile() as root_file:
  91. root_file.file.write(b'root data')
  92. root_file.file.flush()
  93. self.app.expected_calls[('dom0', 'admin.vm.Create.StandaloneVM',
  94. None, b'name=new-vm label=red')] = b'0\x00'
  95. self.app.expected_calls[('dom0', 'admin.label.List', None, None)] = \
  96. b'0\x00red\nblue\n'
  97. self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
  98. b'0\x00new-vm class=AppVM state=Halted\n'
  99. self.app.expected_calls[
  100. ('new-vm', 'admin.vm.volume.List', None, None)] = \
  101. b'0\x00root\nprivate\nvolatile\nkernel\n'
  102. self.app.expected_calls[
  103. ('new-vm', 'admin.vm.volume.Info', 'root', None)] = \
  104. b'0\x00' \
  105. b'pool=other-pool\n' \
  106. b'vid=new-vm-root\n' \
  107. b'size=10000000\n'
  108. self.app.expected_calls[
  109. ('new-vm', 'admin.vm.volume.Import', 'root', b'root data')] = \
  110. b'0\0'
  111. qubesadmin.tools.qvm_create.main(['-l', 'red', '-C', 'StandaloneVM',
  112. '--root-copy-from=' + root_file.name, 'new-vm'],
  113. app=self.app)
  114. self.assertAllCalled()
  115. self.assertTrue(os.path.exists(root_file.name))
  116. def test_006_root_move_from(self):
  117. with tempfile.NamedTemporaryFile(delete=False) as root_file:
  118. root_file.file.write(b'root data')
  119. root_file.file.flush()
  120. self.app.expected_calls[('dom0', 'admin.vm.Create.StandaloneVM',
  121. None, b'name=new-vm label=red')] = b'0\x00'
  122. self.app.expected_calls[('dom0', 'admin.label.List', None, None)] = \
  123. b'0\x00red\nblue\n'
  124. self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
  125. b'0\x00new-vm class=AppVM state=Halted\n'
  126. self.app.expected_calls[
  127. ('new-vm', 'admin.vm.volume.List', None, None)] = \
  128. b'0\x00root\nprivate\nvolatile\nkernel\n'
  129. self.app.expected_calls[
  130. ('new-vm', 'admin.vm.volume.Info', 'root', None)] = \
  131. b'0\x00' \
  132. b'pool=other-pool\n' \
  133. b'vid=new-vm-root\n' \
  134. b'size=20000000\n'
  135. self.app.expected_calls[
  136. ('new-vm', 'admin.vm.volume.Import', 'root', b'root data')] = \
  137. b'0\0'
  138. qubesadmin.tools.qvm_create.main(['-l', 'red', '-C', 'StandaloneVM',
  139. '--root-move-from=' + root_file.name, 'new-vm'],
  140. app=self.app)
  141. self.assertAllCalled()
  142. self.assertFalse(os.path.exists(root_file.name))
  143. def test_007_root_move_copy_both(self):
  144. with tempfile.NamedTemporaryFile() as root_file:
  145. root_file.file.write(b'root data')
  146. root_file.file.flush()
  147. with self.assertRaises(SystemExit):
  148. qubesadmin.tools.qvm_create.main(['-l', 'red', '-C', 'StandaloneVM',
  149. '--root-copy-from=' + root_file.name,
  150. '--root-move-from=' + root_file.name,
  151. 'new-vm'],
  152. app=self.app)
  153. self.assertAllCalled()
  154. self.assertTrue(os.path.exists(root_file.name))
  155. def test_008_root_invalid_path(self):
  156. with self.assertRaises(SystemExit):
  157. qubesadmin.tools.qvm_create.main(['-l', 'red', '-C', 'StandaloneVM',
  158. '--root-copy-from=/invalid', 'new-vm'],
  159. app=self.app)
  160. self.assertAllCalled()
  161. def test_009_help_classes(self):
  162. self.app.expected_calls[('dom0', 'admin.vmclass.List',
  163. None, None)] = b'0\x00StandaloneVM\nAppVM\nTemplateVM\nDispVM\n'
  164. with qubesadmin.tests.tools.StdoutBuffer() as stdout:
  165. qubesadmin.tools.qvm_create.main(['--help-classes'],
  166. app=self.app)
  167. self.assertEqual(stdout.getvalue(),
  168. 'AppVM\nDispVM\nStandaloneVM\nTemplateVM\n')
  169. self.assertAllCalled()
  170. def test_010_root_copy_from_with_resize(self):
  171. with tempfile.NamedTemporaryFile() as root_file:
  172. root_file.file.write(b'root data')
  173. root_file.file.flush()
  174. self.app.expected_calls[('dom0', 'admin.vm.Create.StandaloneVM',
  175. None, b'name=new-vm label=red')] = b'0\x00'
  176. self.app.expected_calls[('dom0', 'admin.label.List', None, None)] = \
  177. b'0\x00red\nblue\n'
  178. self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
  179. b'0\x00new-vm class=AppVM state=Halted\n'
  180. self.app.expected_calls[
  181. ('new-vm', 'admin.vm.volume.List', None, None)] = \
  182. b'0\x00root\nprivate\nvolatile\nkernel\n'
  183. self.app.expected_calls[
  184. ('new-vm', 'admin.vm.volume.Info', 'root', None)] = \
  185. b'0\x00' \
  186. b'pool=other-pool\n' \
  187. b'vid=new-vm-root\n' \
  188. b'size=2\n'
  189. self.app.expected_calls[
  190. ('new-vm', 'admin.vm.volume.Resize', 'root', b'9')] = \
  191. b'0\0'
  192. self.app.expected_calls[
  193. ('new-vm', 'admin.vm.volume.Import', 'root', b'root data')] = \
  194. b'0\0'
  195. qubesadmin.tools.qvm_create.main(['-l', 'red', '-C', 'StandaloneVM',
  196. '--root-copy-from=' + root_file.name, 'new-vm'],
  197. app=self.app)
  198. self.assertAllCalled()
  199. self.assertTrue(os.path.exists(root_file.name))
  200. @unittest.mock.patch('subprocess.check_output')
  201. def test_011_standalonevm(self, check_output_mock):
  202. self.app.expected_calls[('dom0', 'admin.label.List', None, None)] = \
  203. b'0\x00red\nblue\n'
  204. self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
  205. b'0\x00template class=TemplateVM state=Halted\n' \
  206. b'new-vm class=StandaloneVM state=Halted\n'
  207. self.app.expected_calls[
  208. ('template', 'admin.vm.property.Get', 'label', None)] = \
  209. b'0\x00default=False type=label blue'
  210. self.app.expected_calls[
  211. ('template', 'admin.vm.property.Get', 'vcpus', None)] = \
  212. b'0\x00default=False type=int 2'
  213. self.app.expected_calls[
  214. ('template', 'admin.vm.property.Get', 'kernel', None)] = \
  215. b'0\x00default=True type=str kernel-version'
  216. self.app.expected_calls[
  217. ('template', 'admin.vm.property.Get', 'memory', None)] = \
  218. b'0\x00default=True type=int 400'
  219. self.app.expected_calls[
  220. ('template', 'admin.vm.property.Get', 'template', None)] = \
  221. b'2\x00QubesNoSuchPropertyError\x00\x00No such property\x00'
  222. self.app.expected_calls[
  223. ('template', 'admin.vm.property.List', None, None)] = \
  224. b'0\x00name\n' \
  225. b'label\n' \
  226. b'vcpus\n' \
  227. b'kernel\n' \
  228. b'memory\n'
  229. self.app.expected_calls[
  230. ('template', 'admin.vm.tag.List', None, None)] = \
  231. b'0\x00'
  232. self.app.expected_calls[
  233. ('template', 'admin.vm.feature.List', None, None)] = \
  234. b'0\x00'
  235. self.app.expected_calls[
  236. ('template', 'admin.vm.firewall.Get', None, None)] = \
  237. b'0\x00'
  238. self.app.expected_calls[('dom0', 'admin.vm.Create.StandaloneVM', None,
  239. b'name=new-vm label=blue')] = b'0\x00'
  240. # TODO this is weird...
  241. self.app.expected_calls[
  242. ('new-vm', 'admin.vm.property.Set', 'label', b'red')] = \
  243. b'0\x00'
  244. self.app.expected_calls[
  245. ('new-vm', 'admin.vm.property.Set', 'vcpus', b'2')] = \
  246. b'0\x00'
  247. self.app.expected_calls[
  248. ('new-vm', 'admin.vm.firewall.Set', None, b'')] = \
  249. b'0\x00'
  250. self.app.expected_calls[
  251. ('template', 'admin.vm.volume.List', None, None)] = \
  252. b'0\x00root\nprivate\nvolatile\nkernel\n'
  253. self.app.expected_calls[
  254. ('new-vm', 'admin.vm.volume.List', None, None)] = \
  255. b'0\x00root\nprivate\nvolatile\nkernel\n'
  256. self.app.expected_calls[
  257. ('new-vm', 'admin.vm.volume.Info', 'root', None)] = \
  258. b'0\x00' \
  259. b'snap_on_start=False\n' \
  260. b'save_on_stop=True\n' \
  261. b'pool=other-pool\n' \
  262. b'vid=new-vm-root\n' \
  263. b'rw=True\n' \
  264. b'size=2\n'
  265. self.app.expected_calls[
  266. ('new-vm', 'admin.vm.volume.Info', 'private', None)] = \
  267. b'0\x00' \
  268. b'snap_on_start=False\n' \
  269. b'save_on_stop=True\n' \
  270. b'pool=other-pool\n' \
  271. b'vid=new-vm-private\n' \
  272. b'rw=True\n' \
  273. b'size=2\n'
  274. self.app.expected_calls[
  275. ('new-vm', 'admin.vm.volume.Info', 'volatile', None)] = \
  276. b'0\x00' \
  277. b'snap_on_start=False\n' \
  278. b'save_on_stop=False\n' \
  279. b'pool=other-pool\n' \
  280. b'vid=new-vm-volatile\n' \
  281. b'rw=True\n' \
  282. b'size=2\n'
  283. self.app.expected_calls[
  284. ('new-vm', 'admin.vm.volume.Info', 'kernel', None)] = \
  285. b'0\x00' \
  286. b'snap_on_start=False\n' \
  287. b'save_on_stop=False\n' \
  288. b'pool=linux-kernel\n' \
  289. b'vid=kernel-version\n' \
  290. b'rw=False\n' \
  291. b'size=2\n'
  292. self.app.expected_calls[
  293. ('template', 'admin.vm.volume.CloneFrom', 'root', None)] = \
  294. b'0\0clone-cookie'
  295. self.app.expected_calls[
  296. ('new-vm', 'admin.vm.volume.CloneTo', 'root', b'clone-cookie')] = \
  297. b'0\0'
  298. qubesadmin.tools.qvm_create.main(['-C', 'StandaloneVM',
  299. '-t', 'template', '-l', 'red', 'new-vm'],
  300. app=self.app)
  301. check_output_mock.assert_called_once_with(
  302. ['qvm-appmenus', '--init', '--update',
  303. '--source', 'template', 'new-vm'],
  304. stderr=subprocess.STDOUT)
  305. self.assertAllCalled()
  306. def test_012_invalid_label(self):
  307. self.app.expected_calls[('dom0', 'admin.label.List', None, None)] = \
  308. b'0\x00red\nblue\n'
  309. with self.assertRaises(SystemExit):
  310. with qubesadmin.tests.tools.StderrBuffer() as stderr:
  311. qubesadmin.tools.qvm_create.main(['-l', 'invalid', 'name'],
  312. app=self.app)
  313. self.assertIn('red, blue', stderr.getvalue())
  314. self.assertAllCalled()
  315. def test_013_root_copy_from_template_based(self):
  316. with tempfile.NamedTemporaryFile() as root_file:
  317. root_file.file.write(b'root data')
  318. root_file.file.flush()
  319. with self.assertRaises(SystemExit):
  320. with qubesadmin.tests.tools.StderrBuffer() as stderr:
  321. qubesadmin.tools.qvm_create.main(['-l', 'red',
  322. '--root-copy-from=' + root_file.name, 'new-vm'],
  323. app=self.app)
  324. self.assertIn('--root-copy-from', stderr.getvalue())
  325. self.assertAllCalled()
  326. def test_014_standalone_shortcut(self):
  327. self.app.expected_calls[('dom0', 'admin.vm.Create.StandaloneVM',
  328. None, b'name=new-vm label=red')] = b'0\x00'
  329. self.app.expected_calls[('dom0', 'admin.label.List', None, None)] = \
  330. b'0\x00red\nblue\n'
  331. self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
  332. b'0\x00new-vm class=StandaloneVM state=Halted\n'
  333. qubesadmin.tools.qvm_create.main(['-l', 'red', '--standalone', 'new-vm'],
  334. app=self.app)
  335. self.assertAllCalled()
  336. def test_015_disp_shortcut(self):
  337. self.app.expected_calls[('dom0', 'admin.vm.Create.DispVM',
  338. None, b'name=new-vm label=red')] = b'0\x00'
  339. self.app.expected_calls[('dom0', 'admin.label.List', None, None)] = \
  340. b'0\x00red\nblue\n'
  341. self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
  342. b'0\x00new-vm class=DispVM state=Halted\n'
  343. qubesadmin.tools.qvm_create.main(['--disp', 'new-vm'],
  344. app=self.app)
  345. self.assertAllCalled()