test_zfs.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846
  1. import pytest
  2. from flexmock import flexmock
  3. from borgmatic.borg.pattern import Pattern, Pattern_source, Pattern_style, Pattern_type
  4. from borgmatic.hooks.data_source import zfs as module
  5. def test_get_datasets_to_backup_filters_datasets_by_patterns():
  6. flexmock(module.borgmatic.execute).should_receive(
  7. 'execute_command_and_capture_output',
  8. ).and_return(
  9. 'dataset\t/dataset\ton\t-\nother\t/other\ton\t-',
  10. )
  11. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  12. 'get_contained_patterns',
  13. ).with_args('/dataset', object).and_return(
  14. (
  15. Pattern(
  16. '/dataset',
  17. Pattern_type.ROOT,
  18. source=Pattern_source.CONFIG,
  19. ),
  20. ),
  21. )
  22. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  23. 'get_contained_patterns',
  24. ).with_args('/other', object).and_return(())
  25. assert module.get_datasets_to_backup(
  26. 'zfs',
  27. patterns=(
  28. Pattern(
  29. '/foo',
  30. Pattern_type.ROOT,
  31. source=Pattern_source.CONFIG,
  32. ),
  33. Pattern(
  34. '/dataset',
  35. Pattern_type.ROOT,
  36. source=Pattern_source.CONFIG,
  37. ),
  38. Pattern(
  39. '/bar',
  40. Pattern_type.ROOT,
  41. source=Pattern_source.CONFIG,
  42. ),
  43. ),
  44. ) == (
  45. module.Dataset(
  46. name='dataset',
  47. mount_point='/dataset',
  48. contained_patterns=(
  49. Pattern(
  50. '/dataset',
  51. Pattern_type.ROOT,
  52. source=Pattern_source.CONFIG,
  53. ),
  54. ),
  55. ),
  56. )
  57. def test_get_datasets_to_backup_skips_non_root_patterns():
  58. flexmock(module.borgmatic.execute).should_receive(
  59. 'execute_command_and_capture_output',
  60. ).and_return(
  61. 'dataset\t/dataset\ton\t-\nother\t/other\ton\t-',
  62. )
  63. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  64. 'get_contained_patterns',
  65. ).with_args('/dataset', object).and_return(
  66. (
  67. Pattern(
  68. '/dataset',
  69. Pattern_type.EXCLUDE,
  70. source=Pattern_source.CONFIG,
  71. ),
  72. ),
  73. )
  74. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  75. 'get_contained_patterns',
  76. ).with_args('/other', object).and_return(())
  77. assert (
  78. module.get_datasets_to_backup(
  79. 'zfs',
  80. patterns=(
  81. Pattern(
  82. '/foo',
  83. Pattern_type.ROOT,
  84. source=Pattern_source.CONFIG,
  85. ),
  86. Pattern(
  87. '/dataset',
  88. Pattern_type.EXCLUDE,
  89. source=Pattern_source.CONFIG,
  90. ),
  91. Pattern(
  92. '/bar',
  93. Pattern_type.ROOT,
  94. source=Pattern_source.CONFIG,
  95. ),
  96. ),
  97. )
  98. == ()
  99. )
  100. def test_get_datasets_to_backup_skips_non_config_patterns():
  101. flexmock(module.borgmatic.execute).should_receive(
  102. 'execute_command_and_capture_output',
  103. ).and_return(
  104. 'dataset\t/dataset\ton\t-\nother\t/other\ton\t-',
  105. )
  106. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  107. 'get_contained_patterns',
  108. ).with_args('/dataset', object).and_return(
  109. (
  110. Pattern(
  111. '/dataset',
  112. Pattern_type.ROOT,
  113. source=Pattern_source.HOOK,
  114. ),
  115. ),
  116. )
  117. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  118. 'get_contained_patterns',
  119. ).with_args('/other', object).and_return(())
  120. assert (
  121. module.get_datasets_to_backup(
  122. 'zfs',
  123. patterns=(
  124. Pattern(
  125. '/foo',
  126. Pattern_type.ROOT,
  127. source=Pattern_source.CONFIG,
  128. ),
  129. Pattern(
  130. '/dataset',
  131. Pattern_type.ROOT,
  132. source=Pattern_source.HOOK,
  133. ),
  134. Pattern(
  135. '/bar',
  136. Pattern_type.ROOT,
  137. source=Pattern_source.CONFIG,
  138. ),
  139. ),
  140. )
  141. == ()
  142. )
  143. def test_get_datasets_to_backup_filters_datasets_by_user_property():
  144. flexmock(module.borgmatic.execute).should_receive(
  145. 'execute_command_and_capture_output',
  146. ).and_return(
  147. 'dataset\t/dataset\ton\tauto\nother\t/other\ton\t-',
  148. )
  149. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  150. 'get_contained_patterns',
  151. ).with_args('/dataset', object).and_return(())
  152. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  153. 'get_contained_patterns',
  154. ).with_args('/other', object).and_return(())
  155. assert module.get_datasets_to_backup(
  156. 'zfs',
  157. patterns=(Pattern('/foo'), Pattern('/bar')),
  158. ) == (
  159. module.Dataset(
  160. name='dataset',
  161. mount_point='/dataset',
  162. auto_backup=True,
  163. contained_patterns=(Pattern('/dataset', source=Pattern_source.HOOK),),
  164. ),
  165. )
  166. def test_get_datasets_to_backup_filters_datasets_by_canmount_property():
  167. flexmock(module.borgmatic.execute).should_receive(
  168. 'execute_command_and_capture_output',
  169. ).and_return(
  170. 'dataset\t/dataset\toff\t-\nother\t/other\ton\t-',
  171. )
  172. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  173. 'get_contained_patterns',
  174. ).with_args('/dataset', object).and_return((Pattern('/dataset'),))
  175. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  176. 'get_contained_patterns',
  177. ).with_args('/other', object).and_return(())
  178. assert (
  179. module.get_datasets_to_backup(
  180. 'zfs',
  181. patterns=(
  182. Pattern('/foo'),
  183. Pattern('/dataset'),
  184. Pattern('/bar'),
  185. ),
  186. )
  187. == ()
  188. )
  189. def test_get_datasets_to_backup_with_invalid_list_output_raises():
  190. flexmock(module.borgmatic.execute).should_receive(
  191. 'execute_command_and_capture_output',
  192. ).and_return(
  193. 'dataset',
  194. )
  195. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  196. 'get_contained_patterns',
  197. ).never()
  198. with pytest.raises(ValueError, match='zfs'):
  199. module.get_datasets_to_backup('zfs', patterns=(Pattern('/foo'), Pattern('/bar')))
  200. def test_get_all_dataset_mount_points_omits_none():
  201. flexmock(module.borgmatic.execute).should_receive(
  202. 'execute_command_and_capture_output',
  203. ).and_return(
  204. '/dataset\nnone\n/other',
  205. )
  206. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  207. 'get_contained_patterns',
  208. ).and_return((Pattern('/dataset'),))
  209. assert module.get_all_dataset_mount_points('zfs') == (
  210. ('/dataset'),
  211. ('/other'),
  212. )
  213. def test_get_all_dataset_mount_points_omits_duplicates():
  214. flexmock(module.borgmatic.execute).should_receive(
  215. 'execute_command_and_capture_output',
  216. ).and_return(
  217. '/dataset\n/other\n/dataset\n/other',
  218. )
  219. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  220. 'get_contained_patterns',
  221. ).and_return((Pattern('/dataset'),))
  222. assert module.get_all_dataset_mount_points('zfs') == (
  223. ('/dataset'),
  224. ('/other'),
  225. )
  226. @pytest.mark.parametrize(
  227. 'pattern,expected_pattern',
  228. (
  229. (
  230. Pattern('/foo/bar/baz'),
  231. Pattern('/run/borgmatic/zfs_snapshots/b33f/./foo/bar/baz'),
  232. ),
  233. (Pattern('/foo/bar'), Pattern('/run/borgmatic/zfs_snapshots/b33f/./foo/bar')),
  234. (
  235. Pattern('^/foo/bar', Pattern_type.INCLUDE, Pattern_style.REGULAR_EXPRESSION),
  236. Pattern(
  237. '^/run/borgmatic/zfs_snapshots/b33f/./foo/bar',
  238. Pattern_type.INCLUDE,
  239. Pattern_style.REGULAR_EXPRESSION,
  240. ),
  241. ),
  242. (
  243. Pattern('/foo/bar', Pattern_type.INCLUDE, Pattern_style.REGULAR_EXPRESSION),
  244. Pattern(
  245. '/run/borgmatic/zfs_snapshots/b33f/./foo/bar',
  246. Pattern_type.INCLUDE,
  247. Pattern_style.REGULAR_EXPRESSION,
  248. ),
  249. ),
  250. (Pattern('/foo'), Pattern('/run/borgmatic/zfs_snapshots/b33f/./foo')),
  251. (Pattern('/'), Pattern('/run/borgmatic/zfs_snapshots/b33f/./')),
  252. (
  253. Pattern('/foo/./bar/baz'),
  254. Pattern('/run/borgmatic/zfs_snapshots/b33f/foo/./bar/baz'),
  255. ),
  256. ),
  257. )
  258. def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_path(
  259. pattern,
  260. expected_pattern,
  261. ):
  262. flexmock(module.hashlib).should_receive('shake_256').and_return(
  263. flexmock(hexdigest=lambda length: 'b33f'),
  264. )
  265. assert (
  266. module.make_borg_snapshot_pattern(
  267. pattern,
  268. flexmock(mount_point='/something'),
  269. '/run/borgmatic',
  270. )
  271. == expected_pattern
  272. )
  273. def test_dump_data_sources_snapshots_and_mounts_and_replaces_patterns():
  274. dataset = flexmock(
  275. name='dataset',
  276. mount_point='/mnt/dataset',
  277. contained_patterns=(Pattern('/mnt/dataset/subdir'),),
  278. )
  279. flexmock(module).should_receive('get_datasets_to_backup').and_return((dataset,))
  280. flexmock(module.os).should_receive('getpid').and_return(1234)
  281. full_snapshot_name = 'dataset@borgmatic-1234'
  282. flexmock(module).should_receive('snapshot_dataset').with_args(
  283. 'zfs',
  284. full_snapshot_name,
  285. ).once()
  286. flexmock(module.hashlib).should_receive('shake_256').and_return(
  287. flexmock(hexdigest=lambda length: 'b33f'),
  288. )
  289. snapshot_mount_path = '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset'
  290. flexmock(module).should_receive('mount_snapshot').with_args(
  291. 'mount',
  292. full_snapshot_name,
  293. module.os.path.normpath(snapshot_mount_path),
  294. ).once()
  295. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  296. Pattern('/mnt/dataset/subdir'),
  297. dataset,
  298. '/run/borgmatic',
  299. ).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir'))
  300. patterns = [Pattern('/mnt/dataset/subdir')]
  301. flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
  302. object,
  303. Pattern('/mnt/dataset/subdir'),
  304. module.borgmatic.borg.pattern.Pattern(
  305. '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir',
  306. source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
  307. ),
  308. ).once()
  309. assert (
  310. module.dump_data_sources(
  311. hook_config={},
  312. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  313. config_paths=('test.yaml',),
  314. borgmatic_runtime_directory='/run/borgmatic',
  315. patterns=patterns,
  316. dry_run=False,
  317. )
  318. == []
  319. )
  320. def test_dump_data_sources_with_no_datasets_skips_snapshots():
  321. flexmock(module).should_receive('get_datasets_to_backup').and_return(())
  322. flexmock(module.os).should_receive('getpid').and_return(1234)
  323. flexmock(module).should_receive('snapshot_dataset').never()
  324. flexmock(module).should_receive('mount_snapshot').never()
  325. flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
  326. patterns = [Pattern('/mnt/dataset')]
  327. assert (
  328. module.dump_data_sources(
  329. hook_config={},
  330. config={'patterns': flexmock(), 'zfs': {}},
  331. config_paths=('test.yaml',),
  332. borgmatic_runtime_directory='/run/borgmatic',
  333. patterns=patterns,
  334. dry_run=False,
  335. )
  336. == []
  337. )
  338. assert patterns == [Pattern('/mnt/dataset')]
  339. def test_dump_data_sources_uses_custom_commands():
  340. dataset = flexmock(
  341. name='dataset',
  342. mount_point='/mnt/dataset',
  343. contained_patterns=(Pattern('/mnt/dataset/subdir'),),
  344. )
  345. flexmock(module).should_receive('get_datasets_to_backup').and_return((dataset,))
  346. flexmock(module.os).should_receive('getpid').and_return(1234)
  347. full_snapshot_name = 'dataset@borgmatic-1234'
  348. flexmock(module).should_receive('snapshot_dataset').with_args(
  349. '/usr/local/bin/zfs',
  350. full_snapshot_name,
  351. ).once()
  352. flexmock(module.hashlib).should_receive('shake_256').and_return(
  353. flexmock(hexdigest=lambda length: 'b33f'),
  354. )
  355. snapshot_mount_path = '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset'
  356. flexmock(module).should_receive('mount_snapshot').with_args(
  357. '/usr/local/bin/mount',
  358. full_snapshot_name,
  359. module.os.path.normpath(snapshot_mount_path),
  360. ).once()
  361. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  362. Pattern('/mnt/dataset/subdir'),
  363. dataset,
  364. '/run/borgmatic',
  365. ).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir'))
  366. flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
  367. object,
  368. Pattern('/mnt/dataset/subdir'),
  369. module.borgmatic.borg.pattern.Pattern(
  370. '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir',
  371. source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
  372. ),
  373. ).once()
  374. patterns = [Pattern('/mnt/dataset/subdir')]
  375. hook_config = {
  376. 'zfs_command': '/usr/local/bin/zfs',
  377. 'mount_command': '/usr/local/bin/mount',
  378. }
  379. assert (
  380. module.dump_data_sources(
  381. hook_config=hook_config,
  382. config={
  383. 'patterns': flexmock(),
  384. 'zfs': hook_config,
  385. },
  386. config_paths=('test.yaml',),
  387. borgmatic_runtime_directory='/run/borgmatic',
  388. patterns=patterns,
  389. dry_run=False,
  390. )
  391. == []
  392. )
  393. def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_patterns():
  394. flexmock(module).should_receive('get_datasets_to_backup').and_return(
  395. (flexmock(name='dataset', mount_point='/mnt/dataset'),),
  396. )
  397. flexmock(module.os).should_receive('getpid').and_return(1234)
  398. flexmock(module).should_receive('snapshot_dataset').never()
  399. flexmock(module).should_receive('mount_snapshot').never()
  400. flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
  401. patterns = [Pattern('/mnt/dataset')]
  402. assert (
  403. module.dump_data_sources(
  404. hook_config={},
  405. config={'patterns': ('R /mnt/dataset',), 'zfs': {}},
  406. config_paths=('test.yaml',),
  407. borgmatic_runtime_directory='/run/borgmatic',
  408. patterns=patterns,
  409. dry_run=True,
  410. )
  411. == []
  412. )
  413. def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained_patterns():
  414. dataset = flexmock(
  415. name='dataset',
  416. mount_point='/mnt/dataset',
  417. contained_patterns=(Pattern('/mnt/dataset/subdir'),),
  418. )
  419. flexmock(module).should_receive('get_datasets_to_backup').and_return((dataset,))
  420. flexmock(module.os).should_receive('getpid').and_return(1234)
  421. full_snapshot_name = 'dataset@borgmatic-1234'
  422. flexmock(module).should_receive('snapshot_dataset').with_args(
  423. 'zfs',
  424. full_snapshot_name,
  425. ).once()
  426. flexmock(module.hashlib).should_receive('shake_256').and_return(
  427. flexmock(hexdigest=lambda length: 'b33f'),
  428. )
  429. snapshot_mount_path = '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset'
  430. flexmock(module).should_receive('mount_snapshot').with_args(
  431. 'mount',
  432. full_snapshot_name,
  433. module.os.path.normpath(snapshot_mount_path),
  434. ).once()
  435. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  436. Pattern('/mnt/dataset/subdir'),
  437. dataset,
  438. '/run/borgmatic',
  439. ).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir'))
  440. flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
  441. object,
  442. Pattern('/mnt/dataset/subdir'),
  443. module.borgmatic.borg.pattern.Pattern(
  444. '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir',
  445. source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
  446. ),
  447. ).once()
  448. patterns = [Pattern('/hmm')]
  449. assert (
  450. module.dump_data_sources(
  451. hook_config={},
  452. config={'patterns': ('R /mnt/dataset',), 'zfs': {}},
  453. config_paths=('test.yaml',),
  454. borgmatic_runtime_directory='/run/borgmatic',
  455. patterns=patterns,
  456. dry_run=False,
  457. )
  458. == []
  459. )
  460. def test_get_all_snapshots_parses_list_output():
  461. flexmock(module.borgmatic.execute).should_receive(
  462. 'execute_command_and_capture_output',
  463. ).and_return(
  464. 'dataset1@borgmatic-1234\ndataset2@borgmatic-4567',
  465. )
  466. assert module.get_all_snapshots('zfs') == ('dataset1@borgmatic-1234', 'dataset2@borgmatic-4567')
  467. def test_remove_data_source_dumps_unmounts_and_destroys_snapshots():
  468. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  469. flexmock(module.borgmatic.config.paths).should_receive(
  470. 'replace_temporary_subdirectory_with_glob',
  471. ).and_return('/run/borgmatic')
  472. flexmock(module.glob).should_receive('glob').replace_with(
  473. lambda path: [path.replace('*', 'b33f')],
  474. )
  475. flexmock(module.os.path).should_receive('isdir').and_return(True)
  476. flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
  477. flexmock(module.shutil).should_receive('rmtree')
  478. flexmock(module).should_receive('unmount_snapshot').with_args(
  479. 'umount',
  480. '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
  481. ).once()
  482. flexmock(module).should_receive('get_all_snapshots').and_return(
  483. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
  484. )
  485. flexmock(module).should_receive('destroy_snapshot').with_args(
  486. 'zfs',
  487. 'dataset@borgmatic-1234',
  488. ).once()
  489. module.remove_data_source_dumps(
  490. hook_config={},
  491. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  492. borgmatic_runtime_directory='/run/borgmatic',
  493. patterns=flexmock(),
  494. dry_run=False,
  495. )
  496. def test_remove_data_source_dumps_use_custom_commands():
  497. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  498. flexmock(module.borgmatic.config.paths).should_receive(
  499. 'replace_temporary_subdirectory_with_glob',
  500. ).and_return('/run/borgmatic')
  501. flexmock(module.glob).should_receive('glob').replace_with(
  502. lambda path: [path.replace('*', 'b33f')],
  503. )
  504. flexmock(module.os.path).should_receive('isdir').and_return(True)
  505. flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
  506. flexmock(module.shutil).should_receive('rmtree')
  507. flexmock(module).should_receive('unmount_snapshot').with_args(
  508. '/usr/local/bin/umount',
  509. '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
  510. ).once()
  511. flexmock(module).should_receive('get_all_snapshots').and_return(
  512. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
  513. )
  514. flexmock(module).should_receive('destroy_snapshot').with_args(
  515. '/usr/local/bin/zfs',
  516. 'dataset@borgmatic-1234',
  517. ).once()
  518. hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
  519. module.remove_data_source_dumps(
  520. hook_config=hook_config,
  521. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  522. borgmatic_runtime_directory='/run/borgmatic',
  523. patterns=flexmock(),
  524. dry_run=False,
  525. )
  526. def test_remove_data_source_dumps_bails_for_missing_hook_configuration():
  527. flexmock(module).should_receive('get_all_dataset_mount_points').never()
  528. flexmock(module.borgmatic.config.paths).should_receive(
  529. 'replace_temporary_subdirectory_with_glob',
  530. ).never()
  531. module.remove_data_source_dumps(
  532. hook_config=None,
  533. config={'source_directories': '/mnt/dataset'},
  534. borgmatic_runtime_directory='/run/borgmatic',
  535. patterns=flexmock(),
  536. dry_run=False,
  537. )
  538. def test_remove_data_source_dumps_bails_for_missing_zfs_command():
  539. flexmock(module).should_receive('get_all_dataset_mount_points').and_raise(FileNotFoundError)
  540. flexmock(module.borgmatic.config.paths).should_receive(
  541. 'replace_temporary_subdirectory_with_glob',
  542. ).never()
  543. hook_config = {'zfs_command': 'wtf'}
  544. module.remove_data_source_dumps(
  545. hook_config=hook_config,
  546. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  547. borgmatic_runtime_directory='/run/borgmatic',
  548. patterns=flexmock(),
  549. dry_run=False,
  550. )
  551. def test_remove_data_source_dumps_bails_for_zfs_command_error():
  552. flexmock(module).should_receive('get_all_dataset_mount_points').and_raise(
  553. module.subprocess.CalledProcessError(1, 'wtf'),
  554. )
  555. flexmock(module.borgmatic.config.paths).should_receive(
  556. 'replace_temporary_subdirectory_with_glob',
  557. ).never()
  558. hook_config = {'zfs_command': 'wtf'}
  559. module.remove_data_source_dumps(
  560. hook_config=hook_config,
  561. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  562. borgmatic_runtime_directory='/run/borgmatic',
  563. patterns=flexmock(),
  564. dry_run=False,
  565. )
  566. def test_remove_data_source_dumps_bails_for_missing_umount_command():
  567. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  568. flexmock(module.borgmatic.config.paths).should_receive(
  569. 'replace_temporary_subdirectory_with_glob',
  570. ).and_return('/run/borgmatic')
  571. flexmock(module.glob).should_receive('glob').replace_with(
  572. lambda path: [path.replace('*', 'b33f')],
  573. )
  574. flexmock(module.os.path).should_receive('isdir').and_return(True)
  575. flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
  576. flexmock(module.shutil).should_receive('rmtree')
  577. flexmock(module).should_receive('unmount_snapshot').with_args(
  578. '/usr/local/bin/umount',
  579. '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
  580. ).and_raise(FileNotFoundError)
  581. flexmock(module).should_receive('get_all_snapshots').never()
  582. flexmock(module).should_receive('destroy_snapshot').never()
  583. hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
  584. module.remove_data_source_dumps(
  585. hook_config=hook_config,
  586. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  587. borgmatic_runtime_directory='/run/borgmatic',
  588. patterns=flexmock(),
  589. dry_run=False,
  590. )
  591. def test_remove_data_source_dumps_swallows_umount_command_error():
  592. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  593. flexmock(module.borgmatic.config.paths).should_receive(
  594. 'replace_temporary_subdirectory_with_glob',
  595. ).and_return('/run/borgmatic')
  596. flexmock(module.glob).should_receive('glob').replace_with(
  597. lambda path: [path.replace('*', 'b33f')],
  598. )
  599. flexmock(module.os.path).should_receive('isdir').and_return(True)
  600. flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
  601. flexmock(module.shutil).should_receive('rmtree')
  602. flexmock(module).should_receive('unmount_snapshot').with_args(
  603. '/usr/local/bin/umount',
  604. '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
  605. ).and_raise(module.subprocess.CalledProcessError(1, 'wtf'))
  606. flexmock(module).should_receive('get_all_snapshots').and_return(
  607. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
  608. )
  609. flexmock(module).should_receive('destroy_snapshot').with_args(
  610. '/usr/local/bin/zfs',
  611. 'dataset@borgmatic-1234',
  612. ).once()
  613. hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
  614. module.remove_data_source_dumps(
  615. hook_config=hook_config,
  616. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  617. borgmatic_runtime_directory='/run/borgmatic',
  618. patterns=flexmock(),
  619. dry_run=False,
  620. )
  621. def test_remove_data_source_dumps_skips_unmount_snapshot_directories_that_are_not_actually_directories():
  622. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  623. flexmock(module.borgmatic.config.paths).should_receive(
  624. 'replace_temporary_subdirectory_with_glob',
  625. ).and_return('/run/borgmatic')
  626. flexmock(module.glob).should_receive('glob').replace_with(
  627. lambda path: [path.replace('*', 'b33f')],
  628. )
  629. flexmock(module.os.path).should_receive('isdir').and_return(False)
  630. flexmock(module.shutil).should_receive('rmtree').never()
  631. flexmock(module).should_receive('unmount_snapshot').never()
  632. flexmock(module).should_receive('get_all_snapshots').and_return(
  633. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
  634. )
  635. flexmock(module).should_receive('destroy_snapshot').with_args(
  636. 'zfs',
  637. 'dataset@borgmatic-1234',
  638. ).once()
  639. module.remove_data_source_dumps(
  640. hook_config={},
  641. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  642. borgmatic_runtime_directory='/run/borgmatic',
  643. patterns=flexmock(),
  644. dry_run=False,
  645. )
  646. def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_that_are_not_actually_directories():
  647. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  648. flexmock(module.borgmatic.config.paths).should_receive(
  649. 'replace_temporary_subdirectory_with_glob',
  650. ).and_return('/run/borgmatic')
  651. flexmock(module.glob).should_receive('glob').replace_with(
  652. lambda path: [path.replace('*', 'b33f')],
  653. )
  654. flexmock(module.os.path).should_receive('isdir').with_args(
  655. '/run/borgmatic/zfs_snapshots/b33f',
  656. ).and_return(True)
  657. flexmock(module.os.path).should_receive('isdir').with_args(
  658. '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
  659. ).and_return(False)
  660. flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
  661. flexmock(module.shutil).should_receive('rmtree')
  662. flexmock(module).should_receive('unmount_snapshot').never()
  663. flexmock(module).should_receive('get_all_snapshots').and_return(
  664. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
  665. )
  666. flexmock(module).should_receive('destroy_snapshot').with_args(
  667. 'zfs',
  668. 'dataset@borgmatic-1234',
  669. ).once()
  670. module.remove_data_source_dumps(
  671. hook_config={},
  672. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  673. borgmatic_runtime_directory='/run/borgmatic',
  674. patterns=flexmock(),
  675. dry_run=False,
  676. )
  677. def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_that_are_empty():
  678. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  679. flexmock(module.borgmatic.config.paths).should_receive(
  680. 'replace_temporary_subdirectory_with_glob',
  681. ).and_return('/run/borgmatic')
  682. flexmock(module.glob).should_receive('glob').replace_with(
  683. lambda path: [path.replace('*', 'b33f')],
  684. )
  685. flexmock(module.os.path).should_receive('isdir').with_args(
  686. '/run/borgmatic/zfs_snapshots/b33f',
  687. ).and_return(True)
  688. flexmock(module.os.path).should_receive('isdir').with_args(
  689. '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
  690. ).and_return(True)
  691. flexmock(module.os).should_receive('listdir').with_args(
  692. '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
  693. ).and_return([])
  694. flexmock(module.shutil).should_receive('rmtree')
  695. flexmock(module).should_receive('unmount_snapshot').never()
  696. flexmock(module).should_receive('get_all_snapshots').and_return(
  697. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
  698. )
  699. flexmock(module).should_receive('destroy_snapshot').with_args(
  700. 'zfs',
  701. 'dataset@borgmatic-1234',
  702. ).once()
  703. module.remove_data_source_dumps(
  704. hook_config={},
  705. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  706. borgmatic_runtime_directory='/run/borgmatic',
  707. patterns=flexmock(),
  708. dry_run=False,
  709. )
  710. def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_after_rmtree_succeeds():
  711. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  712. flexmock(module.borgmatic.config.paths).should_receive(
  713. 'replace_temporary_subdirectory_with_glob',
  714. ).and_return('/run/borgmatic')
  715. flexmock(module.glob).should_receive('glob').replace_with(
  716. lambda path: [path.replace('*', 'b33f')],
  717. )
  718. flexmock(module.os.path).should_receive('isdir').with_args(
  719. '/run/borgmatic/zfs_snapshots/b33f',
  720. ).and_return(True)
  721. flexmock(module.os.path).should_receive('isdir').with_args(
  722. '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
  723. ).and_return(True).and_return(False)
  724. flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
  725. flexmock(module.shutil).should_receive('rmtree')
  726. flexmock(module).should_receive('unmount_snapshot').never()
  727. flexmock(module).should_receive('get_all_snapshots').and_return(
  728. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
  729. )
  730. flexmock(module).should_receive('destroy_snapshot').with_args(
  731. 'zfs',
  732. 'dataset@borgmatic-1234',
  733. ).once()
  734. module.remove_data_source_dumps(
  735. hook_config={},
  736. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  737. borgmatic_runtime_directory='/run/borgmatic',
  738. patterns=flexmock(),
  739. dry_run=False,
  740. )
  741. def test_remove_data_source_dumps_with_dry_run_skips_unmount_and_destroy():
  742. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  743. flexmock(module.borgmatic.config.paths).should_receive(
  744. 'replace_temporary_subdirectory_with_glob',
  745. ).and_return('/run/borgmatic')
  746. flexmock(module.glob).should_receive('glob').replace_with(
  747. lambda path: [path.replace('*', 'b33f')],
  748. )
  749. flexmock(module.os.path).should_receive('isdir').and_return(True)
  750. flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
  751. flexmock(module.shutil).should_receive('rmtree').never()
  752. flexmock(module).should_receive('unmount_snapshot').never()
  753. flexmock(module).should_receive('get_all_snapshots').and_return(
  754. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
  755. )
  756. flexmock(module).should_receive('destroy_snapshot').never()
  757. module.remove_data_source_dumps(
  758. hook_config={},
  759. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  760. borgmatic_runtime_directory='/run/borgmatic',
  761. patterns=flexmock(),
  762. dry_run=True,
  763. )