test_btrfs.py 32 KB

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