qvm_backup_restore.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  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 itertools
  21. import qubesadmin.tests
  22. import qubesadmin.tests.tools
  23. import qubesadmin.tools.qvm_backup_restore
  24. from unittest import mock
  25. from qubesadmin.backup import BackupVM
  26. from qubesadmin.backup.restore import BackupRestore
  27. from qubesadmin.backup.dispvm import RestoreInDisposableVM
  28. class TC_00_qvm_backup_restore(qubesadmin.tests.QubesTestCase):
  29. def setUp(self):
  30. super(TC_00_qvm_backup_restore, self).setUp()
  31. def tearDown(self):
  32. super(TC_00_qvm_backup_restore, self).tearDown()
  33. @mock.patch('qubesadmin.tools.qvm_backup_restore.input', create=True)
  34. @mock.patch('getpass.getpass')
  35. @mock.patch('qubesadmin.tools.qvm_backup_restore.BackupRestore')
  36. def test_000_simple(self, mock_backup, mock_getpass, mock_input):
  37. mock_getpass.return_value = 'testpass'
  38. mock_input.return_value = 'Y'
  39. vm1 = BackupVM()
  40. vm1.name = 'test-vm'
  41. vm1.backup_path = 'path/in/backup'
  42. vm1.template = None
  43. vm1.klass = 'StandaloneVM'
  44. vm1.label = 'red'
  45. mock_restore_info = {
  46. 1: BackupRestore.VMToRestore(vm1),
  47. }
  48. mock_backup.configure_mock(**{
  49. 'return_value.get_restore_summary.return_value': '',
  50. 'return_value.get_restore_info.return_value': mock_restore_info,
  51. })
  52. with mock.patch('qubesadmin.tools.qvm_backup_restore.handle_broken') \
  53. as mock_handle_broken:
  54. qubesadmin.tools.qvm_backup_restore.main(['/some/path'],
  55. app=self.app)
  56. mock_handle_broken.assert_called_once_with(
  57. self.app, mock.ANY, mock_restore_info)
  58. mock_backup.assert_called_once_with(
  59. self.app, '/some/path', None, 'testpass',
  60. force_compression_filter=None, location_is_service=False)
  61. self.assertAllCalled()
  62. @mock.patch('qubesadmin.tools.qvm_backup_restore.input', create=True)
  63. @mock.patch('getpass.getpass')
  64. @mock.patch('qubesadmin.tools.qvm_backup_restore.BackupRestore')
  65. def test_001_selected_vms(self, mock_backup, mock_getpass, mock_input):
  66. mock_getpass.return_value = 'testpass'
  67. mock_input.return_value = 'Y'
  68. vm1 = BackupVM()
  69. vm1.name = 'test-vm'
  70. vm1.backup_path = 'path/in/backup'
  71. vm1.template = None
  72. vm1.klass = 'StandaloneVM'
  73. vm1.label = 'red'
  74. vm2 = BackupVM()
  75. vm2.name = 'test-vm2'
  76. vm2.backup_path = 'path/in/backup2'
  77. vm2.template = None
  78. vm2.klass = 'StandaloneVM'
  79. vm2.label = 'red'
  80. mock_restore_info = {
  81. 1: BackupRestore.VMToRestore(vm1),
  82. 2: BackupRestore.VMToRestore(vm2),
  83. }
  84. exclude_list = []
  85. mock_backup.configure_mock(**{
  86. 'return_value.get_restore_summary.return_value': '',
  87. 'return_value.options.exclude': exclude_list,
  88. 'return_value.get_restore_info.return_value': mock_restore_info,
  89. })
  90. qubesadmin.tools.qvm_backup_restore.main(['/some/path', 'test-vm'],
  91. app=self.app)
  92. mock_backup.assert_called_once_with(
  93. self.app, '/some/path', None, 'testpass',
  94. force_compression_filter=None, location_is_service=False)
  95. self.assertEqual(mock_backup.return_value.options.exclude, ['test-vm2'])
  96. self.assertAllCalled()
  97. def test_010_handle_broken_no_problems(self):
  98. vm1 = BackupVM()
  99. vm1.name = 'test-vm'
  100. vm1.backup_path = 'path/in/backup'
  101. vm1.template = None
  102. vm1.klass = 'StandaloneVM'
  103. vm1.label = 'red'
  104. mock_restore_info = {
  105. 1: BackupRestore.VMToRestore(vm1),
  106. }
  107. mock_args = mock.Mock()
  108. mock_args.verify_only = False
  109. self.app.log = mock.Mock()
  110. qubesadmin.tools.qvm_backup_restore.handle_broken(
  111. self.app, mock_args, mock_restore_info)
  112. self.assertEqual(self.app.log.mock_calls, [
  113. mock.call.info(
  114. 'The above VMs will be copied and added to your system.'),
  115. mock.call.info(
  116. 'Exisiting VMs will NOT be removed.'),
  117. ])
  118. def assertAppropriateLogging(self, missing_name, action):
  119. '''
  120. :param missing_name: NetVM or TemplateVM
  121. :param action: 'skip_broken', 'ignore_missing'
  122. :return:
  123. '''
  124. expected_calls = [
  125. mock.call.info(
  126. 'The above VMs will be copied and added to your system.'),
  127. mock.call.info(
  128. 'Exisiting VMs will NOT be removed.'),
  129. mock.call.warning(
  130. '*** One or more {}s are missing on the host! ***'.format(
  131. missing_name)),
  132. ]
  133. if action == 'skip_broken':
  134. expected_calls.append(
  135. mock.call.warning(
  136. 'Skipping broken entries: VMs that depend on missing {}s '
  137. 'will NOT be restored.'.format(missing_name))
  138. )
  139. elif action == 'ignore_missing':
  140. expected_calls.append(
  141. mock.call.warning(
  142. 'Ignoring missing entries: VMs that depend on missing '
  143. '{}s will have default value assigned.'.format(
  144. missing_name))
  145. )
  146. self.assertEqual(self.app.log.mock_calls, expected_calls)
  147. def test_011_handle_broken_missing_template(self):
  148. vm1 = BackupVM()
  149. vm1.name = 'test-vm'
  150. vm1.backup_path = 'path/in/backup'
  151. vm1.template = 'not-existing-template'
  152. vm1.klass = 'AppVM'
  153. vm1.label = 'red'
  154. mock_restore_info = {
  155. 1: BackupRestore.VMToRestore(vm1),
  156. }
  157. mock_restore_info[1].problems.add(
  158. BackupRestore.VMToRestore.MISSING_TEMPLATE)
  159. with self.subTest('skip_broken'):
  160. mock_args = mock.Mock()
  161. mock_args.skip_broken = True
  162. mock_args.verify_only = False
  163. self.app.log = mock.Mock()
  164. qubesadmin.tools.qvm_backup_restore.handle_broken(
  165. self.app, mock_args, mock_restore_info)
  166. self.assertAppropriateLogging('TemplateVM', 'skip_broken')
  167. with self.subTest('ignore_missing'):
  168. mock_args = mock.Mock()
  169. mock_args.skip_broken = False
  170. mock_args.ignore_missing = True
  171. mock_args.verify_only = False
  172. self.app.log = mock.Mock()
  173. qubesadmin.tools.qvm_backup_restore.handle_broken(
  174. self.app, mock_args, mock_restore_info)
  175. self.assertAppropriateLogging('TemplateVM', 'ignore_missing')
  176. with self.subTest('error'):
  177. mock_args = mock.Mock()
  178. mock_args.skip_broken = False
  179. mock_args.ignore_missing = False
  180. mock_args.verify_only = False
  181. self.app.log = mock.Mock()
  182. with self.assertRaises(qubesadmin.exc.QubesException):
  183. qubesadmin.tools.qvm_backup_restore.handle_broken(
  184. self.app, mock_args, mock_restore_info)
  185. self.assertAppropriateLogging('TemplateVM', 'error')
  186. def test_012_handle_broken_missing_netvm(self):
  187. vm1 = BackupVM()
  188. vm1.name = 'test-vm'
  189. vm1.backup_path = 'path/in/backup'
  190. vm1.netvm = 'not-existing-netvm'
  191. vm1.klass = 'StandaloneVM'
  192. vm1.label = 'red'
  193. mock_restore_info = {
  194. 1: BackupRestore.VMToRestore(vm1),
  195. }
  196. mock_restore_info[1].problems.add(
  197. BackupRestore.VMToRestore.MISSING_NETVM)
  198. with self.subTest('skip_broken'):
  199. mock_args = mock.Mock()
  200. mock_args.skip_broken = True
  201. mock_args.verify_only = False
  202. self.app.log = mock.Mock()
  203. qubesadmin.tools.qvm_backup_restore.handle_broken(
  204. self.app, mock_args, mock_restore_info)
  205. self.assertAppropriateLogging('NetVM', 'skip_broken')
  206. with self.subTest('ignore_missing'):
  207. mock_args = mock.Mock()
  208. mock_args.skip_broken = False
  209. mock_args.ignore_missing = True
  210. mock_args.verify_only = False
  211. self.app.log = mock.Mock()
  212. qubesadmin.tools.qvm_backup_restore.handle_broken(
  213. self.app, mock_args, mock_restore_info)
  214. self.assertAppropriateLogging('NetVM', 'ignore_missing')
  215. with self.subTest('error'):
  216. mock_args = mock.Mock()
  217. mock_args.skip_broken = False
  218. mock_args.ignore_missing = False
  219. mock_args.verify_only = False
  220. self.app.log = mock.Mock()
  221. with self.assertRaises(qubesadmin.exc.QubesException):
  222. qubesadmin.tools.qvm_backup_restore.handle_broken(
  223. self.app, mock_args, mock_restore_info)
  224. self.assertAppropriateLogging('NetVM', 'error')
  225. def test_100_restore_in_dispvm_parser(self):
  226. """Verify if qvm-backup-restore tool options matches un-parser
  227. for paranoid restore mode"""
  228. parser = qubesadmin.tools.qvm_backup_restore.parser
  229. actions = parser._get_optional_actions()
  230. options_tool = set(itertools.chain(*(a.option_strings for a in actions)))
  231. options_parser = set(itertools.chain(
  232. *(o.opts for o in RestoreInDisposableVM.arguments.values())))
  233. self.assertEqual(options_tool, options_parser)