test_zfs.py 20 KB

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