test_zfs.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. import pytest
  2. from flexmock import flexmock
  3. import borgmatic.execute
  4. from borgmatic.hooks.data_source import zfs as module
  5. def test_get_datasets_to_backup_filters_datasets_by_source_directories():
  6. flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return(
  7. 'dataset\t/dataset\t-\nother\t/other\t-',
  8. )
  9. assert module.get_datasets_to_backup(
  10. 'zfs', source_directories=('/foo', '/dataset', '/bar')
  11. ) == (('dataset', '/dataset'),)
  12. def test_get_datasets_to_backup_filters_datasets_by_user_property():
  13. flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return(
  14. 'dataset\t/dataset\tauto\nother\t/other\t-',
  15. )
  16. assert module.get_datasets_to_backup('zfs', source_directories=('/foo', '/bar')) == (
  17. ('dataset', '/dataset'),
  18. )
  19. def test_get_datasets_to_backup_with_invalid_list_output_raises():
  20. flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return(
  21. 'dataset',
  22. )
  23. with pytest.raises(ValueError, match='zfs'):
  24. module.get_datasets_to_backup('zfs', source_directories=('/foo', '/bar'))
  25. def test_get_get_all_datasets_does_not_filter_datasets():
  26. flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return(
  27. 'dataset\t/dataset\nother\t/other',
  28. )
  29. assert module.get_all_datasets('zfs') == (
  30. ('dataset', '/dataset'),
  31. ('other', '/other'),
  32. )
  33. def test_get_all_datasets_with_invalid_list_output_raises():
  34. flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return(
  35. 'dataset',
  36. )
  37. with pytest.raises(ValueError, match='zfs'):
  38. module.get_all_datasets('zfs')
  39. def test_dump_data_sources_snapshots_and_mounts_and_updates_source_directories():
  40. flexmock(module).should_receive('get_datasets_to_backup').and_return(
  41. (('dataset', '/mnt/dataset'),)
  42. )
  43. flexmock(module.os).should_receive('getpid').and_return(1234)
  44. full_snapshot_name = 'dataset@borgmatic-1234'
  45. flexmock(module).should_receive('snapshot_dataset').with_args(
  46. 'zfs',
  47. full_snapshot_name,
  48. ).once()
  49. snapshot_mount_path = '/run/borgmatic/zfs_snapshots/./mnt/dataset'
  50. flexmock(module).should_receive('mount_snapshot').with_args(
  51. 'mount',
  52. full_snapshot_name,
  53. module.os.path.normpath(snapshot_mount_path),
  54. ).once()
  55. source_directories = ['/mnt/dataset']
  56. assert (
  57. module.dump_data_sources(
  58. hook_config={},
  59. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  60. log_prefix='test',
  61. config_paths=('test.yaml',),
  62. borgmatic_runtime_directory='/run/borgmatic',
  63. source_directories=source_directories,
  64. dry_run=False,
  65. )
  66. == []
  67. )
  68. assert source_directories == [snapshot_mount_path]
  69. def test_dump_data_sources_uses_custom_commands():
  70. flexmock(module).should_receive('get_datasets_to_backup').and_return(
  71. (('dataset', '/mnt/dataset'),)
  72. )
  73. flexmock(module.os).should_receive('getpid').and_return(1234)
  74. full_snapshot_name = 'dataset@borgmatic-1234'
  75. flexmock(module).should_receive('snapshot_dataset').with_args(
  76. '/usr/local/bin/zfs',
  77. full_snapshot_name,
  78. ).once()
  79. snapshot_mount_path = '/run/borgmatic/zfs_snapshots/./mnt/dataset'
  80. flexmock(module).should_receive('mount_snapshot').with_args(
  81. '/usr/local/bin/mount',
  82. full_snapshot_name,
  83. module.os.path.normpath(snapshot_mount_path),
  84. ).once()
  85. source_directories = ['/mnt/dataset']
  86. hook_config = {
  87. 'zfs_command': '/usr/local/bin/zfs',
  88. 'mount_command': '/usr/local/bin/mount',
  89. }
  90. assert (
  91. module.dump_data_sources(
  92. hook_config=hook_config,
  93. config={
  94. 'source_directories': source_directories,
  95. 'zfs': hook_config,
  96. },
  97. log_prefix='test',
  98. config_paths=('test.yaml',),
  99. borgmatic_runtime_directory='/run/borgmatic',
  100. source_directories=source_directories,
  101. dry_run=False,
  102. )
  103. == []
  104. )
  105. assert source_directories == [snapshot_mount_path]
  106. def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_source_directories():
  107. flexmock(module).should_receive('get_datasets_to_backup').and_return(
  108. (('dataset', '/mnt/dataset'),)
  109. )
  110. flexmock(module.os).should_receive('getpid').and_return(1234)
  111. flexmock(module).should_receive('snapshot_dataset').never()
  112. flexmock(module).should_receive('mount_snapshot').never()
  113. source_directories = ['/mnt/dataset']
  114. assert (
  115. module.dump_data_sources(
  116. hook_config={},
  117. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  118. log_prefix='test',
  119. config_paths=('test.yaml',),
  120. borgmatic_runtime_directory='/run/borgmatic',
  121. source_directories=source_directories,
  122. dry_run=True,
  123. )
  124. == []
  125. )
  126. assert source_directories == ['/mnt/dataset']
  127. def test_get_all_snapshots_parses_list_output():
  128. flexmock(borgmatic.execute).should_receive('execute_command_and_capture_output').and_return(
  129. 'dataset1@borgmatic-1234\ndataset2@borgmatic-4567',
  130. )
  131. assert module.get_all_snapshots('zfs') == ('dataset1@borgmatic-1234', 'dataset2@borgmatic-4567')
  132. def test_remove_data_source_dumps_unmounts_and_destroys_snapshots():
  133. flexmock(module).should_receive('get_all_datasets').and_return((('dataset', '/mnt/dataset'),))
  134. flexmock(module.borgmatic.config.paths).should_receive(
  135. 'replace_temporary_subdirectory_with_glob'
  136. ).and_return('/run/borgmatic')
  137. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  138. flexmock(module.os.path).should_receive('isdir').and_return(True)
  139. flexmock(module.shutil).should_receive('rmtree')
  140. flexmock(module).should_receive('unmount_snapshot').with_args(
  141. 'umount', '/run/borgmatic/zfs_snapshots/mnt/dataset'
  142. ).once()
  143. flexmock(module).should_receive('get_all_snapshots').and_return(
  144. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
  145. )
  146. flexmock(module).should_receive('destroy_snapshot').with_args(
  147. 'zfs', 'dataset@borgmatic-1234'
  148. ).once()
  149. module.remove_data_source_dumps(
  150. hook_config={},
  151. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  152. log_prefix='test',
  153. borgmatic_runtime_directory='/run/borgmatic',
  154. dry_run=False,
  155. )
  156. def test_remove_data_source_dumps_use_custom_commands():
  157. flexmock(module).should_receive('get_all_datasets').and_return((('dataset', '/mnt/dataset'),))
  158. flexmock(module.borgmatic.config.paths).should_receive(
  159. 'replace_temporary_subdirectory_with_glob'
  160. ).and_return('/run/borgmatic')
  161. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  162. flexmock(module.os.path).should_receive('isdir').and_return(True)
  163. flexmock(module.shutil).should_receive('rmtree')
  164. flexmock(module).should_receive('unmount_snapshot').with_args(
  165. '/usr/local/bin/umount', '/run/borgmatic/zfs_snapshots/mnt/dataset'
  166. ).once()
  167. flexmock(module).should_receive('get_all_snapshots').and_return(
  168. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
  169. )
  170. flexmock(module).should_receive('destroy_snapshot').with_args(
  171. '/usr/local/bin/zfs', 'dataset@borgmatic-1234'
  172. ).once()
  173. hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
  174. module.remove_data_source_dumps(
  175. hook_config=hook_config,
  176. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  177. log_prefix='test',
  178. borgmatic_runtime_directory='/run/borgmatic',
  179. dry_run=False,
  180. )
  181. def test_remove_data_source_dumps_bails_for_missing_zfs_command():
  182. flexmock(module).should_receive('get_all_datasets').and_raise(FileNotFoundError)
  183. flexmock(module.borgmatic.config.paths).should_receive(
  184. 'replace_temporary_subdirectory_with_glob'
  185. ).never()
  186. hook_config = {'zfs_command': 'wtf'}
  187. module.remove_data_source_dumps(
  188. hook_config=hook_config,
  189. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  190. log_prefix='test',
  191. borgmatic_runtime_directory='/run/borgmatic',
  192. dry_run=False,
  193. )
  194. def test_remove_data_source_dumps_bails_for_zfs_command_error():
  195. flexmock(module).should_receive('get_all_datasets').and_raise(
  196. module.subprocess.CalledProcessError(1, 'wtf')
  197. )
  198. flexmock(module.borgmatic.config.paths).should_receive(
  199. 'replace_temporary_subdirectory_with_glob'
  200. ).never()
  201. hook_config = {'zfs_command': 'wtf'}
  202. module.remove_data_source_dumps(
  203. hook_config=hook_config,
  204. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  205. log_prefix='test',
  206. borgmatic_runtime_directory='/run/borgmatic',
  207. dry_run=False,
  208. )
  209. def test_remove_data_source_dumps_bails_for_missing_umount_command():
  210. flexmock(module).should_receive('get_all_datasets').and_return((('dataset', '/mnt/dataset'),))
  211. flexmock(module.borgmatic.config.paths).should_receive(
  212. 'replace_temporary_subdirectory_with_glob'
  213. ).and_return('/run/borgmatic')
  214. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  215. flexmock(module.os.path).should_receive('isdir').and_return(True)
  216. flexmock(module.shutil).should_receive('rmtree')
  217. flexmock(module).should_receive('unmount_snapshot').with_args(
  218. '/usr/local/bin/umount', '/run/borgmatic/zfs_snapshots/mnt/dataset'
  219. ).and_raise(FileNotFoundError)
  220. flexmock(module).should_receive('get_all_snapshots').never()
  221. flexmock(module).should_receive('destroy_snapshot').never()
  222. hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
  223. module.remove_data_source_dumps(
  224. hook_config=hook_config,
  225. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  226. log_prefix='test',
  227. borgmatic_runtime_directory='/run/borgmatic',
  228. dry_run=False,
  229. )
  230. def test_remove_data_source_dumps_bails_for_umount_command_error():
  231. flexmock(module).should_receive('get_all_datasets').and_return((('dataset', '/mnt/dataset'),))
  232. flexmock(module.borgmatic.config.paths).should_receive(
  233. 'replace_temporary_subdirectory_with_glob'
  234. ).and_return('/run/borgmatic')
  235. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  236. flexmock(module.os.path).should_receive('isdir').and_return(True)
  237. flexmock(module.shutil).should_receive('rmtree')
  238. flexmock(module).should_receive('unmount_snapshot').with_args(
  239. '/usr/local/bin/umount', '/run/borgmatic/zfs_snapshots/mnt/dataset'
  240. ).and_raise(module.subprocess.CalledProcessError(1, 'wtf'))
  241. flexmock(module).should_receive('get_all_snapshots').never()
  242. flexmock(module).should_receive('destroy_snapshot').never()
  243. hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
  244. module.remove_data_source_dumps(
  245. hook_config=hook_config,
  246. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  247. log_prefix='test',
  248. borgmatic_runtime_directory='/run/borgmatic',
  249. dry_run=False,
  250. )
  251. def test_remove_data_source_dumps_skips_unmount_snapshot_directories_that_are_not_actually_directories():
  252. flexmock(module).should_receive('get_all_datasets').and_return((('dataset', '/mnt/dataset'),))
  253. flexmock(module.borgmatic.config.paths).should_receive(
  254. 'replace_temporary_subdirectory_with_glob'
  255. ).and_return('/run/borgmatic')
  256. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  257. flexmock(module.os.path).should_receive('isdir').and_return(False)
  258. flexmock(module.shutil).should_receive('rmtree').never()
  259. flexmock(module).should_receive('unmount_snapshot').never()
  260. flexmock(module).should_receive('get_all_snapshots').and_return(
  261. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
  262. )
  263. flexmock(module).should_receive('destroy_snapshot').with_args(
  264. 'zfs', 'dataset@borgmatic-1234'
  265. ).once()
  266. module.remove_data_source_dumps(
  267. hook_config={},
  268. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  269. log_prefix='test',
  270. borgmatic_runtime_directory='/run/borgmatic',
  271. dry_run=False,
  272. )
  273. def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_that_are_not_actually_directories():
  274. flexmock(module).should_receive('get_all_datasets').and_return((('dataset', '/mnt/dataset'),))
  275. flexmock(module.borgmatic.config.paths).should_receive(
  276. 'replace_temporary_subdirectory_with_glob'
  277. ).and_return('/run/borgmatic')
  278. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  279. flexmock(module.os.path).should_receive('isdir').with_args(
  280. '/run/borgmatic/zfs_snapshots'
  281. ).and_return(True)
  282. flexmock(module.os.path).should_receive('isdir').with_args(
  283. '/run/borgmatic/zfs_snapshots/mnt/dataset'
  284. ).and_return(False)
  285. flexmock(module.shutil).should_receive('rmtree')
  286. flexmock(module).should_receive('unmount_snapshot').never()
  287. flexmock(module).should_receive('get_all_snapshots').and_return(
  288. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
  289. )
  290. flexmock(module).should_receive('destroy_snapshot').with_args(
  291. 'zfs', 'dataset@borgmatic-1234'
  292. ).once()
  293. module.remove_data_source_dumps(
  294. hook_config={},
  295. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  296. log_prefix='test',
  297. borgmatic_runtime_directory='/run/borgmatic',
  298. dry_run=False,
  299. )
  300. def test_remove_data_source_dumps_with_dry_run_skips_unmount_and_destroy():
  301. flexmock(module).should_receive('get_all_datasets').and_return((('dataset', '/mnt/dataset'),))
  302. flexmock(module.borgmatic.config.paths).should_receive(
  303. 'replace_temporary_subdirectory_with_glob'
  304. ).and_return('/run/borgmatic')
  305. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  306. flexmock(module.os.path).should_receive('isdir').and_return(True)
  307. flexmock(module.shutil).should_receive('rmtree').never()
  308. flexmock(module).should_receive('unmount_snapshot').never()
  309. flexmock(module).should_receive('get_all_snapshots').and_return(
  310. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
  311. )
  312. flexmock(module).should_receive('destroy_snapshot').never()
  313. module.remove_data_source_dumps(
  314. hook_config={},
  315. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  316. log_prefix='test',
  317. borgmatic_runtime_directory='/run/borgmatic',
  318. dry_run=True,
  319. )