test_zfs.py 17 KB

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