qvm_backup.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  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 io
  21. import os
  22. import unittest.mock as mock
  23. import asyncio
  24. import qubesadmin.tests
  25. import qubesadmin.tests.tools
  26. import qubesadmin.tools.qvm_backup as qvm_backup
  27. class TC_00_qvm_backup(qubesadmin.tests.QubesTestCase):
  28. def test_000_write_backup_profile(self):
  29. args = qvm_backup.parser.parse_args(['/var/tmp'], app=self.app)
  30. profile = io.StringIO()
  31. qvm_backup.write_backup_profile(profile, args)
  32. expected_profile = (
  33. 'destination_path: /var/tmp\n'
  34. 'destination_vm: dom0\n'
  35. 'include: [\'$type:AppVM\', \'$type:TemplateVM\', '
  36. '\'$type:StandaloneVM\']\n'
  37. )
  38. self.assertEqual(profile.getvalue(), expected_profile)
  39. def test_001_write_backup_profile_include(self):
  40. self.app.expected_calls[('dom0', 'admin.vm.List', None, None)] = \
  41. b'0\0dom0 class=AdminVM state=Running\n' \
  42. b'vm1 class=AppVM state=Halted\n' \
  43. b'vm2 class=AppVM state=Halted\n' \
  44. b'vm3 class=AppVM state=Halted\n'
  45. args = qvm_backup.parser.parse_args(['/var/tmp', 'vm1', 'vm2'],
  46. app=self.app)
  47. profile = io.StringIO()
  48. qvm_backup.write_backup_profile(profile, args)
  49. expected_profile = (
  50. 'destination_path: /var/tmp\n'
  51. 'destination_vm: dom0\n'
  52. 'include: [vm1, vm2]\n'
  53. )
  54. self.assertEqual(profile.getvalue(), expected_profile)
  55. self.assertAllCalled()
  56. def test_002_write_backup_profile_exclude(self):
  57. args = qvm_backup.parser.parse_args([
  58. '-x', 'vm1', '-x', 'vm2', '/var/tmp'], app=self.app)
  59. profile = io.StringIO()
  60. qvm_backup.write_backup_profile(profile, args)
  61. expected_profile = (
  62. 'destination_path: /var/tmp\n'
  63. 'destination_vm: dom0\n'
  64. 'exclude: [vm1, vm2]\n'
  65. 'include: [\'$type:AppVM\', \'$type:TemplateVM\', '
  66. '\'$type:StandaloneVM\']\n'
  67. )
  68. self.assertEqual(profile.getvalue(), expected_profile)
  69. def test_003_write_backup_with_passphrase(self):
  70. args = qvm_backup.parser.parse_args(['/var/tmp'], app=self.app)
  71. profile = io.StringIO()
  72. qvm_backup.write_backup_profile(profile, args, passphrase='test123')
  73. expected_profile = (
  74. 'destination_path: /var/tmp\n'
  75. 'destination_vm: dom0\n'
  76. 'include: [\'$type:AppVM\', \'$type:TemplateVM\', '
  77. '\'$type:StandaloneVM\']\n'
  78. 'passphrase_text: test123\n'
  79. )
  80. self.assertEqual(profile.getvalue(), expected_profile)
  81. @mock.patch('qubesadmin.tools.qvm_backup.backup_profile_dir', '/tmp')
  82. @mock.patch('qubesadmin.tools.qvm_backup.input', create=True)
  83. @mock.patch('getpass.getpass')
  84. def test_010_main_save_profile_cancel(self, mock_getpass, mock_input):
  85. asyncio.set_event_loop(asyncio.new_event_loop())
  86. mock_input.return_value = 'n'
  87. mock_getpass.return_value = 'some password'
  88. self.app.qubesd_connection_type = 'socket'
  89. self.app.expected_calls[('dom0', 'admin.backup.Info', 'test-profile',
  90. None)] = \
  91. b'0\0backup summary'
  92. profile_path = '/tmp/test-profile.conf'
  93. try:
  94. qvm_backup.main(['--save-profile', 'test-profile', '/var/tmp'],
  95. app=self.app)
  96. expected_profile = (
  97. 'destination_path: /var/tmp\n'
  98. 'destination_vm: dom0\n'
  99. 'include: [\'$type:AppVM\', \'$type:TemplateVM\', '
  100. '\'$type:StandaloneVM\']\n'
  101. )
  102. with open(profile_path) as f:
  103. self.assertEqual(expected_profile, f.read())
  104. finally:
  105. os.unlink(profile_path)
  106. @mock.patch('qubesadmin.tools.qvm_backup.backup_profile_dir', '/tmp')
  107. @mock.patch('qubesadmin.tools.qvm_backup.input', create=True)
  108. @mock.patch('getpass.getpass')
  109. def test_011_main_save_profile_confirm(self, mock_getpass, mock_input):
  110. asyncio.set_event_loop(asyncio.new_event_loop())
  111. mock_input.return_value = 'y'
  112. mock_getpass.return_value = 'some password'
  113. self.app.qubesd_connection_type = 'socket'
  114. self.app.expected_calls[('dom0', 'admin.backup.Info', 'test-profile',
  115. None)] = \
  116. b'0\0backup summary'
  117. self.app.expected_calls[('dom0', 'admin.backup.Execute', 'test-profile',
  118. None)] = \
  119. b'0\0'
  120. profile_path = '/tmp/test-profile.conf'
  121. try:
  122. qvm_backup.main(['--save-profile', 'test-profile', '/var/tmp'],
  123. app=self.app)
  124. expected_profile = (
  125. 'destination_path: /var/tmp\n'
  126. 'destination_vm: dom0\n'
  127. 'include: [\'$type:AppVM\', \'$type:TemplateVM\', '
  128. '\'$type:StandaloneVM\']\n'
  129. 'passphrase_text: some password\n'
  130. )
  131. with open(profile_path) as f:
  132. self.assertEqual(expected_profile, f.read())
  133. finally:
  134. os.unlink(profile_path)
  135. @mock.patch('qubesadmin.tools.qvm_backup.backup_profile_dir', '/tmp')
  136. @mock.patch('qubesadmin.tools.qvm_backup.input', create=True)
  137. @mock.patch('getpass.getpass')
  138. def test_012_main_existing_profile(self, mock_getpass, mock_input):
  139. asyncio.set_event_loop(asyncio.new_event_loop())
  140. mock_input.return_value = 'y'
  141. mock_getpass.return_value = 'some password'
  142. self.app.qubesd_connection_type = 'socket'
  143. self.app.expected_calls[('dom0', 'admin.backup.Info', 'test-profile',
  144. None)] = \
  145. b'0\0backup summary'
  146. self.app.expected_calls[('dom0', 'admin.backup.Execute', 'test-profile',
  147. None)] = \
  148. b'0\0'
  149. self.app.expected_calls[('dom0', 'admin.Events', None,
  150. None)] = \
  151. b'0\0'
  152. try:
  153. patch = mock.patch(
  154. 'qubesadmin.events.EventsDispatcher._get_events_reader')
  155. mock_events = patch.start()
  156. self.addCleanup(patch.stop)
  157. mock_events.side_effect = qubesadmin.tests.tools.MockEventsReader([
  158. b'1\0\0connection-established\0\0',
  159. b'1\0\0backup-progress\0backup_profile\0test-profile\0progress\x000'
  160. b'.25\0\0',
  161. ])
  162. except ImportError:
  163. pass
  164. qvm_backup.main(['--profile', 'test-profile'],
  165. app=self.app)
  166. self.assertFalse(os.path.exists('/tmp/test-profile.conf'))
  167. self.assertFalse(mock_getpass.called)
  168. @mock.patch('qubesadmin.tools.qvm_backup.backup_profile_dir', '/tmp')
  169. @mock.patch('qubesadmin.tools.qvm_backup.input', create=True)
  170. @mock.patch('getpass.getpass')
  171. def test_013_main_new_profile_vm(self, mock_getpass, mock_input):
  172. asyncio.set_event_loop(asyncio.new_event_loop())
  173. mock_input.return_value = 'y'
  174. mock_getpass.return_value = 'some password'
  175. self.app.qubesd_connection_type = 'qrexec'
  176. with qubesadmin.tests.tools.StdoutBuffer() as stdout:
  177. qvm_backup.main(['-x', 'vm1', '/var/tmp'],
  178. app=self.app)
  179. expected_output = (
  180. 'To perform the backup according to selected options, create '
  181. 'backup profile (/tmp/profile_name.conf) in dom0 with following '
  182. 'content:\n'
  183. 'destination_path: /var/tmp\n'
  184. 'destination_vm: dom0\n'
  185. 'exclude: [vm1]\n'
  186. 'include: [\'$type:AppVM\', \'$type:TemplateVM\', '
  187. '\'$type:StandaloneVM\']\n'
  188. '# specify backup passphrase below\n'
  189. 'passphrase_text: ...\n'
  190. )
  191. self.assertEqual(stdout.getvalue(), expected_output)
  192. @mock.patch('qubesadmin.tools.qvm_backup.backup_profile_dir', '/tmp')
  193. @mock.patch('qubesadmin.tools.qvm_backup.input', create=True)
  194. @mock.patch('getpass.getpass')
  195. def test_014_main_passphrase_file(self, mock_getpass, mock_input):
  196. asyncio.set_event_loop(asyncio.new_event_loop())
  197. mock_input.return_value = 'y'
  198. mock_getpass.return_value = 'some password'
  199. self.app.qubesd_connection_type = 'socket'
  200. self.app.expected_calls[('dom0', 'admin.backup.Info', 'test-profile',
  201. None)] = \
  202. b'0\0backup summary'
  203. self.app.expected_calls[('dom0', 'admin.backup.Execute', 'test-profile',
  204. None)] = \
  205. b'0\0'
  206. profile_path = '/tmp/test-profile.conf'
  207. try:
  208. stdin = io.StringIO()
  209. stdin.write('other passphrase\n')
  210. stdin.seek(0)
  211. with mock.patch('sys.stdin', stdin):
  212. qvm_backup.main(['--passphrase-file', '-', '--save-profile',
  213. 'test-profile', '/var/tmp'],
  214. app=self.app)
  215. expected_profile = (
  216. 'destination_path: /var/tmp\n'
  217. 'destination_vm: dom0\n'
  218. 'include: [\'$type:AppVM\', \'$type:TemplateVM\', '
  219. '\'$type:StandaloneVM\']\n'
  220. 'passphrase_text: other passphrase\n'
  221. )
  222. with open(profile_path) as f:
  223. self.assertEqual(expected_profile, f.read())
  224. finally:
  225. os.unlink(profile_path)