test_btrfs.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916
  1. import pytest
  2. from flexmock import flexmock
  3. from borgmatic.borg.pattern import Pattern, Pattern_style, Pattern_type
  4. from borgmatic.hooks.data_source import btrfs as module
  5. def test_get_filesystem_mount_points_parses_findmnt_output():
  6. flexmock(module.borgmatic.execute).should_receive(
  7. 'execute_command_and_capture_output'
  8. ).and_return(
  9. '''{
  10. "filesystems": [
  11. {
  12. "target": "/mnt0",
  13. "source": "/dev/loop0",
  14. "fstype": "btrfs",
  15. "options": "rw,relatime,ssd,space_cache=v2,subvolid=5,subvol=/"
  16. },
  17. {
  18. "target": "/mnt1",
  19. "source": "/dev/loop0",
  20. "fstype": "btrfs",
  21. "options": "rw,relatime,ssd,space_cache=v2,subvolid=5,subvol=/"
  22. }
  23. ]
  24. }
  25. '''
  26. )
  27. assert module.get_filesystem_mount_points('findmnt') == ('/mnt0', '/mnt1')
  28. def test_get_filesystem_mount_points_with_invalid_findmnt_json_errors():
  29. flexmock(module.borgmatic.execute).should_receive(
  30. 'execute_command_and_capture_output'
  31. ).and_return('{')
  32. with pytest.raises(ValueError):
  33. module.get_filesystem_mount_points('findmnt')
  34. def test_get_filesystem_mount_points_with_findmnt_json_missing_filesystems_errors():
  35. flexmock(module.borgmatic.execute).should_receive(
  36. 'execute_command_and_capture_output'
  37. ).and_return('{"wtf": "something is wrong here"}')
  38. with pytest.raises(ValueError):
  39. module.get_filesystem_mount_points('findmnt')
  40. def test_get_subvolumes_for_filesystem_parses_subvolume_list_output():
  41. flexmock(module.borgmatic.execute).should_receive(
  42. 'execute_command_and_capture_output'
  43. ).and_return(
  44. 'ID 270 gen 107 top level 5 path subvol1\nID 272 gen 74 top level 5 path subvol2\n'
  45. )
  46. assert module.get_subvolumes_for_filesystem('btrfs', '/mnt') == (
  47. '/mnt',
  48. '/mnt/subvol1',
  49. '/mnt/subvol2',
  50. )
  51. def test_get_subvolumes_for_filesystem_skips_empty_subvolume_paths():
  52. flexmock(module.borgmatic.execute).should_receive(
  53. 'execute_command_and_capture_output'
  54. ).and_return('\n \nID 272 gen 74 top level 5 path subvol2\n')
  55. assert module.get_subvolumes_for_filesystem('btrfs', '/mnt') == ('/mnt', '/mnt/subvol2')
  56. def test_get_subvolumes_for_filesystem_skips_empty_filesystem_mount_points():
  57. flexmock(module.borgmatic.execute).should_receive(
  58. 'execute_command_and_capture_output'
  59. ).and_return(
  60. 'ID 270 gen 107 top level 5 path subvol1\nID 272 gen 74 top level 5 path subvol2\n'
  61. )
  62. assert module.get_subvolumes_for_filesystem('btrfs', ' ') == ()
  63. def test_get_subvolumes_collects_subvolumes_matching_patterns_from_all_filesystems():
  64. flexmock(module).should_receive('get_filesystem_mount_points').and_return(('/mnt1', '/mnt2'))
  65. flexmock(module).should_receive('get_subvolumes_for_filesystem').with_args(
  66. 'btrfs', '/mnt1'
  67. ).and_return(('/one', '/two'))
  68. flexmock(module).should_receive('get_subvolumes_for_filesystem').with_args(
  69. 'btrfs', '/mnt2'
  70. ).and_return(('/three', '/four'))
  71. for path in ('/one', '/four'):
  72. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  73. 'get_contained_patterns'
  74. ).with_args(path, object).and_return((Pattern(path),))
  75. for path in ('/two', '/three'):
  76. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  77. 'get_contained_patterns'
  78. ).with_args(path, object).and_return(())
  79. assert module.get_subvolumes(
  80. 'btrfs',
  81. 'findmnt',
  82. patterns=[
  83. Pattern('/one'),
  84. Pattern('/four'),
  85. Pattern('/five'),
  86. Pattern('/six'),
  87. Pattern('/mnt2'),
  88. Pattern('/mnt3'),
  89. ],
  90. ) == (
  91. module.Subvolume('/four', contained_patterns=(Pattern('/four'),)),
  92. module.Subvolume('/one', contained_patterns=(Pattern('/one'),)),
  93. )
  94. def test_get_subvolumes_without_patterns_collects_all_subvolumes_from_all_filesystems():
  95. flexmock(module).should_receive('get_filesystem_mount_points').and_return(('/mnt1', '/mnt2'))
  96. flexmock(module).should_receive('get_subvolumes_for_filesystem').with_args(
  97. 'btrfs', '/mnt1'
  98. ).and_return(('/one', '/two'))
  99. flexmock(module).should_receive('get_subvolumes_for_filesystem').with_args(
  100. 'btrfs', '/mnt2'
  101. ).and_return(('/three', '/four'))
  102. for path in ('/one', '/two', '/three', '/four'):
  103. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  104. 'get_contained_patterns'
  105. ).with_args(path, object).and_return((Pattern(path),))
  106. assert module.get_subvolumes('btrfs', 'findmnt') == (
  107. module.Subvolume('/four', contained_patterns=(Pattern('/four'),)),
  108. module.Subvolume('/one', contained_patterns=(Pattern('/one'),)),
  109. module.Subvolume('/three', contained_patterns=(Pattern('/three'),)),
  110. module.Subvolume('/two', contained_patterns=(Pattern('/two'),)),
  111. )
  112. @pytest.mark.parametrize(
  113. 'subvolume_path,expected_snapshot_path',
  114. (
  115. ('/foo/bar', '/foo/bar/.borgmatic-snapshot-1234/foo/bar'),
  116. ('/', '/.borgmatic-snapshot-1234'),
  117. ),
  118. )
  119. def test_make_snapshot_path_includes_stripped_subvolume_path(
  120. subvolume_path, expected_snapshot_path
  121. ):
  122. flexmock(module.os).should_receive('getpid').and_return(1234)
  123. assert module.make_snapshot_path(subvolume_path) == expected_snapshot_path
  124. @pytest.mark.parametrize(
  125. 'subvolume_path,pattern,expected_pattern',
  126. (
  127. (
  128. '/foo/bar',
  129. Pattern('/foo/bar/baz'),
  130. Pattern('/foo/bar/.borgmatic-snapshot-1234/./foo/bar/baz'),
  131. ),
  132. ('/foo/bar', Pattern('/foo/bar'), Pattern('/foo/bar/.borgmatic-snapshot-1234/./foo/bar')),
  133. (
  134. '/foo/bar',
  135. Pattern('^/foo/bar', Pattern_type.INCLUDE, Pattern_style.REGULAR_EXPRESSION),
  136. Pattern(
  137. '^/foo/bar/.borgmatic-snapshot-1234/./foo/bar',
  138. Pattern_type.INCLUDE,
  139. Pattern_style.REGULAR_EXPRESSION,
  140. ),
  141. ),
  142. (
  143. '/foo/bar',
  144. Pattern('/foo/bar', Pattern_type.INCLUDE, Pattern_style.REGULAR_EXPRESSION),
  145. Pattern(
  146. '/foo/bar/.borgmatic-snapshot-1234/./foo/bar',
  147. Pattern_type.INCLUDE,
  148. Pattern_style.REGULAR_EXPRESSION,
  149. ),
  150. ),
  151. ('/', Pattern('/foo'), Pattern('/.borgmatic-snapshot-1234/./foo')),
  152. ('/', Pattern('/'), Pattern('/.borgmatic-snapshot-1234/./')),
  153. ),
  154. )
  155. def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_path(
  156. subvolume_path, pattern, expected_pattern
  157. ):
  158. flexmock(module.os).should_receive('getpid').and_return(1234)
  159. assert module.make_borg_snapshot_pattern(subvolume_path, pattern) == expected_pattern
  160. def test_dump_data_sources_snapshots_each_subvolume_and_updates_patterns():
  161. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  162. config = {'btrfs': {}}
  163. flexmock(module).should_receive('get_subvolumes').and_return(
  164. (
  165. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  166. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  167. )
  168. )
  169. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  170. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  171. )
  172. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  173. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  174. )
  175. flexmock(module).should_receive('snapshot_subvolume').with_args(
  176. 'btrfs', '/mnt/subvol1', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  177. ).once()
  178. flexmock(module).should_receive('snapshot_subvolume').with_args(
  179. 'btrfs', '/mnt/subvol2', '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  180. ).once()
  181. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  182. '/mnt/subvol1'
  183. ).and_return(
  184. Pattern(
  185. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  186. Pattern_type.NO_RECURSE,
  187. Pattern_style.FNMATCH,
  188. )
  189. )
  190. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  191. '/mnt/subvol2'
  192. ).and_return(
  193. Pattern(
  194. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
  195. Pattern_type.NO_RECURSE,
  196. Pattern_style.FNMATCH,
  197. )
  198. )
  199. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  200. '/mnt/subvol1', object
  201. ).and_return(Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'))
  202. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  203. '/mnt/subvol2', object
  204. ).and_return(Pattern('/mnt/subvol2/.borgmatic-1234/mnt/subvol2'))
  205. assert (
  206. module.dump_data_sources(
  207. hook_config=config['btrfs'],
  208. config=config,
  209. log_prefix='test',
  210. config_paths=('test.yaml',),
  211. borgmatic_runtime_directory='/run/borgmatic',
  212. patterns=patterns,
  213. dry_run=False,
  214. )
  215. == []
  216. )
  217. assert patterns == [
  218. Pattern('/foo'),
  219. Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'),
  220. Pattern(
  221. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  222. Pattern_type.NO_RECURSE,
  223. Pattern_style.FNMATCH,
  224. ),
  225. Pattern('/mnt/subvol2/.borgmatic-1234/mnt/subvol2'),
  226. Pattern(
  227. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
  228. Pattern_type.NO_RECURSE,
  229. Pattern_style.FNMATCH,
  230. ),
  231. ]
  232. assert config == {
  233. 'btrfs': {},
  234. }
  235. def test_dump_data_sources_uses_custom_btrfs_command_in_commands():
  236. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  237. config = {'btrfs': {'btrfs_command': '/usr/local/bin/btrfs'}}
  238. flexmock(module).should_receive('get_subvolumes').and_return(
  239. (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),)
  240. )
  241. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  242. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  243. )
  244. flexmock(module).should_receive('snapshot_subvolume').with_args(
  245. '/usr/local/bin/btrfs', '/mnt/subvol1', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  246. ).once()
  247. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  248. '/mnt/subvol1'
  249. ).and_return(
  250. Pattern(
  251. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  252. Pattern_type.NO_RECURSE,
  253. Pattern_style.FNMATCH,
  254. )
  255. )
  256. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  257. '/mnt/subvol1', object
  258. ).and_return(Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'))
  259. assert (
  260. module.dump_data_sources(
  261. hook_config=config['btrfs'],
  262. config=config,
  263. log_prefix='test',
  264. config_paths=('test.yaml',),
  265. borgmatic_runtime_directory='/run/borgmatic',
  266. patterns=patterns,
  267. dry_run=False,
  268. )
  269. == []
  270. )
  271. assert patterns == [
  272. Pattern('/foo'),
  273. Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'),
  274. Pattern(
  275. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  276. Pattern_type.NO_RECURSE,
  277. Pattern_style.FNMATCH,
  278. ),
  279. ]
  280. assert config == {
  281. 'btrfs': {
  282. 'btrfs_command': '/usr/local/bin/btrfs',
  283. },
  284. }
  285. def test_dump_data_sources_uses_custom_findmnt_command_in_commands():
  286. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  287. config = {'btrfs': {'findmnt_command': '/usr/local/bin/findmnt'}}
  288. flexmock(module).should_receive('get_subvolumes').with_args(
  289. 'btrfs', '/usr/local/bin/findmnt', patterns
  290. ).and_return(
  291. (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),)
  292. ).once()
  293. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  294. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  295. )
  296. flexmock(module).should_receive('snapshot_subvolume').with_args(
  297. 'btrfs', '/mnt/subvol1', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  298. ).once()
  299. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  300. '/mnt/subvol1'
  301. ).and_return(
  302. Pattern(
  303. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  304. Pattern_type.NO_RECURSE,
  305. Pattern_style.FNMATCH,
  306. )
  307. )
  308. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  309. '/mnt/subvol1', object
  310. ).and_return(Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'))
  311. assert (
  312. module.dump_data_sources(
  313. hook_config=config['btrfs'],
  314. config=config,
  315. log_prefix='test',
  316. config_paths=('test.yaml',),
  317. borgmatic_runtime_directory='/run/borgmatic',
  318. patterns=patterns,
  319. dry_run=False,
  320. )
  321. == []
  322. )
  323. assert patterns == [
  324. Pattern('/foo'),
  325. Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'),
  326. Pattern(
  327. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  328. Pattern_type.NO_RECURSE,
  329. Pattern_style.FNMATCH,
  330. ),
  331. ]
  332. assert config == {
  333. 'btrfs': {
  334. 'findmnt_command': '/usr/local/bin/findmnt',
  335. },
  336. }
  337. def test_dump_data_sources_with_dry_run_skips_snapshot_and_patterns_update():
  338. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  339. config = {'btrfs': {}}
  340. flexmock(module).should_receive('get_subvolumes').and_return(
  341. (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),)
  342. )
  343. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  344. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  345. )
  346. flexmock(module).should_receive('snapshot_subvolume').never()
  347. flexmock(module).should_receive('make_snapshot_exclude_pattern').never()
  348. assert (
  349. module.dump_data_sources(
  350. hook_config=config['btrfs'],
  351. config=config,
  352. log_prefix='test',
  353. config_paths=('test.yaml',),
  354. borgmatic_runtime_directory='/run/borgmatic',
  355. patterns=patterns,
  356. dry_run=True,
  357. )
  358. == []
  359. )
  360. assert patterns == [Pattern('/foo'), Pattern('/mnt/subvol1')]
  361. assert config == {'btrfs': {}}
  362. def test_dump_data_sources_without_matching_subvolumes_skips_snapshot_and_patterns_update():
  363. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  364. config = {'btrfs': {}}
  365. flexmock(module).should_receive('get_subvolumes').and_return(())
  366. flexmock(module).should_receive('make_snapshot_path').never()
  367. flexmock(module).should_receive('snapshot_subvolume').never()
  368. flexmock(module).should_receive('make_snapshot_exclude_pattern').never()
  369. assert (
  370. module.dump_data_sources(
  371. hook_config=config['btrfs'],
  372. config=config,
  373. log_prefix='test',
  374. config_paths=('test.yaml',),
  375. borgmatic_runtime_directory='/run/borgmatic',
  376. patterns=patterns,
  377. dry_run=False,
  378. )
  379. == []
  380. )
  381. assert patterns == [Pattern('/foo'), Pattern('/mnt/subvol1')]
  382. assert config == {'btrfs': {}}
  383. def test_dump_data_sources_snapshots_adds_to_existing_exclude_patterns():
  384. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  385. config = {'btrfs': {}, 'exclude_patterns': ['/bar']}
  386. flexmock(module).should_receive('get_subvolumes').and_return(
  387. (
  388. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  389. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  390. )
  391. )
  392. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  393. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  394. )
  395. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  396. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  397. )
  398. flexmock(module).should_receive('snapshot_subvolume').with_args(
  399. 'btrfs', '/mnt/subvol1', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  400. ).once()
  401. flexmock(module).should_receive('snapshot_subvolume').with_args(
  402. 'btrfs', '/mnt/subvol2', '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  403. ).once()
  404. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  405. '/mnt/subvol1'
  406. ).and_return(
  407. Pattern(
  408. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  409. Pattern_type.NO_RECURSE,
  410. Pattern_style.FNMATCH,
  411. )
  412. )
  413. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  414. '/mnt/subvol2'
  415. ).and_return(
  416. Pattern(
  417. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
  418. Pattern_type.NO_RECURSE,
  419. Pattern_style.FNMATCH,
  420. )
  421. )
  422. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  423. '/mnt/subvol1', object
  424. ).and_return(Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'))
  425. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  426. '/mnt/subvol2', object
  427. ).and_return(Pattern('/mnt/subvol2/.borgmatic-1234/mnt/subvol2'))
  428. assert (
  429. module.dump_data_sources(
  430. hook_config=config['btrfs'],
  431. config=config,
  432. log_prefix='test',
  433. config_paths=('test.yaml',),
  434. borgmatic_runtime_directory='/run/borgmatic',
  435. patterns=patterns,
  436. dry_run=False,
  437. )
  438. == []
  439. )
  440. assert patterns == [
  441. Pattern('/foo'),
  442. Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'),
  443. Pattern(
  444. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  445. Pattern_type.NO_RECURSE,
  446. Pattern_style.FNMATCH,
  447. ),
  448. Pattern('/mnt/subvol2/.borgmatic-1234/mnt/subvol2'),
  449. Pattern(
  450. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
  451. Pattern_type.NO_RECURSE,
  452. Pattern_style.FNMATCH,
  453. ),
  454. ]
  455. assert config == {
  456. 'btrfs': {},
  457. 'exclude_patterns': ['/bar'],
  458. }
  459. def test_remove_data_source_dumps_deletes_snapshots():
  460. config = {'btrfs': {}}
  461. flexmock(module).should_receive('get_subvolumes').and_return(
  462. (
  463. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  464. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  465. )
  466. )
  467. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  468. '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1'
  469. )
  470. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  471. '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2'
  472. )
  473. flexmock(module.borgmatic.config.paths).should_receive(
  474. 'replace_temporary_subdirectory_with_glob'
  475. ).with_args(
  476. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  477. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  478. ).and_return(
  479. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  480. )
  481. flexmock(module.borgmatic.config.paths).should_receive(
  482. 'replace_temporary_subdirectory_with_glob'
  483. ).with_args(
  484. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  485. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  486. ).and_return(
  487. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  488. )
  489. flexmock(module.glob).should_receive('glob').with_args(
  490. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  491. ).and_return(
  492. ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1')
  493. )
  494. flexmock(module.glob).should_receive('glob').with_args(
  495. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  496. ).and_return(
  497. ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2')
  498. )
  499. flexmock(module.os.path).should_receive('isdir').with_args(
  500. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  501. ).and_return(True)
  502. flexmock(module.os.path).should_receive('isdir').with_args(
  503. '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'
  504. ).and_return(True)
  505. flexmock(module.os.path).should_receive('isdir').with_args(
  506. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  507. ).and_return(True)
  508. flexmock(module.os.path).should_receive('isdir').with_args(
  509. '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'
  510. ).and_return(False)
  511. flexmock(module).should_receive('delete_snapshot').with_args(
  512. 'btrfs', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  513. ).once()
  514. flexmock(module).should_receive('delete_snapshot').with_args(
  515. 'btrfs', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'
  516. ).once()
  517. flexmock(module).should_receive('delete_snapshot').with_args(
  518. 'btrfs', '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  519. ).once()
  520. flexmock(module).should_receive('delete_snapshot').with_args(
  521. 'btrfs', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'
  522. ).never()
  523. flexmock(module.shutil).should_receive('rmtree').with_args(
  524. '/mnt/subvol1/.borgmatic-1234'
  525. ).once()
  526. flexmock(module.shutil).should_receive('rmtree').with_args(
  527. '/mnt/subvol1/.borgmatic-5678'
  528. ).once()
  529. flexmock(module.shutil).should_receive('rmtree').with_args(
  530. '/mnt/subvol2/.borgmatic-1234'
  531. ).once()
  532. flexmock(module.shutil).should_receive('rmtree').with_args(
  533. '/mnt/subvol2/.borgmatic-5678'
  534. ).never()
  535. module.remove_data_source_dumps(
  536. hook_config=config['btrfs'],
  537. config=config,
  538. log_prefix='test',
  539. borgmatic_runtime_directory='/run/borgmatic',
  540. dry_run=False,
  541. )
  542. def test_remove_data_source_dumps_without_hook_configuration_bails():
  543. flexmock(module).should_receive('get_subvolumes').never()
  544. flexmock(module).should_receive('make_snapshot_path').never()
  545. flexmock(module.borgmatic.config.paths).should_receive(
  546. 'replace_temporary_subdirectory_with_glob'
  547. ).never()
  548. flexmock(module).should_receive('delete_snapshot').never()
  549. flexmock(module.shutil).should_receive('rmtree').never()
  550. module.remove_data_source_dumps(
  551. hook_config=None,
  552. config={'source_directories': '/mnt/subvolume'},
  553. log_prefix='test',
  554. borgmatic_runtime_directory='/run/borgmatic',
  555. dry_run=False,
  556. )
  557. def test_remove_data_source_dumps_with_get_subvolumes_file_not_found_error_bails():
  558. config = {'btrfs': {}}
  559. flexmock(module).should_receive('get_subvolumes').and_raise(FileNotFoundError)
  560. flexmock(module).should_receive('make_snapshot_path').never()
  561. flexmock(module.borgmatic.config.paths).should_receive(
  562. 'replace_temporary_subdirectory_with_glob'
  563. ).never()
  564. flexmock(module).should_receive('delete_snapshot').never()
  565. flexmock(module.shutil).should_receive('rmtree').never()
  566. module.remove_data_source_dumps(
  567. hook_config=config['btrfs'],
  568. config=config,
  569. log_prefix='test',
  570. borgmatic_runtime_directory='/run/borgmatic',
  571. dry_run=False,
  572. )
  573. def test_remove_data_source_dumps_with_get_subvolumes_called_process_error_bails():
  574. config = {'btrfs': {}}
  575. flexmock(module).should_receive('get_subvolumes').and_raise(
  576. module.subprocess.CalledProcessError(1, 'command', 'error')
  577. )
  578. flexmock(module).should_receive('make_snapshot_path').never()
  579. flexmock(module.borgmatic.config.paths).should_receive(
  580. 'replace_temporary_subdirectory_with_glob'
  581. ).never()
  582. flexmock(module).should_receive('delete_snapshot').never()
  583. flexmock(module.shutil).should_receive('rmtree').never()
  584. module.remove_data_source_dumps(
  585. hook_config=config['btrfs'],
  586. config=config,
  587. log_prefix='test',
  588. borgmatic_runtime_directory='/run/borgmatic',
  589. dry_run=False,
  590. )
  591. def test_remove_data_source_dumps_with_dry_run_skips_deletes():
  592. config = {'btrfs': {}}
  593. flexmock(module).should_receive('get_subvolumes').and_return(
  594. (
  595. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  596. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  597. )
  598. )
  599. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  600. '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1'
  601. )
  602. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  603. '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2'
  604. )
  605. flexmock(module.borgmatic.config.paths).should_receive(
  606. 'replace_temporary_subdirectory_with_glob'
  607. ).with_args(
  608. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  609. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  610. ).and_return(
  611. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  612. )
  613. flexmock(module.borgmatic.config.paths).should_receive(
  614. 'replace_temporary_subdirectory_with_glob'
  615. ).with_args(
  616. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  617. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  618. ).and_return(
  619. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  620. )
  621. flexmock(module.glob).should_receive('glob').with_args(
  622. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  623. ).and_return(
  624. ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1')
  625. )
  626. flexmock(module.glob).should_receive('glob').with_args(
  627. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  628. ).and_return(
  629. ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2')
  630. )
  631. flexmock(module.os.path).should_receive('isdir').with_args(
  632. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  633. ).and_return(True)
  634. flexmock(module.os.path).should_receive('isdir').with_args(
  635. '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'
  636. ).and_return(True)
  637. flexmock(module.os.path).should_receive('isdir').with_args(
  638. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  639. ).and_return(True)
  640. flexmock(module.os.path).should_receive('isdir').with_args(
  641. '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'
  642. ).and_return(False)
  643. flexmock(module).should_receive('delete_snapshot').never()
  644. flexmock(module.shutil).should_receive('rmtree').never()
  645. module.remove_data_source_dumps(
  646. hook_config=config['btrfs'],
  647. config=config,
  648. log_prefix='test',
  649. borgmatic_runtime_directory='/run/borgmatic',
  650. dry_run=True,
  651. )
  652. def test_remove_data_source_dumps_without_subvolumes_skips_deletes():
  653. config = {'btrfs': {}}
  654. flexmock(module).should_receive('get_subvolumes').and_return(())
  655. flexmock(module).should_receive('make_snapshot_path').never()
  656. flexmock(module.borgmatic.config.paths).should_receive(
  657. 'replace_temporary_subdirectory_with_glob'
  658. ).never()
  659. flexmock(module).should_receive('delete_snapshot').never()
  660. flexmock(module.shutil).should_receive('rmtree').never()
  661. module.remove_data_source_dumps(
  662. hook_config=config['btrfs'],
  663. config=config,
  664. log_prefix='test',
  665. borgmatic_runtime_directory='/run/borgmatic',
  666. dry_run=False,
  667. )
  668. def test_remove_data_source_without_snapshots_skips_deletes():
  669. config = {'btrfs': {}}
  670. flexmock(module).should_receive('get_subvolumes').and_return(
  671. (
  672. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  673. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  674. )
  675. )
  676. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  677. '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1'
  678. )
  679. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  680. '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2'
  681. )
  682. flexmock(module.borgmatic.config.paths).should_receive(
  683. 'replace_temporary_subdirectory_with_glob'
  684. ).with_args(
  685. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  686. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  687. ).and_return(
  688. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  689. )
  690. flexmock(module.borgmatic.config.paths).should_receive(
  691. 'replace_temporary_subdirectory_with_glob'
  692. ).with_args(
  693. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  694. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  695. ).and_return(
  696. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  697. )
  698. flexmock(module.glob).should_receive('glob').and_return(())
  699. flexmock(module.os.path).should_receive('isdir').never()
  700. flexmock(module).should_receive('delete_snapshot').never()
  701. flexmock(module.shutil).should_receive('rmtree').never()
  702. module.remove_data_source_dumps(
  703. hook_config=config['btrfs'],
  704. config=config,
  705. log_prefix='test',
  706. borgmatic_runtime_directory='/run/borgmatic',
  707. dry_run=False,
  708. )
  709. def test_remove_data_source_dumps_with_delete_snapshot_file_not_found_error_bails():
  710. config = {'btrfs': {}}
  711. flexmock(module).should_receive('get_subvolumes').and_return(
  712. (
  713. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  714. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  715. )
  716. )
  717. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  718. '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1'
  719. )
  720. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  721. '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2'
  722. )
  723. flexmock(module.borgmatic.config.paths).should_receive(
  724. 'replace_temporary_subdirectory_with_glob'
  725. ).with_args(
  726. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  727. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  728. ).and_return(
  729. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  730. )
  731. flexmock(module.borgmatic.config.paths).should_receive(
  732. 'replace_temporary_subdirectory_with_glob'
  733. ).with_args(
  734. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  735. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  736. ).and_return(
  737. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  738. )
  739. flexmock(module.glob).should_receive('glob').with_args(
  740. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  741. ).and_return(
  742. ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1')
  743. )
  744. flexmock(module.glob).should_receive('glob').with_args(
  745. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  746. ).and_return(
  747. ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2')
  748. )
  749. flexmock(module.os.path).should_receive('isdir').with_args(
  750. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  751. ).and_return(True)
  752. flexmock(module.os.path).should_receive('isdir').with_args(
  753. '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'
  754. ).and_return(True)
  755. flexmock(module.os.path).should_receive('isdir').with_args(
  756. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  757. ).and_return(True)
  758. flexmock(module.os.path).should_receive('isdir').with_args(
  759. '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'
  760. ).and_return(False)
  761. flexmock(module).should_receive('delete_snapshot').and_raise(FileNotFoundError)
  762. flexmock(module.shutil).should_receive('rmtree').never()
  763. module.remove_data_source_dumps(
  764. hook_config=config['btrfs'],
  765. config=config,
  766. log_prefix='test',
  767. borgmatic_runtime_directory='/run/borgmatic',
  768. dry_run=False,
  769. )
  770. def test_remove_data_source_dumps_with_delete_snapshot_called_process_error_bails():
  771. config = {'btrfs': {}}
  772. flexmock(module).should_receive('get_subvolumes').and_return(
  773. (
  774. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  775. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  776. )
  777. )
  778. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  779. '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1'
  780. )
  781. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  782. '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2'
  783. )
  784. flexmock(module.borgmatic.config.paths).should_receive(
  785. 'replace_temporary_subdirectory_with_glob'
  786. ).with_args(
  787. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  788. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  789. ).and_return(
  790. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  791. )
  792. flexmock(module.borgmatic.config.paths).should_receive(
  793. 'replace_temporary_subdirectory_with_glob'
  794. ).with_args(
  795. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  796. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  797. ).and_return(
  798. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  799. )
  800. flexmock(module.glob).should_receive('glob').with_args(
  801. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  802. ).and_return(
  803. ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1')
  804. )
  805. flexmock(module.glob).should_receive('glob').with_args(
  806. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  807. ).and_return(
  808. ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2')
  809. )
  810. flexmock(module.os.path).should_receive('isdir').with_args(
  811. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  812. ).and_return(True)
  813. flexmock(module.os.path).should_receive('isdir').with_args(
  814. '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'
  815. ).and_return(True)
  816. flexmock(module.os.path).should_receive('isdir').with_args(
  817. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  818. ).and_return(True)
  819. flexmock(module.os.path).should_receive('isdir').with_args(
  820. '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'
  821. ).and_return(False)
  822. flexmock(module).should_receive('delete_snapshot').and_raise(
  823. module.subprocess.CalledProcessError(1, 'command', 'error')
  824. )
  825. flexmock(module.shutil).should_receive('rmtree').never()
  826. module.remove_data_source_dumps(
  827. hook_config=config['btrfs'],
  828. config=config,
  829. log_prefix='test',
  830. borgmatic_runtime_directory='/run/borgmatic',
  831. dry_run=False,
  832. )