test_zfs.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864
  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(
  302. 'get_last_pattern_index'
  303. ).and_return(0)
  304. flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
  305. object,
  306. Pattern('/mnt/dataset/subdir'),
  307. module.borgmatic.borg.pattern.Pattern(
  308. '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir',
  309. source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
  310. ),
  311. 0,
  312. ).once()
  313. assert (
  314. module.dump_data_sources(
  315. hook_config={},
  316. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  317. config_paths=('test.yaml',),
  318. borgmatic_runtime_directory='/run/borgmatic',
  319. patterns=patterns,
  320. dry_run=False,
  321. )
  322. == []
  323. )
  324. def test_dump_data_sources_with_no_datasets_skips_snapshots():
  325. flexmock(module).should_receive('get_datasets_to_backup').and_return(())
  326. flexmock(module.os).should_receive('getpid').and_return(1234)
  327. flexmock(module).should_receive('snapshot_dataset').never()
  328. flexmock(module).should_receive('mount_snapshot').never()
  329. flexmock(module.borgmatic.hooks.data_source.config).should_receive(
  330. 'get_last_pattern_index'
  331. ).never()
  332. flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
  333. patterns = [Pattern('/mnt/dataset')]
  334. assert (
  335. module.dump_data_sources(
  336. hook_config={},
  337. config={'patterns': flexmock(), 'zfs': {}},
  338. config_paths=('test.yaml',),
  339. borgmatic_runtime_directory='/run/borgmatic',
  340. patterns=patterns,
  341. dry_run=False,
  342. )
  343. == []
  344. )
  345. assert patterns == [Pattern('/mnt/dataset')]
  346. def test_dump_data_sources_uses_custom_commands():
  347. dataset = flexmock(
  348. name='dataset',
  349. mount_point='/mnt/dataset',
  350. contained_patterns=(Pattern('/mnt/dataset/subdir'),),
  351. )
  352. flexmock(module).should_receive('get_datasets_to_backup').and_return((dataset,))
  353. flexmock(module.os).should_receive('getpid').and_return(1234)
  354. full_snapshot_name = 'dataset@borgmatic-1234'
  355. flexmock(module).should_receive('snapshot_dataset').with_args(
  356. '/usr/local/bin/zfs',
  357. full_snapshot_name,
  358. ).once()
  359. flexmock(module.hashlib).should_receive('shake_256').and_return(
  360. flexmock(hexdigest=lambda length: 'b33f'),
  361. )
  362. snapshot_mount_path = '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset'
  363. flexmock(module).should_receive('mount_snapshot').with_args(
  364. '/usr/local/bin/mount',
  365. full_snapshot_name,
  366. module.os.path.normpath(snapshot_mount_path),
  367. ).once()
  368. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  369. Pattern('/mnt/dataset/subdir'),
  370. dataset,
  371. '/run/borgmatic',
  372. ).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir'))
  373. flexmock(module.borgmatic.hooks.data_source.config).should_receive(
  374. 'get_last_pattern_index'
  375. ).and_return(0)
  376. flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
  377. object,
  378. Pattern('/mnt/dataset/subdir'),
  379. module.borgmatic.borg.pattern.Pattern(
  380. '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir',
  381. source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
  382. ),
  383. 0,
  384. ).once()
  385. patterns = [Pattern('/mnt/dataset/subdir')]
  386. hook_config = {
  387. 'zfs_command': '/usr/local/bin/zfs',
  388. 'mount_command': '/usr/local/bin/mount',
  389. }
  390. assert (
  391. module.dump_data_sources(
  392. hook_config=hook_config,
  393. config={
  394. 'patterns': flexmock(),
  395. 'zfs': hook_config,
  396. },
  397. config_paths=('test.yaml',),
  398. borgmatic_runtime_directory='/run/borgmatic',
  399. patterns=patterns,
  400. dry_run=False,
  401. )
  402. == []
  403. )
  404. def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_patterns():
  405. flexmock(module).should_receive('get_datasets_to_backup').and_return(
  406. (flexmock(name='dataset', mount_point='/mnt/dataset'),),
  407. )
  408. flexmock(module.os).should_receive('getpid').and_return(1234)
  409. flexmock(module).should_receive('snapshot_dataset').never()
  410. flexmock(module).should_receive('mount_snapshot').never()
  411. flexmock(module.borgmatic.hooks.data_source.config).should_receive(
  412. 'get_last_pattern_index'
  413. ).and_return(0)
  414. flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
  415. patterns = [Pattern('/mnt/dataset')]
  416. assert (
  417. module.dump_data_sources(
  418. hook_config={},
  419. config={'patterns': ('R /mnt/dataset',), 'zfs': {}},
  420. config_paths=('test.yaml',),
  421. borgmatic_runtime_directory='/run/borgmatic',
  422. patterns=patterns,
  423. dry_run=True,
  424. )
  425. == []
  426. )
  427. def test_dump_data_sources_ignores_mismatch_between_given_patterns_and_contained_patterns():
  428. dataset = flexmock(
  429. name='dataset',
  430. mount_point='/mnt/dataset',
  431. contained_patterns=(Pattern('/mnt/dataset/subdir'),),
  432. )
  433. flexmock(module).should_receive('get_datasets_to_backup').and_return((dataset,))
  434. flexmock(module.os).should_receive('getpid').and_return(1234)
  435. full_snapshot_name = 'dataset@borgmatic-1234'
  436. flexmock(module).should_receive('snapshot_dataset').with_args(
  437. 'zfs',
  438. full_snapshot_name,
  439. ).once()
  440. flexmock(module.hashlib).should_receive('shake_256').and_return(
  441. flexmock(hexdigest=lambda length: 'b33f'),
  442. )
  443. snapshot_mount_path = '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset'
  444. flexmock(module).should_receive('mount_snapshot').with_args(
  445. 'mount',
  446. full_snapshot_name,
  447. module.os.path.normpath(snapshot_mount_path),
  448. ).once()
  449. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  450. Pattern('/mnt/dataset/subdir'),
  451. dataset,
  452. '/run/borgmatic',
  453. ).and_return(Pattern('/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir'))
  454. flexmock(module.borgmatic.hooks.data_source.config).should_receive(
  455. 'get_last_pattern_index'
  456. ).and_return(0)
  457. flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
  458. object,
  459. Pattern('/mnt/dataset/subdir'),
  460. module.borgmatic.borg.pattern.Pattern(
  461. '/run/borgmatic/zfs_snapshots/b33f/./mnt/dataset/subdir',
  462. source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
  463. ),
  464. 0,
  465. ).once()
  466. patterns = [Pattern('/hmm')]
  467. assert (
  468. module.dump_data_sources(
  469. hook_config={},
  470. config={'patterns': ('R /mnt/dataset',), 'zfs': {}},
  471. config_paths=('test.yaml',),
  472. borgmatic_runtime_directory='/run/borgmatic',
  473. patterns=patterns,
  474. dry_run=False,
  475. )
  476. == []
  477. )
  478. def test_get_all_snapshots_parses_list_output():
  479. flexmock(module.borgmatic.execute).should_receive(
  480. 'execute_command_and_capture_output',
  481. ).and_return(
  482. 'dataset1@borgmatic-1234\ndataset2@borgmatic-4567',
  483. )
  484. assert module.get_all_snapshots('zfs') == ('dataset1@borgmatic-1234', 'dataset2@borgmatic-4567')
  485. def test_remove_data_source_dumps_unmounts_and_destroys_snapshots():
  486. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  487. flexmock(module.borgmatic.config.paths).should_receive(
  488. 'replace_temporary_subdirectory_with_glob',
  489. ).and_return('/run/borgmatic')
  490. flexmock(module.glob).should_receive('glob').replace_with(
  491. lambda path: [path.replace('*', 'b33f')],
  492. )
  493. flexmock(module.os.path).should_receive('isdir').and_return(True)
  494. flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
  495. flexmock(module.shutil).should_receive('rmtree')
  496. flexmock(module).should_receive('unmount_snapshot').with_args(
  497. 'umount',
  498. '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
  499. ).once()
  500. flexmock(module).should_receive('get_all_snapshots').and_return(
  501. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
  502. )
  503. flexmock(module).should_receive('destroy_snapshot').with_args(
  504. 'zfs',
  505. 'dataset@borgmatic-1234',
  506. ).once()
  507. module.remove_data_source_dumps(
  508. hook_config={},
  509. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  510. borgmatic_runtime_directory='/run/borgmatic',
  511. patterns=flexmock(),
  512. dry_run=False,
  513. )
  514. def test_remove_data_source_dumps_use_custom_commands():
  515. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  516. flexmock(module.borgmatic.config.paths).should_receive(
  517. 'replace_temporary_subdirectory_with_glob',
  518. ).and_return('/run/borgmatic')
  519. flexmock(module.glob).should_receive('glob').replace_with(
  520. lambda path: [path.replace('*', 'b33f')],
  521. )
  522. flexmock(module.os.path).should_receive('isdir').and_return(True)
  523. flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
  524. flexmock(module.shutil).should_receive('rmtree')
  525. flexmock(module).should_receive('unmount_snapshot').with_args(
  526. '/usr/local/bin/umount',
  527. '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
  528. ).once()
  529. flexmock(module).should_receive('get_all_snapshots').and_return(
  530. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
  531. )
  532. flexmock(module).should_receive('destroy_snapshot').with_args(
  533. '/usr/local/bin/zfs',
  534. 'dataset@borgmatic-1234',
  535. ).once()
  536. hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
  537. module.remove_data_source_dumps(
  538. hook_config=hook_config,
  539. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  540. borgmatic_runtime_directory='/run/borgmatic',
  541. patterns=flexmock(),
  542. dry_run=False,
  543. )
  544. def test_remove_data_source_dumps_bails_for_missing_hook_configuration():
  545. flexmock(module).should_receive('get_all_dataset_mount_points').never()
  546. flexmock(module.borgmatic.config.paths).should_receive(
  547. 'replace_temporary_subdirectory_with_glob',
  548. ).never()
  549. module.remove_data_source_dumps(
  550. hook_config=None,
  551. config={'source_directories': '/mnt/dataset'},
  552. borgmatic_runtime_directory='/run/borgmatic',
  553. patterns=flexmock(),
  554. dry_run=False,
  555. )
  556. def test_remove_data_source_dumps_bails_for_missing_zfs_command():
  557. flexmock(module).should_receive('get_all_dataset_mount_points').and_raise(FileNotFoundError)
  558. flexmock(module.borgmatic.config.paths).should_receive(
  559. 'replace_temporary_subdirectory_with_glob',
  560. ).never()
  561. hook_config = {'zfs_command': 'wtf'}
  562. module.remove_data_source_dumps(
  563. hook_config=hook_config,
  564. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  565. borgmatic_runtime_directory='/run/borgmatic',
  566. patterns=flexmock(),
  567. dry_run=False,
  568. )
  569. def test_remove_data_source_dumps_bails_for_zfs_command_error():
  570. flexmock(module).should_receive('get_all_dataset_mount_points').and_raise(
  571. module.subprocess.CalledProcessError(1, 'wtf'),
  572. )
  573. flexmock(module.borgmatic.config.paths).should_receive(
  574. 'replace_temporary_subdirectory_with_glob',
  575. ).never()
  576. hook_config = {'zfs_command': 'wtf'}
  577. module.remove_data_source_dumps(
  578. hook_config=hook_config,
  579. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  580. borgmatic_runtime_directory='/run/borgmatic',
  581. patterns=flexmock(),
  582. dry_run=False,
  583. )
  584. def test_remove_data_source_dumps_bails_for_missing_umount_command():
  585. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  586. flexmock(module.borgmatic.config.paths).should_receive(
  587. 'replace_temporary_subdirectory_with_glob',
  588. ).and_return('/run/borgmatic')
  589. flexmock(module.glob).should_receive('glob').replace_with(
  590. lambda path: [path.replace('*', 'b33f')],
  591. )
  592. flexmock(module.os.path).should_receive('isdir').and_return(True)
  593. flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
  594. flexmock(module.shutil).should_receive('rmtree')
  595. flexmock(module).should_receive('unmount_snapshot').with_args(
  596. '/usr/local/bin/umount',
  597. '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
  598. ).and_raise(FileNotFoundError)
  599. flexmock(module).should_receive('get_all_snapshots').never()
  600. flexmock(module).should_receive('destroy_snapshot').never()
  601. hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
  602. module.remove_data_source_dumps(
  603. hook_config=hook_config,
  604. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  605. borgmatic_runtime_directory='/run/borgmatic',
  606. patterns=flexmock(),
  607. dry_run=False,
  608. )
  609. def test_remove_data_source_dumps_swallows_umount_command_error():
  610. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  611. flexmock(module.borgmatic.config.paths).should_receive(
  612. 'replace_temporary_subdirectory_with_glob',
  613. ).and_return('/run/borgmatic')
  614. flexmock(module.glob).should_receive('glob').replace_with(
  615. lambda path: [path.replace('*', 'b33f')],
  616. )
  617. flexmock(module.os.path).should_receive('isdir').and_return(True)
  618. flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
  619. flexmock(module.shutil).should_receive('rmtree')
  620. flexmock(module).should_receive('unmount_snapshot').with_args(
  621. '/usr/local/bin/umount',
  622. '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
  623. ).and_raise(module.subprocess.CalledProcessError(1, 'wtf'))
  624. flexmock(module).should_receive('get_all_snapshots').and_return(
  625. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
  626. )
  627. flexmock(module).should_receive('destroy_snapshot').with_args(
  628. '/usr/local/bin/zfs',
  629. 'dataset@borgmatic-1234',
  630. ).once()
  631. hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
  632. module.remove_data_source_dumps(
  633. hook_config=hook_config,
  634. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  635. borgmatic_runtime_directory='/run/borgmatic',
  636. patterns=flexmock(),
  637. dry_run=False,
  638. )
  639. def test_remove_data_source_dumps_skips_unmount_snapshot_directories_that_are_not_actually_directories():
  640. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  641. flexmock(module.borgmatic.config.paths).should_receive(
  642. 'replace_temporary_subdirectory_with_glob',
  643. ).and_return('/run/borgmatic')
  644. flexmock(module.glob).should_receive('glob').replace_with(
  645. lambda path: [path.replace('*', 'b33f')],
  646. )
  647. flexmock(module.os.path).should_receive('isdir').and_return(False)
  648. flexmock(module.shutil).should_receive('rmtree').never()
  649. flexmock(module).should_receive('unmount_snapshot').never()
  650. flexmock(module).should_receive('get_all_snapshots').and_return(
  651. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
  652. )
  653. flexmock(module).should_receive('destroy_snapshot').with_args(
  654. 'zfs',
  655. 'dataset@borgmatic-1234',
  656. ).once()
  657. module.remove_data_source_dumps(
  658. hook_config={},
  659. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  660. borgmatic_runtime_directory='/run/borgmatic',
  661. patterns=flexmock(),
  662. dry_run=False,
  663. )
  664. def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_that_are_not_actually_directories():
  665. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  666. flexmock(module.borgmatic.config.paths).should_receive(
  667. 'replace_temporary_subdirectory_with_glob',
  668. ).and_return('/run/borgmatic')
  669. flexmock(module.glob).should_receive('glob').replace_with(
  670. lambda path: [path.replace('*', 'b33f')],
  671. )
  672. flexmock(module.os.path).should_receive('isdir').with_args(
  673. '/run/borgmatic/zfs_snapshots/b33f',
  674. ).and_return(True)
  675. flexmock(module.os.path).should_receive('isdir').with_args(
  676. '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
  677. ).and_return(False)
  678. flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
  679. flexmock(module.shutil).should_receive('rmtree')
  680. flexmock(module).should_receive('unmount_snapshot').never()
  681. flexmock(module).should_receive('get_all_snapshots').and_return(
  682. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
  683. )
  684. flexmock(module).should_receive('destroy_snapshot').with_args(
  685. 'zfs',
  686. 'dataset@borgmatic-1234',
  687. ).once()
  688. module.remove_data_source_dumps(
  689. hook_config={},
  690. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  691. borgmatic_runtime_directory='/run/borgmatic',
  692. patterns=flexmock(),
  693. dry_run=False,
  694. )
  695. def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_that_are_empty():
  696. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  697. flexmock(module.borgmatic.config.paths).should_receive(
  698. 'replace_temporary_subdirectory_with_glob',
  699. ).and_return('/run/borgmatic')
  700. flexmock(module.glob).should_receive('glob').replace_with(
  701. lambda path: [path.replace('*', 'b33f')],
  702. )
  703. flexmock(module.os.path).should_receive('isdir').with_args(
  704. '/run/borgmatic/zfs_snapshots/b33f',
  705. ).and_return(True)
  706. flexmock(module.os.path).should_receive('isdir').with_args(
  707. '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
  708. ).and_return(True)
  709. flexmock(module.os).should_receive('listdir').with_args(
  710. '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
  711. ).and_return([])
  712. flexmock(module.shutil).should_receive('rmtree')
  713. flexmock(module).should_receive('unmount_snapshot').never()
  714. flexmock(module).should_receive('get_all_snapshots').and_return(
  715. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
  716. )
  717. flexmock(module).should_receive('destroy_snapshot').with_args(
  718. 'zfs',
  719. 'dataset@borgmatic-1234',
  720. ).once()
  721. module.remove_data_source_dumps(
  722. hook_config={},
  723. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  724. borgmatic_runtime_directory='/run/borgmatic',
  725. patterns=flexmock(),
  726. dry_run=False,
  727. )
  728. def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_after_rmtree_succeeds():
  729. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  730. flexmock(module.borgmatic.config.paths).should_receive(
  731. 'replace_temporary_subdirectory_with_glob',
  732. ).and_return('/run/borgmatic')
  733. flexmock(module.glob).should_receive('glob').replace_with(
  734. lambda path: [path.replace('*', 'b33f')],
  735. )
  736. flexmock(module.os.path).should_receive('isdir').with_args(
  737. '/run/borgmatic/zfs_snapshots/b33f',
  738. ).and_return(True)
  739. flexmock(module.os.path).should_receive('isdir').with_args(
  740. '/run/borgmatic/zfs_snapshots/b33f/mnt/dataset',
  741. ).and_return(True).and_return(False)
  742. flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
  743. flexmock(module.shutil).should_receive('rmtree')
  744. flexmock(module).should_receive('unmount_snapshot').never()
  745. flexmock(module).should_receive('get_all_snapshots').and_return(
  746. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
  747. )
  748. flexmock(module).should_receive('destroy_snapshot').with_args(
  749. 'zfs',
  750. 'dataset@borgmatic-1234',
  751. ).once()
  752. module.remove_data_source_dumps(
  753. hook_config={},
  754. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  755. borgmatic_runtime_directory='/run/borgmatic',
  756. patterns=flexmock(),
  757. dry_run=False,
  758. )
  759. def test_remove_data_source_dumps_with_dry_run_skips_unmount_and_destroy():
  760. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  761. flexmock(module.borgmatic.config.paths).should_receive(
  762. 'replace_temporary_subdirectory_with_glob',
  763. ).and_return('/run/borgmatic')
  764. flexmock(module.glob).should_receive('glob').replace_with(
  765. lambda path: [path.replace('*', 'b33f')],
  766. )
  767. flexmock(module.os.path).should_receive('isdir').and_return(True)
  768. flexmock(module.os).should_receive('listdir').and_return(['file.txt'])
  769. flexmock(module.shutil).should_receive('rmtree').never()
  770. flexmock(module).should_receive('unmount_snapshot').never()
  771. flexmock(module).should_receive('get_all_snapshots').and_return(
  772. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid'),
  773. )
  774. flexmock(module).should_receive('destroy_snapshot').never()
  775. module.remove_data_source_dumps(
  776. hook_config={},
  777. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  778. borgmatic_runtime_directory='/run/borgmatic',
  779. patterns=flexmock(),
  780. dry_run=True,
  781. )