test_btrfs.py 34 KB

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