test_btrfs.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990
  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. flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
  177. flexmock()
  178. )
  179. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  180. config = {'btrfs': {}}
  181. flexmock(module).should_receive('get_subvolumes').and_return(
  182. (
  183. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  184. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  185. )
  186. )
  187. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  188. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  189. )
  190. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  191. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  192. )
  193. flexmock(module).should_receive('snapshot_subvolume').with_args(
  194. 'btrfs', '/mnt/subvol1', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  195. ).once()
  196. flexmock(module).should_receive('snapshot_subvolume').with_args(
  197. 'btrfs', '/mnt/subvol2', '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  198. ).once()
  199. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  200. '/mnt/subvol1'
  201. ).and_return(
  202. Pattern(
  203. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  204. Pattern_type.NO_RECURSE,
  205. Pattern_style.FNMATCH,
  206. )
  207. )
  208. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  209. '/mnt/subvol2'
  210. ).and_return(
  211. Pattern(
  212. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
  213. Pattern_type.NO_RECURSE,
  214. Pattern_style.FNMATCH,
  215. )
  216. )
  217. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  218. '/mnt/subvol1', object
  219. ).and_return(Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'))
  220. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  221. '/mnt/subvol2', object
  222. ).and_return(Pattern('/mnt/subvol2/.borgmatic-1234/mnt/subvol2'))
  223. assert (
  224. module.dump_data_sources(
  225. hook_config=config['btrfs'],
  226. config=config,
  227. config_paths=('test.yaml',),
  228. borgmatic_runtime_directory='/run/borgmatic',
  229. patterns=patterns,
  230. dry_run=False,
  231. )
  232. == []
  233. )
  234. assert patterns == [
  235. Pattern('/foo'),
  236. Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'),
  237. Pattern(
  238. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  239. Pattern_type.NO_RECURSE,
  240. Pattern_style.FNMATCH,
  241. ),
  242. Pattern('/mnt/subvol2/.borgmatic-1234/mnt/subvol2'),
  243. Pattern(
  244. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
  245. Pattern_type.NO_RECURSE,
  246. Pattern_style.FNMATCH,
  247. ),
  248. ]
  249. assert config == {
  250. 'btrfs': {},
  251. }
  252. def test_dump_data_sources_uses_custom_btrfs_command_in_commands():
  253. flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
  254. flexmock()
  255. )
  256. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  257. config = {'btrfs': {'btrfs_command': '/usr/local/bin/btrfs'}}
  258. flexmock(module).should_receive('get_subvolumes').and_return(
  259. (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),)
  260. )
  261. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  262. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  263. )
  264. flexmock(module).should_receive('snapshot_subvolume').with_args(
  265. '/usr/local/bin/btrfs', '/mnt/subvol1', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  266. ).once()
  267. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  268. '/mnt/subvol1'
  269. ).and_return(
  270. Pattern(
  271. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  272. Pattern_type.NO_RECURSE,
  273. Pattern_style.FNMATCH,
  274. )
  275. )
  276. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  277. '/mnt/subvol1', object
  278. ).and_return(Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'))
  279. assert (
  280. module.dump_data_sources(
  281. hook_config=config['btrfs'],
  282. config=config,
  283. config_paths=('test.yaml',),
  284. borgmatic_runtime_directory='/run/borgmatic',
  285. patterns=patterns,
  286. dry_run=False,
  287. )
  288. == []
  289. )
  290. assert patterns == [
  291. Pattern('/foo'),
  292. Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'),
  293. Pattern(
  294. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  295. Pattern_type.NO_RECURSE,
  296. Pattern_style.FNMATCH,
  297. ),
  298. ]
  299. assert config == {
  300. 'btrfs': {
  301. 'btrfs_command': '/usr/local/bin/btrfs',
  302. },
  303. }
  304. def test_dump_data_sources_uses_custom_findmnt_command_in_commands():
  305. flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
  306. flexmock()
  307. )
  308. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  309. config = {'btrfs': {'findmnt_command': '/usr/local/bin/findmnt'}}
  310. flexmock(module).should_receive('get_subvolumes').with_args(
  311. 'btrfs', '/usr/local/bin/findmnt', patterns
  312. ).and_return(
  313. (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),)
  314. ).once()
  315. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  316. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  317. )
  318. flexmock(module).should_receive('snapshot_subvolume').with_args(
  319. 'btrfs', '/mnt/subvol1', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  320. ).once()
  321. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  322. '/mnt/subvol1'
  323. ).and_return(
  324. Pattern(
  325. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  326. Pattern_type.NO_RECURSE,
  327. Pattern_style.FNMATCH,
  328. )
  329. )
  330. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  331. '/mnt/subvol1', object
  332. ).and_return(Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'))
  333. assert (
  334. module.dump_data_sources(
  335. hook_config=config['btrfs'],
  336. config=config,
  337. config_paths=('test.yaml',),
  338. borgmatic_runtime_directory='/run/borgmatic',
  339. patterns=patterns,
  340. dry_run=False,
  341. )
  342. == []
  343. )
  344. assert patterns == [
  345. Pattern('/foo'),
  346. Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'),
  347. Pattern(
  348. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  349. Pattern_type.NO_RECURSE,
  350. Pattern_style.FNMATCH,
  351. ),
  352. ]
  353. assert config == {
  354. 'btrfs': {
  355. 'findmnt_command': '/usr/local/bin/findmnt',
  356. },
  357. }
  358. def test_dump_data_sources_with_dry_run_skips_snapshot_and_patterns_update():
  359. flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
  360. flexmock()
  361. )
  362. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  363. config = {'btrfs': {}}
  364. flexmock(module).should_receive('get_subvolumes').and_return(
  365. (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),)
  366. )
  367. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  368. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  369. )
  370. flexmock(module).should_receive('snapshot_subvolume').never()
  371. flexmock(module).should_receive('make_snapshot_exclude_pattern').never()
  372. assert (
  373. module.dump_data_sources(
  374. hook_config=config['btrfs'],
  375. config=config,
  376. config_paths=('test.yaml',),
  377. borgmatic_runtime_directory='/run/borgmatic',
  378. patterns=patterns,
  379. dry_run=True,
  380. )
  381. == []
  382. )
  383. assert patterns == [Pattern('/foo'), Pattern('/mnt/subvol1')]
  384. assert config == {'btrfs': {}}
  385. def test_dump_data_sources_without_matching_subvolumes_skips_snapshot_and_patterns_update():
  386. flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
  387. flexmock()
  388. )
  389. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  390. config = {'btrfs': {}}
  391. flexmock(module).should_receive('get_subvolumes').and_return(())
  392. flexmock(module).should_receive('make_snapshot_path').never()
  393. flexmock(module).should_receive('snapshot_subvolume').never()
  394. flexmock(module).should_receive('make_snapshot_exclude_pattern').never()
  395. assert (
  396. module.dump_data_sources(
  397. hook_config=config['btrfs'],
  398. config=config,
  399. config_paths=('test.yaml',),
  400. borgmatic_runtime_directory='/run/borgmatic',
  401. patterns=patterns,
  402. dry_run=False,
  403. )
  404. == []
  405. )
  406. assert patterns == [Pattern('/foo'), Pattern('/mnt/subvol1')]
  407. assert config == {'btrfs': {}}
  408. def test_dump_data_sources_snapshots_adds_to_existing_exclude_patterns():
  409. flexmock(module.borgmatic.hooks.command).should_receive('Before_after_hooks').and_return(
  410. flexmock()
  411. )
  412. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  413. config = {'btrfs': {}, 'exclude_patterns': ['/bar']}
  414. flexmock(module).should_receive('get_subvolumes').and_return(
  415. (
  416. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  417. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  418. )
  419. )
  420. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  421. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  422. )
  423. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  424. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  425. )
  426. flexmock(module).should_receive('snapshot_subvolume').with_args(
  427. 'btrfs', '/mnt/subvol1', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  428. ).once()
  429. flexmock(module).should_receive('snapshot_subvolume').with_args(
  430. 'btrfs', '/mnt/subvol2', '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  431. ).once()
  432. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  433. '/mnt/subvol1'
  434. ).and_return(
  435. Pattern(
  436. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  437. Pattern_type.NO_RECURSE,
  438. Pattern_style.FNMATCH,
  439. )
  440. )
  441. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  442. '/mnt/subvol2'
  443. ).and_return(
  444. Pattern(
  445. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
  446. Pattern_type.NO_RECURSE,
  447. Pattern_style.FNMATCH,
  448. )
  449. )
  450. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  451. '/mnt/subvol1', object
  452. ).and_return(Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'))
  453. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  454. '/mnt/subvol2', object
  455. ).and_return(Pattern('/mnt/subvol2/.borgmatic-1234/mnt/subvol2'))
  456. assert (
  457. module.dump_data_sources(
  458. hook_config=config['btrfs'],
  459. config=config,
  460. config_paths=('test.yaml',),
  461. borgmatic_runtime_directory='/run/borgmatic',
  462. patterns=patterns,
  463. dry_run=False,
  464. )
  465. == []
  466. )
  467. assert patterns == [
  468. Pattern('/foo'),
  469. Pattern('/mnt/subvol1/.borgmatic-1234/mnt/subvol1'),
  470. Pattern(
  471. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1/.borgmatic-1234',
  472. Pattern_type.NO_RECURSE,
  473. Pattern_style.FNMATCH,
  474. ),
  475. Pattern('/mnt/subvol2/.borgmatic-1234/mnt/subvol2'),
  476. Pattern(
  477. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2/.borgmatic-1234',
  478. Pattern_type.NO_RECURSE,
  479. Pattern_style.FNMATCH,
  480. ),
  481. ]
  482. assert config == {
  483. 'btrfs': {},
  484. 'exclude_patterns': ['/bar'],
  485. }
  486. def test_remove_data_source_dumps_deletes_snapshots():
  487. config = {'btrfs': {}}
  488. flexmock(module).should_receive('get_subvolumes').and_return(
  489. (
  490. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  491. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  492. )
  493. )
  494. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  495. '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1'
  496. )
  497. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  498. '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2'
  499. )
  500. flexmock(module.borgmatic.config.paths).should_receive(
  501. 'replace_temporary_subdirectory_with_glob'
  502. ).with_args(
  503. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  504. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  505. ).and_return(
  506. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  507. )
  508. flexmock(module.borgmatic.config.paths).should_receive(
  509. 'replace_temporary_subdirectory_with_glob'
  510. ).with_args(
  511. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  512. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  513. ).and_return(
  514. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  515. )
  516. flexmock(module.glob).should_receive('glob').with_args(
  517. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  518. ).and_return(
  519. ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1')
  520. )
  521. flexmock(module.glob).should_receive('glob').with_args(
  522. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  523. ).and_return(
  524. ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2')
  525. )
  526. flexmock(module.os.path).should_receive('isdir').with_args(
  527. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  528. ).and_return(True)
  529. flexmock(module.os.path).should_receive('isdir').with_args(
  530. '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'
  531. ).and_return(True)
  532. flexmock(module.os.path).should_receive('isdir').with_args(
  533. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  534. ).and_return(True)
  535. flexmock(module.os.path).should_receive('isdir').with_args(
  536. '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'
  537. ).and_return(False)
  538. flexmock(module).should_receive('delete_snapshot').with_args(
  539. 'btrfs', '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  540. ).once()
  541. flexmock(module).should_receive('delete_snapshot').with_args(
  542. 'btrfs', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'
  543. ).once()
  544. flexmock(module).should_receive('delete_snapshot').with_args(
  545. 'btrfs', '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  546. ).once()
  547. flexmock(module).should_receive('delete_snapshot').with_args(
  548. 'btrfs', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'
  549. ).never()
  550. flexmock(module.os.path).should_receive('isdir').with_args(
  551. '/mnt/subvol1/.borgmatic-1234'
  552. ).and_return(True)
  553. flexmock(module.os.path).should_receive('isdir').with_args(
  554. '/mnt/subvol1/.borgmatic-5678'
  555. ).and_return(True)
  556. flexmock(module.os.path).should_receive('isdir').with_args(
  557. '/mnt/subvol2/.borgmatic-1234'
  558. ).and_return(True)
  559. flexmock(module.os.path).should_receive('isdir').with_args(
  560. '/mnt/subvol2/.borgmatic-5678'
  561. ).and_return(True)
  562. flexmock(module.shutil).should_receive('rmtree').with_args(
  563. '/mnt/subvol1/.borgmatic-1234'
  564. ).once()
  565. flexmock(module.shutil).should_receive('rmtree').with_args(
  566. '/mnt/subvol1/.borgmatic-5678'
  567. ).once()
  568. flexmock(module.shutil).should_receive('rmtree').with_args(
  569. '/mnt/subvol2/.borgmatic-1234'
  570. ).once()
  571. flexmock(module.shutil).should_receive('rmtree').with_args(
  572. '/mnt/subvol2/.borgmatic-5678'
  573. ).never()
  574. module.remove_data_source_dumps(
  575. hook_config=config['btrfs'],
  576. config=config,
  577. borgmatic_runtime_directory='/run/borgmatic',
  578. dry_run=False,
  579. )
  580. def test_remove_data_source_dumps_without_hook_configuration_bails():
  581. flexmock(module).should_receive('get_subvolumes').never()
  582. flexmock(module).should_receive('make_snapshot_path').never()
  583. flexmock(module.borgmatic.config.paths).should_receive(
  584. 'replace_temporary_subdirectory_with_glob'
  585. ).never()
  586. flexmock(module).should_receive('delete_snapshot').never()
  587. flexmock(module.shutil).should_receive('rmtree').never()
  588. module.remove_data_source_dumps(
  589. hook_config=None,
  590. config={'source_directories': '/mnt/subvolume'},
  591. borgmatic_runtime_directory='/run/borgmatic',
  592. dry_run=False,
  593. )
  594. def test_remove_data_source_dumps_with_get_subvolumes_file_not_found_error_bails():
  595. config = {'btrfs': {}}
  596. flexmock(module).should_receive('get_subvolumes').and_raise(FileNotFoundError)
  597. flexmock(module).should_receive('make_snapshot_path').never()
  598. flexmock(module.borgmatic.config.paths).should_receive(
  599. 'replace_temporary_subdirectory_with_glob'
  600. ).never()
  601. flexmock(module).should_receive('delete_snapshot').never()
  602. flexmock(module.shutil).should_receive('rmtree').never()
  603. module.remove_data_source_dumps(
  604. hook_config=config['btrfs'],
  605. config=config,
  606. borgmatic_runtime_directory='/run/borgmatic',
  607. dry_run=False,
  608. )
  609. def test_remove_data_source_dumps_with_get_subvolumes_called_process_error_bails():
  610. config = {'btrfs': {}}
  611. flexmock(module).should_receive('get_subvolumes').and_raise(
  612. module.subprocess.CalledProcessError(1, 'command', 'error')
  613. )
  614. flexmock(module).should_receive('make_snapshot_path').never()
  615. flexmock(module.borgmatic.config.paths).should_receive(
  616. 'replace_temporary_subdirectory_with_glob'
  617. ).never()
  618. flexmock(module).should_receive('delete_snapshot').never()
  619. flexmock(module.shutil).should_receive('rmtree').never()
  620. module.remove_data_source_dumps(
  621. hook_config=config['btrfs'],
  622. config=config,
  623. borgmatic_runtime_directory='/run/borgmatic',
  624. dry_run=False,
  625. )
  626. def test_remove_data_source_dumps_with_dry_run_skips_deletes():
  627. config = {'btrfs': {}}
  628. flexmock(module).should_receive('get_subvolumes').and_return(
  629. (
  630. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  631. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  632. )
  633. )
  634. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  635. '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1'
  636. )
  637. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  638. '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2'
  639. )
  640. flexmock(module.borgmatic.config.paths).should_receive(
  641. 'replace_temporary_subdirectory_with_glob'
  642. ).with_args(
  643. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  644. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  645. ).and_return(
  646. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  647. )
  648. flexmock(module.borgmatic.config.paths).should_receive(
  649. 'replace_temporary_subdirectory_with_glob'
  650. ).with_args(
  651. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  652. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  653. ).and_return(
  654. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  655. )
  656. flexmock(module.glob).should_receive('glob').with_args(
  657. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  658. ).and_return(
  659. ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1')
  660. )
  661. flexmock(module.glob).should_receive('glob').with_args(
  662. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  663. ).and_return(
  664. ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2')
  665. )
  666. flexmock(module.os.path).should_receive('isdir').with_args(
  667. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  668. ).and_return(True)
  669. flexmock(module.os.path).should_receive('isdir').with_args(
  670. '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'
  671. ).and_return(True)
  672. flexmock(module.os.path).should_receive('isdir').with_args(
  673. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  674. ).and_return(True)
  675. flexmock(module.os.path).should_receive('isdir').with_args(
  676. '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'
  677. ).and_return(False)
  678. flexmock(module).should_receive('delete_snapshot').never()
  679. flexmock(module.shutil).should_receive('rmtree').never()
  680. module.remove_data_source_dumps(
  681. hook_config=config['btrfs'],
  682. config=config,
  683. borgmatic_runtime_directory='/run/borgmatic',
  684. dry_run=True,
  685. )
  686. def test_remove_data_source_dumps_without_subvolumes_skips_deletes():
  687. config = {'btrfs': {}}
  688. flexmock(module).should_receive('get_subvolumes').and_return(())
  689. flexmock(module).should_receive('make_snapshot_path').never()
  690. flexmock(module.borgmatic.config.paths).should_receive(
  691. 'replace_temporary_subdirectory_with_glob'
  692. ).never()
  693. flexmock(module).should_receive('delete_snapshot').never()
  694. flexmock(module.shutil).should_receive('rmtree').never()
  695. module.remove_data_source_dumps(
  696. hook_config=config['btrfs'],
  697. config=config,
  698. borgmatic_runtime_directory='/run/borgmatic',
  699. dry_run=False,
  700. )
  701. def test_remove_data_source_without_snapshots_skips_deletes():
  702. config = {'btrfs': {}}
  703. flexmock(module).should_receive('get_subvolumes').and_return(
  704. (
  705. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  706. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  707. )
  708. )
  709. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  710. '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1'
  711. )
  712. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  713. '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2'
  714. )
  715. flexmock(module.borgmatic.config.paths).should_receive(
  716. 'replace_temporary_subdirectory_with_glob'
  717. ).with_args(
  718. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  719. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  720. ).and_return(
  721. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  722. )
  723. flexmock(module.borgmatic.config.paths).should_receive(
  724. 'replace_temporary_subdirectory_with_glob'
  725. ).with_args(
  726. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  727. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  728. ).and_return(
  729. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  730. )
  731. flexmock(module.glob).should_receive('glob').and_return(())
  732. flexmock(module.os.path).should_receive('isdir').never()
  733. flexmock(module).should_receive('delete_snapshot').never()
  734. flexmock(module.shutil).should_receive('rmtree').never()
  735. module.remove_data_source_dumps(
  736. hook_config=config['btrfs'],
  737. config=config,
  738. borgmatic_runtime_directory='/run/borgmatic',
  739. dry_run=False,
  740. )
  741. def test_remove_data_source_dumps_with_delete_snapshot_file_not_found_error_bails():
  742. config = {'btrfs': {}}
  743. flexmock(module).should_receive('get_subvolumes').and_return(
  744. (
  745. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  746. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  747. )
  748. )
  749. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  750. '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1'
  751. )
  752. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  753. '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2'
  754. )
  755. flexmock(module.borgmatic.config.paths).should_receive(
  756. 'replace_temporary_subdirectory_with_glob'
  757. ).with_args(
  758. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  759. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  760. ).and_return(
  761. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  762. )
  763. flexmock(module.borgmatic.config.paths).should_receive(
  764. 'replace_temporary_subdirectory_with_glob'
  765. ).with_args(
  766. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  767. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  768. ).and_return(
  769. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  770. )
  771. flexmock(module.glob).should_receive('glob').with_args(
  772. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  773. ).and_return(
  774. ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1')
  775. )
  776. flexmock(module.glob).should_receive('glob').with_args(
  777. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  778. ).and_return(
  779. ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2')
  780. )
  781. flexmock(module.os.path).should_receive('isdir').with_args(
  782. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  783. ).and_return(True)
  784. flexmock(module.os.path).should_receive('isdir').with_args(
  785. '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'
  786. ).and_return(True)
  787. flexmock(module.os.path).should_receive('isdir').with_args(
  788. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  789. ).and_return(True)
  790. flexmock(module.os.path).should_receive('isdir').with_args(
  791. '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'
  792. ).and_return(False)
  793. flexmock(module).should_receive('delete_snapshot').and_raise(FileNotFoundError)
  794. flexmock(module.shutil).should_receive('rmtree').never()
  795. module.remove_data_source_dumps(
  796. hook_config=config['btrfs'],
  797. config=config,
  798. borgmatic_runtime_directory='/run/borgmatic',
  799. dry_run=False,
  800. )
  801. def test_remove_data_source_dumps_with_delete_snapshot_called_process_error_bails():
  802. config = {'btrfs': {}}
  803. flexmock(module).should_receive('get_subvolumes').and_return(
  804. (
  805. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  806. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  807. )
  808. )
  809. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  810. '/mnt/subvol1/.borgmatic-1234/./mnt/subvol1'
  811. )
  812. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  813. '/mnt/subvol2/.borgmatic-1234/./mnt/subvol2'
  814. )
  815. flexmock(module.borgmatic.config.paths).should_receive(
  816. 'replace_temporary_subdirectory_with_glob'
  817. ).with_args(
  818. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1',
  819. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  820. ).and_return(
  821. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  822. )
  823. flexmock(module.borgmatic.config.paths).should_receive(
  824. 'replace_temporary_subdirectory_with_glob'
  825. ).with_args(
  826. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2',
  827. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  828. ).and_return(
  829. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  830. )
  831. flexmock(module.glob).should_receive('glob').with_args(
  832. '/mnt/subvol1/.borgmatic-*/mnt/subvol1'
  833. ).and_return(
  834. ('/mnt/subvol1/.borgmatic-1234/mnt/subvol1', '/mnt/subvol1/.borgmatic-5678/mnt/subvol1')
  835. )
  836. flexmock(module.glob).should_receive('glob').with_args(
  837. '/mnt/subvol2/.borgmatic-*/mnt/subvol2'
  838. ).and_return(
  839. ('/mnt/subvol2/.borgmatic-1234/mnt/subvol2', '/mnt/subvol2/.borgmatic-5678/mnt/subvol2')
  840. )
  841. flexmock(module.os.path).should_receive('isdir').with_args(
  842. '/mnt/subvol1/.borgmatic-1234/mnt/subvol1'
  843. ).and_return(True)
  844. flexmock(module.os.path).should_receive('isdir').with_args(
  845. '/mnt/subvol1/.borgmatic-5678/mnt/subvol1'
  846. ).and_return(True)
  847. flexmock(module.os.path).should_receive('isdir').with_args(
  848. '/mnt/subvol2/.borgmatic-1234/mnt/subvol2'
  849. ).and_return(True)
  850. flexmock(module.os.path).should_receive('isdir').with_args(
  851. '/mnt/subvol2/.borgmatic-5678/mnt/subvol2'
  852. ).and_return(False)
  853. flexmock(module).should_receive('delete_snapshot').and_raise(
  854. module.subprocess.CalledProcessError(1, 'command', 'error')
  855. )
  856. flexmock(module.shutil).should_receive('rmtree').never()
  857. module.remove_data_source_dumps(
  858. hook_config=config['btrfs'],
  859. config=config,
  860. borgmatic_runtime_directory='/run/borgmatic',
  861. dry_run=False,
  862. )
  863. def test_remove_data_source_dumps_with_root_subvolume_skips_duplicate_removal():
  864. config = {'btrfs': {}}
  865. flexmock(module).should_receive('get_subvolumes').and_return(
  866. (module.Subvolume('/', contained_patterns=(Pattern('/etc'),)),)
  867. )
  868. flexmock(module).should_receive('make_snapshot_path').with_args('/').and_return(
  869. '/.borgmatic-1234'
  870. )
  871. flexmock(module.borgmatic.config.paths).should_receive(
  872. 'replace_temporary_subdirectory_with_glob'
  873. ).with_args(
  874. '/.borgmatic-1234',
  875. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  876. ).and_return(
  877. '/.borgmatic-*'
  878. )
  879. flexmock(module.glob).should_receive('glob').with_args('/.borgmatic-*').and_return(
  880. ('/.borgmatic-1234', '/.borgmatic-5678')
  881. )
  882. flexmock(module.os.path).should_receive('isdir').with_args('/.borgmatic-1234').and_return(
  883. True
  884. ).and_return(False)
  885. flexmock(module.os.path).should_receive('isdir').with_args('/.borgmatic-5678').and_return(
  886. True
  887. ).and_return(False)
  888. flexmock(module).should_receive('delete_snapshot').with_args('btrfs', '/.borgmatic-1234').once()
  889. flexmock(module).should_receive('delete_snapshot').with_args('btrfs', '/.borgmatic-5678').once()
  890. flexmock(module.os.path).should_receive('isdir').with_args('').and_return(False)
  891. flexmock(module.shutil).should_receive('rmtree').never()
  892. module.remove_data_source_dumps(
  893. hook_config=config['btrfs'],
  894. config=config,
  895. borgmatic_runtime_directory='/run/borgmatic',
  896. dry_run=False,
  897. )