test_btrfs.py 36 KB

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