qvm_backup.py 9.3 KB

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