test_zfs.py 29 KB

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