test_btrfs.py 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177
  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 btrfs as module
  5. def test_path_is_a_subvolume_with_btrfs_success_call_returns_true():
  6. module.path_is_a_subvolume.cache_clear()
  7. flexmock(module.borgmatic.execute).should_receive(
  8. 'execute_command',
  9. ).with_args(('btrfs', 'subvolume', 'show', '/mnt0'), output_log_level=None, close_fds=True)
  10. assert module.path_is_a_subvolume('btrfs', '/mnt0') is True
  11. def test_path_is_a_subvolume_with_btrfs_error_returns_false():
  12. module.path_is_a_subvolume.cache_clear()
  13. flexmock(module.borgmatic.execute).should_receive(
  14. 'execute_command',
  15. ).with_args(
  16. ('btrfs', 'subvolume', 'show', '/mnt0'), output_log_level=None, close_fds=True
  17. ).and_raise(
  18. module.subprocess.CalledProcessError(1, 'btrfs'),
  19. )
  20. assert module.path_is_a_subvolume('btrfs', '/mnt0') is False
  21. def test_path_is_a_subvolume_caches_result_after_first_call():
  22. module.path_is_a_subvolume.cache_clear()
  23. flexmock(module.borgmatic.execute).should_receive(
  24. 'execute_command',
  25. ).once()
  26. assert module.path_is_a_subvolume('btrfs', '/mnt0') is True
  27. assert module.path_is_a_subvolume('btrfs', '/mnt0') is True
  28. def test_get_subvolume_property_with_invalid_btrfs_output_errors():
  29. module.get_subvolume_property.cache_clear()
  30. flexmock(module.borgmatic.execute).should_receive(
  31. 'execute_command_and_capture_output',
  32. ).and_return('invalid')
  33. with pytest.raises(ValueError):
  34. module.get_subvolume_property('btrfs', '/foo', 'ro')
  35. def test_get_subvolume_property_with_true_output_returns_true_bool():
  36. module.get_subvolume_property.cache_clear()
  37. flexmock(module.borgmatic.execute).should_receive(
  38. 'execute_command_and_capture_output',
  39. ).and_return('ro=true')
  40. assert module.get_subvolume_property('btrfs', '/foo', 'ro') is True
  41. def test_get_subvolume_property_with_false_output_returns_false_bool():
  42. module.get_subvolume_property.cache_clear()
  43. flexmock(module.borgmatic.execute).should_receive(
  44. 'execute_command_and_capture_output',
  45. ).and_return('ro=false')
  46. assert module.get_subvolume_property('btrfs', '/foo', 'ro') is False
  47. def test_get_subvolume_property_passes_through_general_value():
  48. module.get_subvolume_property.cache_clear()
  49. flexmock(module.borgmatic.execute).should_receive(
  50. 'execute_command_and_capture_output',
  51. ).and_return('thing=value')
  52. assert module.get_subvolume_property('btrfs', '/foo', 'thing') == 'value'
  53. def test_get_subvolume_property_caches_result_after_first_call():
  54. module.get_subvolume_property.cache_clear()
  55. flexmock(module.borgmatic.execute).should_receive(
  56. 'execute_command_and_capture_output',
  57. ).and_return('thing=value').once()
  58. assert module.get_subvolume_property('btrfs', '/foo', 'thing') == 'value'
  59. assert module.get_subvolume_property('btrfs', '/foo', 'thing') == 'value'
  60. def test_get_containing_subvolume_path_with_subvolume_self_returns_it():
  61. flexmock(module).should_receive('path_is_a_subvolume').with_args(
  62. 'btrfs', '/foo/bar/baz'
  63. ).and_return(True)
  64. flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/foo/bar').never()
  65. flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/foo').never()
  66. flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/').never()
  67. flexmock(module).should_receive('get_subvolume_property').and_return(False)
  68. assert module.get_containing_subvolume_path('btrfs', '/foo/bar/baz') == '/foo/bar/baz'
  69. def test_get_containing_subvolume_path_with_subvolume_parent_returns_it():
  70. flexmock(module).should_receive('path_is_a_subvolume').with_args(
  71. 'btrfs', '/foo/bar/baz'
  72. ).and_return(False)
  73. flexmock(module).should_receive('path_is_a_subvolume').with_args(
  74. 'btrfs', '/foo/bar'
  75. ).and_return(True)
  76. flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/foo').never()
  77. flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/').never()
  78. flexmock(module).should_receive('get_subvolume_property').and_return(False)
  79. assert module.get_containing_subvolume_path('btrfs', '/foo/bar/baz') == '/foo/bar'
  80. def test_get_containing_subvolume_path_with_subvolume_grandparent_returns_it():
  81. flexmock(module).should_receive('path_is_a_subvolume').with_args(
  82. 'btrfs', '/foo/bar/baz'
  83. ).and_return(False)
  84. flexmock(module).should_receive('path_is_a_subvolume').with_args(
  85. 'btrfs', '/foo/bar'
  86. ).and_return(False)
  87. flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/foo').and_return(
  88. True
  89. )
  90. flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/').never()
  91. flexmock(module).should_receive('get_subvolume_property').and_return(False)
  92. assert module.get_containing_subvolume_path('btrfs', '/foo/bar/baz') == '/foo'
  93. def test_get_containing_subvolume_path_without_subvolume_ancestor_returns_none():
  94. flexmock(module).should_receive('path_is_a_subvolume').with_args(
  95. 'btrfs', '/foo/bar/baz'
  96. ).and_return(False)
  97. flexmock(module).should_receive('path_is_a_subvolume').with_args(
  98. 'btrfs', '/foo/bar'
  99. ).and_return(False)
  100. flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/foo').and_return(
  101. False
  102. )
  103. flexmock(module).should_receive('path_is_a_subvolume').with_args('btrfs', '/').and_return(False)
  104. flexmock(module).should_receive('get_subvolume_property').and_return(False)
  105. assert module.get_containing_subvolume_path('btrfs', '/foo/bar/baz') is None
  106. def test_get_containing_subvolume_path_with_read_only_subvolume_returns_none():
  107. flexmock(module).should_receive('path_is_a_subvolume').with_args(
  108. 'btrfs', '/foo/bar/baz'
  109. ).and_return(True)
  110. flexmock(module).should_receive('get_subvolume_property').and_return(True)
  111. assert module.get_containing_subvolume_path('btrfs', '/foo/bar/baz') is None
  112. def test_get_containing_subvolume_path_with_read_only_error_returns_none():
  113. flexmock(module).should_receive('path_is_a_subvolume').with_args(
  114. 'btrfs', '/foo/bar/baz'
  115. ).and_return(True)
  116. flexmock(module).should_receive('get_subvolume_property').and_raise(
  117. module.subprocess.CalledProcessError(1, 'wtf')
  118. )
  119. assert module.get_containing_subvolume_path('btrfs', '/foo/bar/baz') is None
  120. def test_get_all_subvolume_paths_skips_non_root_and_non_config_patterns():
  121. flexmock(module).should_receive('get_containing_subvolume_path').with_args(
  122. 'btrfs', '/foo'
  123. ).never()
  124. flexmock(module).should_receive('get_containing_subvolume_path').with_args(
  125. 'btrfs', '/bar'
  126. ).and_return('/bar').once()
  127. flexmock(module).should_receive('get_containing_subvolume_path').with_args(
  128. 'btrfs', '/baz'
  129. ).never()
  130. assert module.get_all_subvolume_paths(
  131. 'btrfs',
  132. (
  133. module.borgmatic.borg.pattern.Pattern(
  134. '/foo',
  135. type=module.borgmatic.borg.pattern.Pattern_type.ROOT,
  136. source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
  137. ),
  138. module.borgmatic.borg.pattern.Pattern(
  139. '/bar',
  140. type=module.borgmatic.borg.pattern.Pattern_type.ROOT,
  141. source=module.borgmatic.borg.pattern.Pattern_source.CONFIG,
  142. ),
  143. module.borgmatic.borg.pattern.Pattern(
  144. '/baz',
  145. type=module.borgmatic.borg.pattern.Pattern_type.INCLUDE,
  146. source=module.borgmatic.borg.pattern.Pattern_source.CONFIG,
  147. ),
  148. ),
  149. ) == ('/bar',)
  150. def test_get_all_subvolume_paths_skips_non_btrfs_patterns():
  151. flexmock(module).should_receive('get_containing_subvolume_path').with_args(
  152. 'btrfs', '/foo'
  153. ).and_return(None).once()
  154. flexmock(module).should_receive('get_containing_subvolume_path').with_args(
  155. 'btrfs', '/bar'
  156. ).and_return('/bar').once()
  157. assert module.get_all_subvolume_paths(
  158. 'btrfs',
  159. (
  160. module.borgmatic.borg.pattern.Pattern(
  161. '/foo',
  162. type=module.borgmatic.borg.pattern.Pattern_type.ROOT,
  163. source=module.borgmatic.borg.pattern.Pattern_source.CONFIG,
  164. ),
  165. module.borgmatic.borg.pattern.Pattern(
  166. '/bar',
  167. type=module.borgmatic.borg.pattern.Pattern_type.ROOT,
  168. source=module.borgmatic.borg.pattern.Pattern_source.CONFIG,
  169. ),
  170. ),
  171. ) == ('/bar',)
  172. def test_get_all_subvolume_paths_sorts_subvolume_paths():
  173. flexmock(module).should_receive('get_containing_subvolume_path').with_args(
  174. 'btrfs', '/foo'
  175. ).and_return('/foo').once()
  176. flexmock(module).should_receive('get_containing_subvolume_path').with_args(
  177. 'btrfs', '/bar'
  178. ).and_return('/bar').once()
  179. flexmock(module).should_receive('get_containing_subvolume_path').with_args(
  180. 'btrfs', '/baz'
  181. ).and_return('/baz').once()
  182. assert module.get_all_subvolume_paths(
  183. 'btrfs',
  184. (
  185. module.borgmatic.borg.pattern.Pattern(
  186. '/foo',
  187. type=module.borgmatic.borg.pattern.Pattern_type.ROOT,
  188. source=module.borgmatic.borg.pattern.Pattern_source.CONFIG,
  189. ),
  190. module.borgmatic.borg.pattern.Pattern(
  191. '/bar',
  192. type=module.borgmatic.borg.pattern.Pattern_type.ROOT,
  193. source=module.borgmatic.borg.pattern.Pattern_source.CONFIG,
  194. ),
  195. module.borgmatic.borg.pattern.Pattern(
  196. '/baz',
  197. type=module.borgmatic.borg.pattern.Pattern_type.ROOT,
  198. source=module.borgmatic.borg.pattern.Pattern_source.CONFIG,
  199. ),
  200. ),
  201. ) == ('/bar', '/baz', '/foo')
  202. def test_get_subvolumes_collects_subvolumes_matching_patterns():
  203. flexmock(module).should_receive('get_all_subvolume_paths').and_return(('/mnt1', '/mnt2'))
  204. contained_pattern = Pattern(
  205. '/mnt1',
  206. type=Pattern_type.ROOT,
  207. source=Pattern_source.CONFIG,
  208. )
  209. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  210. 'get_contained_patterns',
  211. ).with_args('/mnt1', object).and_return((contained_pattern,))
  212. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  213. 'get_contained_patterns',
  214. ).with_args('/mnt2', object).and_return(())
  215. assert module.get_subvolumes(
  216. 'btrfs',
  217. patterns=[
  218. Pattern('/mnt1'),
  219. Pattern('/mnt3'),
  220. ],
  221. ) == (module.Subvolume('/mnt1', contained_patterns=(contained_pattern,)),)
  222. def test_get_subvolumes_skips_non_root_patterns():
  223. flexmock(module).should_receive('get_all_subvolume_paths').and_return(('/mnt1', '/mnt2'))
  224. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  225. 'get_contained_patterns',
  226. ).with_args('/mnt1', object).and_return(
  227. (
  228. Pattern(
  229. '/mnt1',
  230. type=Pattern_type.EXCLUDE,
  231. source=Pattern_source.CONFIG,
  232. ),
  233. ),
  234. )
  235. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  236. 'get_contained_patterns',
  237. ).with_args('/mnt2', object).and_return(())
  238. assert (
  239. module.get_subvolumes(
  240. 'btrfs',
  241. patterns=[
  242. Pattern('/mnt1'),
  243. Pattern('/mnt3'),
  244. ],
  245. )
  246. == ()
  247. )
  248. def test_get_subvolumes_skips_non_config_patterns():
  249. flexmock(module).should_receive('get_all_subvolume_paths').and_return(('/mnt1', '/mnt2'))
  250. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  251. 'get_contained_patterns',
  252. ).with_args('/mnt1', object).and_return(
  253. (
  254. Pattern(
  255. '/mnt1',
  256. type=Pattern_type.ROOT,
  257. source=Pattern_source.HOOK,
  258. ),
  259. ),
  260. )
  261. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  262. 'get_contained_patterns',
  263. ).with_args('/mnt2', object).and_return(())
  264. assert (
  265. module.get_subvolumes(
  266. 'btrfs',
  267. patterns=[
  268. Pattern('/mnt1'),
  269. Pattern('/mnt3'),
  270. ],
  271. )
  272. == ()
  273. )
  274. @pytest.mark.parametrize(
  275. 'subvolume_path,expected_snapshot_path',
  276. (
  277. ('/foo/bar', '/foo/bar/.borgmatic-snapshot-1234/foo/bar'),
  278. ('/', '/.borgmatic-snapshot-1234'),
  279. ),
  280. )
  281. def test_make_snapshot_path_includes_stripped_subvolume_path(
  282. subvolume_path,
  283. expected_snapshot_path,
  284. ):
  285. flexmock(module.os).should_receive('getpid').and_return(1234)
  286. assert module.make_snapshot_path(subvolume_path) == expected_snapshot_path
  287. @pytest.mark.parametrize(
  288. 'subvolume_path,pattern,expected_pattern',
  289. (
  290. (
  291. '/foo/bar',
  292. Pattern('/foo/bar/baz'),
  293. Pattern('/foo/bar/.borgmatic-snapshot-1234/./foo/bar/baz'),
  294. ),
  295. ('/foo/bar', Pattern('/foo/bar'), Pattern('/foo/bar/.borgmatic-snapshot-1234/./foo/bar')),
  296. (
  297. '/foo/bar',
  298. Pattern('^/foo/bar', Pattern_type.INCLUDE, Pattern_style.REGULAR_EXPRESSION),
  299. Pattern(
  300. '^/foo/bar/.borgmatic-snapshot-1234/./foo/bar',
  301. Pattern_type.INCLUDE,
  302. Pattern_style.REGULAR_EXPRESSION,
  303. ),
  304. ),
  305. (
  306. '/foo/bar',
  307. Pattern('/foo/bar', Pattern_type.INCLUDE, Pattern_style.REGULAR_EXPRESSION),
  308. Pattern(
  309. '/foo/bar/.borgmatic-snapshot-1234/./foo/bar',
  310. Pattern_type.INCLUDE,
  311. Pattern_style.REGULAR_EXPRESSION,
  312. ),
  313. ),
  314. ('/', Pattern('/foo'), Pattern('/.borgmatic-snapshot-1234/./foo')),
  315. ('/', Pattern('/'), Pattern('/.borgmatic-snapshot-1234/./')),
  316. (
  317. '/foo/bar',
  318. Pattern('/foo/bar/./baz'),
  319. Pattern('/foo/bar/.borgmatic-snapshot-1234/foo/bar/./baz'),
  320. ),
  321. ),
  322. )
  323. def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_path(
  324. subvolume_path,
  325. pattern,
  326. expected_pattern,
  327. ):
  328. flexmock(module.os).should_receive('getpid').and_return(1234)
  329. assert module.make_borg_snapshot_pattern(subvolume_path, pattern) == expected_pattern
  330. def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns():
  331. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  332. config = {'btrfs': {}}
  333. flexmock(module).should_receive('get_subvolumes').and_return(
  334. (
  335. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  336. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  337. ),
  338. )
  339. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  340. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  341. )
  342. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  343. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  344. )
  345. flexmock(module).should_receive('snapshot_subvolume').with_args(
  346. 'btrfs',
  347. '/mnt/subvol1',
  348. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  349. ).once()
  350. flexmock(module).should_receive('snapshot_subvolume').with_args(
  351. 'btrfs',
  352. '/mnt/subvol2',
  353. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  354. ).once()
  355. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  356. '/mnt/subvol1',
  357. ).and_return(
  358. Pattern(
  359. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  360. Pattern_type.NO_RECURSE,
  361. Pattern_style.FNMATCH,
  362. ),
  363. )
  364. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  365. '/mnt/subvol2',
  366. ).and_return(
  367. Pattern(
  368. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
  369. Pattern_type.NO_RECURSE,
  370. Pattern_style.FNMATCH,
  371. ),
  372. )
  373. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  374. '/mnt/subvol1',
  375. object,
  376. ).and_return(Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'))
  377. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  378. '/mnt/subvol2',
  379. object,
  380. ).and_return(Pattern('/mnt/subvol2/.borgmatic-1234/mnt/subvol2'))
  381. assert (
  382. module.dump_data_sources(
  383. hook_config=config['btrfs'],
  384. config=config,
  385. config_paths=('test.yaml',),
  386. borgmatic_runtime_directory='/run/borgmatic',
  387. patterns=patterns,
  388. dry_run=False,
  389. )
  390. == []
  391. )
  392. assert patterns == [
  393. Pattern('/foo'),
  394. Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'),
  395. Pattern(
  396. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  397. Pattern_type.NO_RECURSE,
  398. Pattern_style.FNMATCH,
  399. ),
  400. Pattern('/mnt/subvol2/.borgmatic-1234/mnt/subvol2'),
  401. Pattern(
  402. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
  403. Pattern_type.NO_RECURSE,
  404. Pattern_style.FNMATCH,
  405. ),
  406. ]
  407. assert config == {
  408. 'btrfs': {},
  409. }
  410. def test_dump_data_sources_uses_custom_btrfs_command_in_commands():
  411. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  412. config = {'btrfs': {'btrfs_command': '/usr/local/bin/btrfs'}}
  413. flexmock(module).should_receive('get_subvolumes').and_return(
  414. (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),),
  415. )
  416. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  417. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  418. )
  419. flexmock(module).should_receive('snapshot_subvolume').with_args(
  420. '/usr/local/bin/btrfs',
  421. '/mnt/subvol1',
  422. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  423. ).once()
  424. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  425. '/mnt/subvol1',
  426. ).and_return(
  427. Pattern(
  428. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  429. Pattern_type.NO_RECURSE,
  430. Pattern_style.FNMATCH,
  431. ),
  432. )
  433. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  434. '/mnt/subvol1',
  435. object,
  436. ).and_return(Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'))
  437. assert (
  438. module.dump_data_sources(
  439. hook_config=config['btrfs'],
  440. config=config,
  441. config_paths=('test.yaml',),
  442. borgmatic_runtime_directory='/run/borgmatic',
  443. patterns=patterns,
  444. dry_run=False,
  445. )
  446. == []
  447. )
  448. assert patterns == [
  449. Pattern('/foo'),
  450. Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'),
  451. Pattern(
  452. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  453. Pattern_type.NO_RECURSE,
  454. Pattern_style.FNMATCH,
  455. ),
  456. ]
  457. assert config == {
  458. 'btrfs': {
  459. 'btrfs_command': '/usr/local/bin/btrfs',
  460. },
  461. }
  462. def test_dump_data_sources_with_findmnt_command_warns():
  463. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  464. config = {'btrfs': {'findmnt_command': '/usr/local/bin/findmnt'}}
  465. flexmock(module.logger).should_receive('warning').once()
  466. flexmock(module).should_receive('get_subvolumes').with_args(
  467. 'btrfs',
  468. patterns,
  469. ).and_return(
  470. (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),),
  471. ).once()
  472. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  473. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  474. )
  475. flexmock(module).should_receive('snapshot_subvolume').with_args(
  476. 'btrfs',
  477. '/mnt/subvol1',
  478. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  479. ).once()
  480. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  481. '/mnt/subvol1',
  482. ).and_return(
  483. Pattern(
  484. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  485. Pattern_type.NO_RECURSE,
  486. Pattern_style.FNMATCH,
  487. ),
  488. )
  489. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  490. '/mnt/subvol1',
  491. object,
  492. ).and_return(Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'))
  493. assert (
  494. module.dump_data_sources(
  495. hook_config=config['btrfs'],
  496. config=config,
  497. config_paths=('test.yaml',),
  498. borgmatic_runtime_directory='/run/borgmatic',
  499. patterns=patterns,
  500. dry_run=False,
  501. )
  502. == []
  503. )
  504. assert patterns == [
  505. Pattern('/foo'),
  506. Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'),
  507. Pattern(
  508. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  509. Pattern_type.NO_RECURSE,
  510. Pattern_style.FNMATCH,
  511. ),
  512. ]
  513. assert config == {
  514. 'btrfs': {
  515. 'findmnt_command': '/usr/local/bin/findmnt',
  516. },
  517. }
  518. def test_dump_data_sources_with_dry_run_skips_snapshot_and_patterns_update():
  519. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  520. config = {'btrfs': {}}
  521. flexmock(module).should_receive('get_subvolumes').and_return(
  522. (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),),
  523. )
  524. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  525. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  526. )
  527. flexmock(module).should_receive('snapshot_subvolume').never()
  528. flexmock(module).should_receive('make_snapshot_exclude_pattern').never()
  529. assert (
  530. module.dump_data_sources(
  531. hook_config=config['btrfs'],
  532. config=config,
  533. config_paths=('test.yaml',),
  534. borgmatic_runtime_directory='/run/borgmatic',
  535. patterns=patterns,
  536. dry_run=True,
  537. )
  538. == []
  539. )
  540. assert patterns == [Pattern('/foo'), Pattern('/mnt/subvol1')]
  541. assert config == {'btrfs': {}}
  542. def test_dump_data_sources_without_matching_subvolumes_skips_snapshot_and_patterns_update():
  543. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  544. config = {'btrfs': {}}
  545. flexmock(module).should_receive('get_subvolumes').and_return(())
  546. flexmock(module).should_receive('make_snapshot_path').never()
  547. flexmock(module).should_receive('snapshot_subvolume').never()
  548. flexmock(module).should_receive('make_snapshot_exclude_pattern').never()
  549. assert (
  550. module.dump_data_sources(
  551. hook_config=config['btrfs'],
  552. config=config,
  553. config_paths=('test.yaml',),
  554. borgmatic_runtime_directory='/run/borgmatic',
  555. patterns=patterns,
  556. dry_run=False,
  557. )
  558. == []
  559. )
  560. assert patterns == [Pattern('/foo'), Pattern('/mnt/subvol1')]
  561. assert config == {'btrfs': {}}
  562. def test_dump_data_sources_snapshots_adds_to_existing_exclude_patterns():
  563. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  564. config = {'btrfs': {}, 'exclude_patterns': ['/bar']}
  565. flexmock(module).should_receive('get_subvolumes').and_return(
  566. (
  567. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  568. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  569. ),
  570. )
  571. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  572. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  573. )
  574. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  575. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  576. )
  577. flexmock(module).should_receive('snapshot_subvolume').with_args(
  578. 'btrfs',
  579. '/mnt/subvol1',
  580. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  581. ).once()
  582. flexmock(module).should_receive('snapshot_subvolume').with_args(
  583. 'btrfs',
  584. '/mnt/subvol2',
  585. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  586. ).once()
  587. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  588. '/mnt/subvol1',
  589. ).and_return(
  590. Pattern(
  591. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  592. Pattern_type.NO_RECURSE,
  593. Pattern_style.FNMATCH,
  594. ),
  595. )
  596. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  597. '/mnt/subvol2',
  598. ).and_return(
  599. Pattern(
  600. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
  601. Pattern_type.NO_RECURSE,
  602. Pattern_style.FNMATCH,
  603. ),
  604. )
  605. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  606. '/mnt/subvol1',
  607. object,
  608. ).and_return(Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'))
  609. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  610. '/mnt/subvol2',
  611. object,
  612. ).and_return(Pattern('/mnt/subvol2/.borgmatic-1234/mnt/subvol2'))
  613. assert (
  614. module.dump_data_sources(
  615. hook_config=config['btrfs'],
  616. config=config,
  617. config_paths=('test.yaml',),
  618. borgmatic_runtime_directory='/run/borgmatic',
  619. patterns=patterns,
  620. dry_run=False,
  621. )
  622. == []
  623. )
  624. assert patterns == [
  625. Pattern('/foo'),
  626. Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'),
  627. Pattern(
  628. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  629. Pattern_type.NO_RECURSE,
  630. Pattern_style.FNMATCH,
  631. ),
  632. Pattern('/mnt/subvol2/.borgmatic-1234/mnt/subvol2'),
  633. Pattern(
  634. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
  635. Pattern_type.NO_RECURSE,
  636. Pattern_style.FNMATCH,
  637. ),
  638. ]
  639. assert config == {
  640. 'btrfs': {},
  641. 'exclude_patterns': ['/bar'],
  642. }
  643. def test_remove_data_source_dumps_deletes_snapshots():
  644. config = {'btrfs': {}}
  645. flexmock(module).should_receive('get_subvolumes').and_return(
  646. (
  647. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  648. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  649. ),
  650. )
  651. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  652. '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1',
  653. )
  654. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  655. '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2',
  656. )
  657. flexmock(module.borgmatic.config.paths).should_receive(
  658. 'replace_temporary_subdirectory_with_glob',
  659. ).with_args(
  660. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  661. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  662. ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
  663. flexmock(module.borgmatic.config.paths).should_receive(
  664. 'replace_temporary_subdirectory_with_glob',
  665. ).with_args(
  666. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  667. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  668. ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
  669. flexmock(module.glob).should_receive('glob').with_args(
  670. '/mnt/subvol1/.borgmatic-*/mnt/subvol1',
  671. ).and_return(
  672. ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'),
  673. )
  674. flexmock(module.glob).should_receive('glob').with_args(
  675. '/mnt/subvol2/.borgmatic-*/mnt/subvol2',
  676. ).and_return(
  677. ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'),
  678. )
  679. flexmock(module.os.path).should_receive('isdir').with_args(
  680. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  681. ).and_return(True)
  682. flexmock(module.os.path).should_receive('isdir').with_args(
  683. '/mnt/subvol1/.borgmatic-5678/mnt/subvol1',
  684. ).and_return(True)
  685. flexmock(module.os.path).should_receive('isdir').with_args(
  686. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  687. ).and_return(True)
  688. flexmock(module.os.path).should_receive('isdir').with_args(
  689. '/mnt/subvol2/.borgmatic-5678/mnt/subvol2',
  690. ).and_return(False)
  691. flexmock(module).should_receive('delete_snapshot').with_args(
  692. 'btrfs',
  693. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  694. ).once()
  695. flexmock(module).should_receive('delete_snapshot').with_args(
  696. 'btrfs',
  697. '/mnt/subvol1/.borgmatic-5678/mnt/subvol1',
  698. ).once()
  699. flexmock(module).should_receive('delete_snapshot').with_args(
  700. 'btrfs',
  701. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  702. ).once()
  703. flexmock(module).should_receive('delete_snapshot').with_args(
  704. 'btrfs',
  705. '/mnt/subvol2/.borgmatic-5678/mnt/subvol2',
  706. ).never()
  707. flexmock(module.os.path).should_receive('isdir').with_args(
  708. '/mnt/subvol1/.borgmatic-1234',
  709. ).and_return(True)
  710. flexmock(module.os.path).should_receive('isdir').with_args(
  711. '/mnt/subvol1/.borgmatic-5678',
  712. ).and_return(True)
  713. flexmock(module.os.path).should_receive('isdir').with_args(
  714. '/mnt/subvol2/.borgmatic-1234',
  715. ).and_return(True)
  716. flexmock(module.os.path).should_receive('isdir').with_args(
  717. '/mnt/subvol2/.borgmatic-5678',
  718. ).and_return(True)
  719. flexmock(module.shutil).should_receive('rmtree').with_args(
  720. '/mnt/subvol1/.borgmatic-1234',
  721. ).once()
  722. flexmock(module.shutil).should_receive('rmtree').with_args(
  723. '/mnt/subvol1/.borgmatic-5678',
  724. ).once()
  725. flexmock(module.shutil).should_receive('rmtree').with_args(
  726. '/mnt/subvol2/.borgmatic-1234',
  727. ).once()
  728. flexmock(module.shutil).should_receive('rmtree').with_args(
  729. '/mnt/subvol2/.borgmatic-5678',
  730. ).never()
  731. module.remove_data_source_dumps(
  732. hook_config=config['btrfs'],
  733. config=config,
  734. borgmatic_runtime_directory='/run/borgmatic',
  735. patterns=flexmock(),
  736. dry_run=False,
  737. )
  738. def test_remove_data_source_dumps_without_hook_configuration_bails():
  739. flexmock(module).should_receive('get_subvolumes').never()
  740. flexmock(module).should_receive('make_snapshot_path').never()
  741. flexmock(module.borgmatic.config.paths).should_receive(
  742. 'replace_temporary_subdirectory_with_glob',
  743. ).never()
  744. flexmock(module).should_receive('delete_snapshot').never()
  745. flexmock(module.shutil).should_receive('rmtree').never()
  746. module.remove_data_source_dumps(
  747. hook_config=None,
  748. config={'source_directories': '/mnt/subvolume'},
  749. borgmatic_runtime_directory='/run/borgmatic',
  750. patterns=flexmock(),
  751. dry_run=False,
  752. )
  753. def test_remove_data_source_dumps_with_get_subvolumes_file_not_found_error_bails():
  754. config = {'btrfs': {}}
  755. flexmock(module).should_receive('get_subvolumes').and_raise(FileNotFoundError)
  756. flexmock(module).should_receive('make_snapshot_path').never()
  757. flexmock(module.borgmatic.config.paths).should_receive(
  758. 'replace_temporary_subdirectory_with_glob',
  759. ).never()
  760. flexmock(module).should_receive('delete_snapshot').never()
  761. flexmock(module.shutil).should_receive('rmtree').never()
  762. module.remove_data_source_dumps(
  763. hook_config=config['btrfs'],
  764. config=config,
  765. borgmatic_runtime_directory='/run/borgmatic',
  766. patterns=flexmock(),
  767. dry_run=False,
  768. )
  769. def test_remove_data_source_dumps_with_get_subvolumes_called_process_error_bails():
  770. config = {'btrfs': {}}
  771. flexmock(module).should_receive('get_subvolumes').and_raise(
  772. module.subprocess.CalledProcessError(1, 'command', 'error'),
  773. )
  774. flexmock(module).should_receive('make_snapshot_path').never()
  775. flexmock(module.borgmatic.config.paths).should_receive(
  776. 'replace_temporary_subdirectory_with_glob',
  777. ).never()
  778. flexmock(module).should_receive('delete_snapshot').never()
  779. flexmock(module.shutil).should_receive('rmtree').never()
  780. module.remove_data_source_dumps(
  781. hook_config=config['btrfs'],
  782. config=config,
  783. borgmatic_runtime_directory='/run/borgmatic',
  784. patterns=flexmock(),
  785. dry_run=False,
  786. )
  787. def test_remove_data_source_dumps_with_dry_run_skips_deletes():
  788. config = {'btrfs': {}}
  789. flexmock(module).should_receive('get_subvolumes').and_return(
  790. (
  791. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  792. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  793. ),
  794. )
  795. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  796. '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1',
  797. )
  798. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  799. '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2',
  800. )
  801. flexmock(module.borgmatic.config.paths).should_receive(
  802. 'replace_temporary_subdirectory_with_glob',
  803. ).with_args(
  804. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  805. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  806. ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
  807. flexmock(module.borgmatic.config.paths).should_receive(
  808. 'replace_temporary_subdirectory_with_glob',
  809. ).with_args(
  810. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  811. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  812. ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
  813. flexmock(module.glob).should_receive('glob').with_args(
  814. '/mnt/subvol1/.borgmatic-*/mnt/subvol1',
  815. ).and_return(
  816. ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'),
  817. )
  818. flexmock(module.glob).should_receive('glob').with_args(
  819. '/mnt/subvol2/.borgmatic-*/mnt/subvol2',
  820. ).and_return(
  821. ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'),
  822. )
  823. flexmock(module.os.path).should_receive('isdir').with_args(
  824. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  825. ).and_return(True)
  826. flexmock(module.os.path).should_receive('isdir').with_args(
  827. '/mnt/subvol1/.borgmatic-5678/mnt/subvol1',
  828. ).and_return(True)
  829. flexmock(module.os.path).should_receive('isdir').with_args(
  830. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  831. ).and_return(True)
  832. flexmock(module.os.path).should_receive('isdir').with_args(
  833. '/mnt/subvol2/.borgmatic-5678/mnt/subvol2',
  834. ).and_return(False)
  835. flexmock(module).should_receive('delete_snapshot').never()
  836. flexmock(module.shutil).should_receive('rmtree').never()
  837. module.remove_data_source_dumps(
  838. hook_config=config['btrfs'],
  839. config=config,
  840. borgmatic_runtime_directory='/run/borgmatic',
  841. patterns=flexmock(),
  842. dry_run=True,
  843. )
  844. def test_remove_data_source_dumps_without_subvolumes_skips_deletes():
  845. config = {'btrfs': {}}
  846. flexmock(module).should_receive('get_subvolumes').and_return(())
  847. flexmock(module).should_receive('make_snapshot_path').never()
  848. flexmock(module.borgmatic.config.paths).should_receive(
  849. 'replace_temporary_subdirectory_with_glob',
  850. ).never()
  851. flexmock(module).should_receive('delete_snapshot').never()
  852. flexmock(module.shutil).should_receive('rmtree').never()
  853. module.remove_data_source_dumps(
  854. hook_config=config['btrfs'],
  855. config=config,
  856. borgmatic_runtime_directory='/run/borgmatic',
  857. patterns=flexmock(),
  858. dry_run=False,
  859. )
  860. def test_remove_data_source_without_snapshots_skips_deletes():
  861. config = {'btrfs': {}}
  862. flexmock(module).should_receive('get_subvolumes').and_return(
  863. (
  864. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  865. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  866. ),
  867. )
  868. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  869. '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1',
  870. )
  871. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  872. '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2',
  873. )
  874. flexmock(module.borgmatic.config.paths).should_receive(
  875. 'replace_temporary_subdirectory_with_glob',
  876. ).with_args(
  877. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  878. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  879. ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
  880. flexmock(module.borgmatic.config.paths).should_receive(
  881. 'replace_temporary_subdirectory_with_glob',
  882. ).with_args(
  883. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  884. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  885. ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
  886. flexmock(module.glob).should_receive('glob').and_return(())
  887. flexmock(module.os.path).should_receive('isdir').never()
  888. flexmock(module).should_receive('delete_snapshot').never()
  889. flexmock(module.shutil).should_receive('rmtree').never()
  890. module.remove_data_source_dumps(
  891. hook_config=config['btrfs'],
  892. config=config,
  893. borgmatic_runtime_directory='/run/borgmatic',
  894. patterns=flexmock(),
  895. dry_run=False,
  896. )
  897. def test_remove_data_source_dumps_with_delete_snapshot_file_not_found_error_bails():
  898. config = {'btrfs': {}}
  899. flexmock(module).should_receive('get_subvolumes').and_return(
  900. (
  901. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  902. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  903. ),
  904. )
  905. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  906. '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1',
  907. )
  908. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  909. '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2',
  910. )
  911. flexmock(module.borgmatic.config.paths).should_receive(
  912. 'replace_temporary_subdirectory_with_glob',
  913. ).with_args(
  914. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  915. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  916. ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
  917. flexmock(module.borgmatic.config.paths).should_receive(
  918. 'replace_temporary_subdirectory_with_glob',
  919. ).with_args(
  920. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  921. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  922. ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
  923. flexmock(module.glob).should_receive('glob').with_args(
  924. '/mnt/subvol1/.borgmatic-*/mnt/subvol1',
  925. ).and_return(
  926. ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'),
  927. )
  928. flexmock(module.glob).should_receive('glob').with_args(
  929. '/mnt/subvol2/.borgmatic-*/mnt/subvol2',
  930. ).and_return(
  931. ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'),
  932. )
  933. flexmock(module.os.path).should_receive('isdir').with_args(
  934. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  935. ).and_return(True)
  936. flexmock(module.os.path).should_receive('isdir').with_args(
  937. '/mnt/subvol1/.borgmatic-5678/mnt/subvol1',
  938. ).and_return(True)
  939. flexmock(module.os.path).should_receive('isdir').with_args(
  940. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  941. ).and_return(True)
  942. flexmock(module.os.path).should_receive('isdir').with_args(
  943. '/mnt/subvol2/.borgmatic-5678/mnt/subvol2',
  944. ).and_return(False)
  945. flexmock(module).should_receive('delete_snapshot').and_raise(FileNotFoundError)
  946. flexmock(module.shutil).should_receive('rmtree').never()
  947. module.remove_data_source_dumps(
  948. hook_config=config['btrfs'],
  949. config=config,
  950. borgmatic_runtime_directory='/run/borgmatic',
  951. patterns=flexmock(),
  952. dry_run=False,
  953. )
  954. def test_remove_data_source_dumps_with_delete_snapshot_called_process_error_bails():
  955. config = {'btrfs': {}}
  956. flexmock(module).should_receive('get_subvolumes').and_return(
  957. (
  958. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  959. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  960. ),
  961. )
  962. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  963. '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1',
  964. )
  965. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  966. '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2',
  967. )
  968. flexmock(module.borgmatic.config.paths).should_receive(
  969. 'replace_temporary_subdirectory_with_glob',
  970. ).with_args(
  971. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  972. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  973. ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
  974. flexmock(module.borgmatic.config.paths).should_receive(
  975. 'replace_temporary_subdirectory_with_glob',
  976. ).with_args(
  977. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  978. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  979. ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
  980. flexmock(module.glob).should_receive('glob').with_args(
  981. '/mnt/subvol1/.borgmatic-*/mnt/subvol1',
  982. ).and_return(
  983. ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'),
  984. )
  985. flexmock(module.glob).should_receive('glob').with_args(
  986. '/mnt/subvol2/.borgmatic-*/mnt/subvol2',
  987. ).and_return(
  988. ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'),
  989. )
  990. flexmock(module.os.path).should_receive('isdir').with_args(
  991. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  992. ).and_return(True)
  993. flexmock(module.os.path).should_receive('isdir').with_args(
  994. '/mnt/subvol1/.borgmatic-5678/mnt/subvol1',
  995. ).and_return(True)
  996. flexmock(module.os.path).should_receive('isdir').with_args(
  997. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  998. ).and_return(True)
  999. flexmock(module.os.path).should_receive('isdir').with_args(
  1000. '/mnt/subvol2/.borgmatic-5678/mnt/subvol2',
  1001. ).and_return(False)
  1002. flexmock(module).should_receive('delete_snapshot').and_raise(
  1003. module.subprocess.CalledProcessError(1, 'command', 'error'),
  1004. )
  1005. flexmock(module.shutil).should_receive('rmtree').never()
  1006. module.remove_data_source_dumps(
  1007. hook_config=config['btrfs'],
  1008. config=config,
  1009. borgmatic_runtime_directory='/run/borgmatic',
  1010. patterns=flexmock(),
  1011. dry_run=False,
  1012. )
  1013. def test_remove_data_source_dumps_with_root_subvolume_skips_duplicate_removal():
  1014. config = {'btrfs': {}}
  1015. flexmock(module).should_receive('get_subvolumes').and_return(
  1016. (module.Subvolume('/', contained_patterns=(Pattern('/etc'),)),),
  1017. )
  1018. flexmock(module).should_receive('make_snapshot_path').with_args('/').and_return(
  1019. '/.borgmatic-1234',
  1020. )
  1021. flexmock(module.borgmatic.config.paths).should_receive(
  1022. 'replace_temporary_subdirectory_with_glob',
  1023. ).with_args(
  1024. '/.borgmatic-1234',
  1025. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  1026. ).and_return('/.borgmatic-*')
  1027. flexmock(module.glob).should_receive('glob').with_args('/.borgmatic-*').and_return(
  1028. ('/.borgmatic-1234', '/.borgmatic-5678'),
  1029. )
  1030. flexmock(module.os.path).should_receive('isdir').with_args('/.borgmatic-1234').and_return(
  1031. True,
  1032. ).and_return(False)
  1033. flexmock(module.os.path).should_receive('isdir').with_args('/.borgmatic-5678').and_return(
  1034. True,
  1035. ).and_return(False)
  1036. flexmock(module).should_receive('delete_snapshot').with_args('btrfs', '/.borgmatic-1234').once()
  1037. flexmock(module).should_receive('delete_snapshot').with_args('btrfs', '/.borgmatic-5678').once()
  1038. flexmock(module.os.path).should_receive('isdir').with_args('').and_return(False)
  1039. flexmock(module.shutil).should_receive('rmtree').never()
  1040. module.remove_data_source_dumps(
  1041. hook_config=config['btrfs'],
  1042. config=config,
  1043. borgmatic_runtime_directory='/run/borgmatic',
  1044. patterns=flexmock(),
  1045. dry_run=False,
  1046. )