2
0

test_btrfs.py 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148
  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_path_is_a_subvolume_with_btrfs_inode_returns_true():
  6. module.path_is_a_subvolume.cache_clear()
  7. flexmock(module.os).should_receive('stat').and_return(
  8. flexmock(st_ino=module.BTRFS_SUBVOLUME_INODE_NUMBER)
  9. )
  10. assert module.path_is_a_subvolume('/mnt0') is True
  11. def test_path_is_a_subvolume_with_non_btrfs_inode_returns_false():
  12. module.path_is_a_subvolume.cache_clear()
  13. flexmock(module.os).should_receive('stat').and_return(flexmock(st_ino=12345))
  14. assert module.path_is_a_subvolume('/mnt0') is False
  15. def test_path_is_a_subvolume_caches_result_after_first_call():
  16. module.path_is_a_subvolume.cache_clear()
  17. flexmock(module.os).should_receive('stat').and_return(
  18. flexmock(st_ino=module.BTRFS_SUBVOLUME_INODE_NUMBER)
  19. ).once()
  20. assert module.path_is_a_subvolume('/mnt0') is True
  21. assert module.path_is_a_subvolume('/mnt0') is True
  22. def test_path_is_a_subvolume_with_missing_path_returns_false():
  23. module.path_is_a_subvolume.cache_clear()
  24. flexmock(module.os).should_receive('stat').and_raise(FileNotFoundError)
  25. assert module.path_is_a_subvolume('/mnt0') is False
  26. def test_get_subvolume_property_with_invalid_btrfs_output_errors():
  27. module.get_subvolume_property.cache_clear()
  28. flexmock(module.borgmatic.execute).should_receive(
  29. 'execute_command_and_capture_output',
  30. ).and_return('invalid')
  31. with pytest.raises(ValueError):
  32. module.get_subvolume_property('btrfs', '/foo', 'ro')
  33. def test_get_subvolume_property_with_true_output_returns_true_bool():
  34. module.get_subvolume_property.cache_clear()
  35. flexmock(module.borgmatic.execute).should_receive(
  36. 'execute_command_and_capture_output',
  37. ).and_return('ro=true')
  38. assert module.get_subvolume_property('btrfs', '/foo', 'ro') is True
  39. def test_get_subvolume_property_with_false_output_returns_false_bool():
  40. module.get_subvolume_property.cache_clear()
  41. flexmock(module.borgmatic.execute).should_receive(
  42. 'execute_command_and_capture_output',
  43. ).and_return('ro=false')
  44. assert module.get_subvolume_property('btrfs', '/foo', 'ro') is False
  45. def test_get_subvolume_property_passes_through_general_value():
  46. module.get_subvolume_property.cache_clear()
  47. flexmock(module.borgmatic.execute).should_receive(
  48. 'execute_command_and_capture_output',
  49. ).and_return('thing=value')
  50. assert module.get_subvolume_property('btrfs', '/foo', 'thing') == 'value'
  51. def test_get_subvolume_property_caches_result_after_first_call():
  52. module.get_subvolume_property.cache_clear()
  53. flexmock(module.borgmatic.execute).should_receive(
  54. 'execute_command_and_capture_output',
  55. ).and_return('thing=value').once()
  56. assert module.get_subvolume_property('btrfs', '/foo', 'thing') == 'value'
  57. assert module.get_subvolume_property('btrfs', '/foo', 'thing') == 'value'
  58. def test_get_containing_subvolume_path_with_subvolume_self_returns_it():
  59. flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar/baz').and_return(
  60. True
  61. )
  62. flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar').never()
  63. flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo').never()
  64. flexmock(module).should_receive('path_is_a_subvolume').with_args('/').never()
  65. flexmock(module).should_receive('get_subvolume_property').and_return(False)
  66. assert module.get_containing_subvolume_path('btrfs', '/foo/bar/baz') == '/foo/bar/baz'
  67. def test_get_containing_subvolume_path_with_subvolume_parent_returns_it():
  68. flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar/baz').and_return(
  69. False
  70. )
  71. flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar').and_return(True)
  72. flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo').never()
  73. flexmock(module).should_receive('path_is_a_subvolume').with_args('/').never()
  74. flexmock(module).should_receive('get_subvolume_property').and_return(False)
  75. assert module.get_containing_subvolume_path('btrfs', '/foo/bar/baz') == '/foo/bar'
  76. def test_get_containing_subvolume_path_with_subvolume_grandparent_returns_it():
  77. flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar/baz').and_return(
  78. False
  79. )
  80. flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar').and_return(False)
  81. flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo').and_return(True)
  82. flexmock(module).should_receive('path_is_a_subvolume').with_args('/').never()
  83. flexmock(module).should_receive('get_subvolume_property').and_return(False)
  84. assert module.get_containing_subvolume_path('btrfs', '/foo/bar/baz') == '/foo'
  85. def test_get_containing_subvolume_path_without_subvolume_ancestor_returns_none():
  86. flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar/baz').and_return(
  87. False
  88. )
  89. flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar').and_return(False)
  90. flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo').and_return(False)
  91. flexmock(module).should_receive('path_is_a_subvolume').with_args('/').and_return(False)
  92. flexmock(module).should_receive('get_subvolume_property').and_return(False)
  93. assert module.get_containing_subvolume_path('btrfs', '/foo/bar/baz') is None
  94. def test_get_containing_subvolume_path_with_read_only_subvolume_returns_none():
  95. flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar/baz').and_return(
  96. True
  97. )
  98. flexmock(module).should_receive('get_subvolume_property').and_return(True)
  99. assert module.get_containing_subvolume_path('btrfs', '/foo/bar/baz') is None
  100. def test_get_containing_subvolume_path_with_read_only_error_returns_none():
  101. flexmock(module).should_receive('path_is_a_subvolume').with_args('/foo/bar/baz').and_return(
  102. True
  103. )
  104. flexmock(module).should_receive('get_subvolume_property').and_raise(
  105. module.subprocess.CalledProcessError(1, 'wtf')
  106. )
  107. assert module.get_containing_subvolume_path('btrfs', '/foo/bar/baz') is None
  108. def test_get_all_subvolume_paths_skips_non_root_and_non_config_patterns():
  109. flexmock(module).should_receive('get_containing_subvolume_path').with_args(
  110. 'btrfs', '/foo'
  111. ).never()
  112. flexmock(module).should_receive('get_containing_subvolume_path').with_args(
  113. 'btrfs', '/bar'
  114. ).and_return('/bar').once()
  115. flexmock(module).should_receive('get_containing_subvolume_path').with_args(
  116. 'btrfs', '/baz'
  117. ).never()
  118. assert module.get_all_subvolume_paths(
  119. 'btrfs',
  120. (
  121. module.borgmatic.borg.pattern.Pattern(
  122. '/foo',
  123. type=module.borgmatic.borg.pattern.Pattern_type.ROOT,
  124. source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
  125. ),
  126. module.borgmatic.borg.pattern.Pattern(
  127. '/bar',
  128. type=module.borgmatic.borg.pattern.Pattern_type.ROOT,
  129. source=module.borgmatic.borg.pattern.Pattern_source.CONFIG,
  130. ),
  131. module.borgmatic.borg.pattern.Pattern(
  132. '/baz',
  133. type=module.borgmatic.borg.pattern.Pattern_type.INCLUDE,
  134. source=module.borgmatic.borg.pattern.Pattern_source.CONFIG,
  135. ),
  136. ),
  137. ) == ('/bar',)
  138. def test_get_all_subvolume_paths_skips_non_btrfs_patterns():
  139. flexmock(module).should_receive('get_containing_subvolume_path').with_args(
  140. 'btrfs', '/foo'
  141. ).and_return(None).once()
  142. flexmock(module).should_receive('get_containing_subvolume_path').with_args(
  143. 'btrfs', '/bar'
  144. ).and_return('/bar').once()
  145. assert module.get_all_subvolume_paths(
  146. 'btrfs',
  147. (
  148. module.borgmatic.borg.pattern.Pattern(
  149. '/foo',
  150. type=module.borgmatic.borg.pattern.Pattern_type.ROOT,
  151. source=module.borgmatic.borg.pattern.Pattern_source.CONFIG,
  152. ),
  153. module.borgmatic.borg.pattern.Pattern(
  154. '/bar',
  155. type=module.borgmatic.borg.pattern.Pattern_type.ROOT,
  156. source=module.borgmatic.borg.pattern.Pattern_source.CONFIG,
  157. ),
  158. ),
  159. ) == ('/bar',)
  160. def test_get_all_subvolume_paths_sorts_subvolume_paths():
  161. flexmock(module).should_receive('get_containing_subvolume_path').with_args(
  162. 'btrfs', '/foo'
  163. ).and_return('/foo').once()
  164. flexmock(module).should_receive('get_containing_subvolume_path').with_args(
  165. 'btrfs', '/bar'
  166. ).and_return('/bar').once()
  167. flexmock(module).should_receive('get_containing_subvolume_path').with_args(
  168. 'btrfs', '/baz'
  169. ).and_return('/baz').once()
  170. assert module.get_all_subvolume_paths(
  171. 'btrfs',
  172. (
  173. module.borgmatic.borg.pattern.Pattern(
  174. '/foo',
  175. type=module.borgmatic.borg.pattern.Pattern_type.ROOT,
  176. source=module.borgmatic.borg.pattern.Pattern_source.CONFIG,
  177. ),
  178. module.borgmatic.borg.pattern.Pattern(
  179. '/bar',
  180. type=module.borgmatic.borg.pattern.Pattern_type.ROOT,
  181. source=module.borgmatic.borg.pattern.Pattern_source.CONFIG,
  182. ),
  183. module.borgmatic.borg.pattern.Pattern(
  184. '/baz',
  185. type=module.borgmatic.borg.pattern.Pattern_type.ROOT,
  186. source=module.borgmatic.borg.pattern.Pattern_source.CONFIG,
  187. ),
  188. ),
  189. ) == ('/bar', '/baz', '/foo')
  190. def test_get_subvolumes_collects_subvolumes_matching_patterns():
  191. flexmock(module).should_receive('get_all_subvolume_paths').and_return(('/mnt1', '/mnt2'))
  192. contained_pattern = Pattern(
  193. '/mnt1',
  194. type=Pattern_type.ROOT,
  195. source=Pattern_source.CONFIG,
  196. )
  197. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  198. 'get_contained_patterns',
  199. ).with_args('/mnt1', object).and_return((contained_pattern,))
  200. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  201. 'get_contained_patterns',
  202. ).with_args('/mnt2', object).and_return(())
  203. assert module.get_subvolumes(
  204. 'btrfs',
  205. patterns=[
  206. Pattern('/mnt1'),
  207. Pattern('/mnt3'),
  208. ],
  209. ) == (module.Subvolume('/mnt1', contained_patterns=(contained_pattern,)),)
  210. def test_get_subvolumes_skips_non_root_patterns():
  211. flexmock(module).should_receive('get_all_subvolume_paths').and_return(('/mnt1', '/mnt2'))
  212. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  213. 'get_contained_patterns',
  214. ).with_args('/mnt1', object).and_return(
  215. (
  216. Pattern(
  217. '/mnt1',
  218. type=Pattern_type.EXCLUDE,
  219. source=Pattern_source.CONFIG,
  220. ),
  221. ),
  222. )
  223. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  224. 'get_contained_patterns',
  225. ).with_args('/mnt2', object).and_return(())
  226. assert (
  227. module.get_subvolumes(
  228. 'btrfs',
  229. patterns=[
  230. Pattern('/mnt1'),
  231. Pattern('/mnt3'),
  232. ],
  233. )
  234. == ()
  235. )
  236. def test_get_subvolumes_skips_non_config_patterns():
  237. flexmock(module).should_receive('get_all_subvolume_paths').and_return(('/mnt1', '/mnt2'))
  238. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  239. 'get_contained_patterns',
  240. ).with_args('/mnt1', object).and_return(
  241. (
  242. Pattern(
  243. '/mnt1',
  244. type=Pattern_type.ROOT,
  245. source=Pattern_source.HOOK,
  246. ),
  247. ),
  248. )
  249. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  250. 'get_contained_patterns',
  251. ).with_args('/mnt2', object).and_return(())
  252. assert (
  253. module.get_subvolumes(
  254. 'btrfs',
  255. patterns=[
  256. Pattern('/mnt1'),
  257. Pattern('/mnt3'),
  258. ],
  259. )
  260. == ()
  261. )
  262. @pytest.mark.parametrize(
  263. 'subvolume_path,expected_snapshot_path',
  264. (
  265. ('/foo/bar', '/foo/bar/.borgmatic-snapshot-1234/foo/bar'),
  266. ('/', '/.borgmatic-snapshot-1234'),
  267. ),
  268. )
  269. def test_make_snapshot_path_includes_stripped_subvolume_path(
  270. subvolume_path,
  271. expected_snapshot_path,
  272. ):
  273. flexmock(module.os).should_receive('getpid').and_return(1234)
  274. assert module.make_snapshot_path(subvolume_path) == expected_snapshot_path
  275. @pytest.mark.parametrize(
  276. 'subvolume_path,pattern,expected_pattern',
  277. (
  278. (
  279. '/foo/bar',
  280. Pattern('/foo/bar/baz'),
  281. Pattern('/foo/bar/.borgmatic-snapshot-1234/./foo/bar/baz'),
  282. ),
  283. ('/foo/bar', Pattern('/foo/bar'), Pattern('/foo/bar/.borgmatic-snapshot-1234/./foo/bar')),
  284. (
  285. '/foo/bar',
  286. Pattern('^/foo/bar', Pattern_type.INCLUDE, Pattern_style.REGULAR_EXPRESSION),
  287. Pattern(
  288. '^/foo/bar/.borgmatic-snapshot-1234/./foo/bar',
  289. Pattern_type.INCLUDE,
  290. Pattern_style.REGULAR_EXPRESSION,
  291. ),
  292. ),
  293. (
  294. '/foo/bar',
  295. Pattern('/foo/bar', Pattern_type.INCLUDE, Pattern_style.REGULAR_EXPRESSION),
  296. Pattern(
  297. '/foo/bar/.borgmatic-snapshot-1234/./foo/bar',
  298. Pattern_type.INCLUDE,
  299. Pattern_style.REGULAR_EXPRESSION,
  300. ),
  301. ),
  302. ('/', Pattern('/foo'), Pattern('/.borgmatic-snapshot-1234/./foo')),
  303. ('/', Pattern('/'), Pattern('/.borgmatic-snapshot-1234/./')),
  304. (
  305. '/foo/bar',
  306. Pattern('/foo/bar/./baz'),
  307. Pattern('/foo/bar/.borgmatic-snapshot-1234/foo/bar/./baz'),
  308. ),
  309. ),
  310. )
  311. def test_make_borg_snapshot_pattern_includes_slashdot_hack_and_stripped_pattern_path(
  312. subvolume_path,
  313. pattern,
  314. expected_pattern,
  315. ):
  316. flexmock(module.os).should_receive('getpid').and_return(1234)
  317. assert module.make_borg_snapshot_pattern(subvolume_path, pattern) == expected_pattern
  318. def test_dump_data_sources_snapshots_each_subvolume_and_replaces_patterns():
  319. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1'), Pattern('/mnt/subvol2')]
  320. config = {'btrfs': {}}
  321. flexmock(module).should_receive('get_subvolumes').and_return(
  322. (
  323. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  324. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  325. ),
  326. )
  327. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  328. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  329. )
  330. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  331. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
  332. )
  333. flexmock(module).should_receive('snapshot_subvolume').with_args(
  334. 'btrfs',
  335. '/mnt/subvol1',
  336. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  337. ).once()
  338. flexmock(module).should_receive('snapshot_subvolume').with_args(
  339. 'btrfs',
  340. '/mnt/subvol2',
  341. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
  342. ).once()
  343. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  344. '/mnt/subvol1',
  345. ).and_return(
  346. Pattern(
  347. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
  348. Pattern_type.NO_RECURSE,
  349. Pattern_style.FNMATCH,
  350. ),
  351. )
  352. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  353. '/mnt/subvol2',
  354. ).and_return(
  355. Pattern(
  356. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2/.borgmatic-snapshot-1234',
  357. Pattern_type.NO_RECURSE,
  358. Pattern_style.FNMATCH,
  359. ),
  360. )
  361. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  362. '/mnt/subvol1',
  363. object,
  364. ).and_return(Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1'))
  365. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  366. '/mnt/subvol2',
  367. object,
  368. ).and_return(Pattern('/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2'))
  369. flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
  370. object,
  371. Pattern('/mnt/subvol1'),
  372. module.borgmatic.borg.pattern.Pattern(
  373. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  374. source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
  375. ),
  376. ).once()
  377. flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
  378. object,
  379. Pattern('/mnt/subvol2'),
  380. module.borgmatic.borg.pattern.Pattern(
  381. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
  382. source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
  383. ),
  384. ).once()
  385. flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
  386. object,
  387. module.borgmatic.borg.pattern.Pattern(
  388. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
  389. Pattern_type.NO_RECURSE,
  390. Pattern_style.FNMATCH,
  391. ),
  392. ).once()
  393. flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
  394. object,
  395. module.borgmatic.borg.pattern.Pattern(
  396. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2/.borgmatic-snapshot-1234',
  397. Pattern_type.NO_RECURSE,
  398. Pattern_style.FNMATCH,
  399. ),
  400. ).once()
  401. assert (
  402. module.dump_data_sources(
  403. hook_config=config['btrfs'],
  404. config=config,
  405. config_paths=('test.yaml',),
  406. borgmatic_runtime_directory='/run/borgmatic',
  407. patterns=patterns,
  408. dry_run=False,
  409. )
  410. == []
  411. )
  412. assert config == {
  413. 'btrfs': {},
  414. }
  415. def test_dump_data_sources_uses_custom_btrfs_command_in_commands():
  416. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  417. config = {'btrfs': {'btrfs_command': '/usr/local/bin/btrfs'}}
  418. flexmock(module).should_receive('get_subvolumes').and_return(
  419. (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),),
  420. )
  421. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  422. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  423. )
  424. flexmock(module).should_receive('snapshot_subvolume').with_args(
  425. '/usr/local/bin/btrfs',
  426. '/mnt/subvol1',
  427. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  428. ).once()
  429. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  430. '/mnt/subvol1',
  431. ).and_return(
  432. Pattern(
  433. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
  434. Pattern_type.NO_RECURSE,
  435. Pattern_style.FNMATCH,
  436. ),
  437. )
  438. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  439. '/mnt/subvol1',
  440. object,
  441. ).and_return(Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1'))
  442. flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
  443. object,
  444. Pattern('/mnt/subvol1'),
  445. module.borgmatic.borg.pattern.Pattern(
  446. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  447. source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
  448. ),
  449. ).once()
  450. flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
  451. object,
  452. module.borgmatic.borg.pattern.Pattern(
  453. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
  454. Pattern_type.NO_RECURSE,
  455. Pattern_style.FNMATCH,
  456. ),
  457. ).once()
  458. assert (
  459. module.dump_data_sources(
  460. hook_config=config['btrfs'],
  461. config=config,
  462. config_paths=('test.yaml',),
  463. borgmatic_runtime_directory='/run/borgmatic',
  464. patterns=patterns,
  465. dry_run=False,
  466. )
  467. == []
  468. )
  469. assert config == {
  470. 'btrfs': {
  471. 'btrfs_command': '/usr/local/bin/btrfs',
  472. },
  473. }
  474. def test_dump_data_sources_with_findmnt_command_warns():
  475. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  476. config = {'btrfs': {'findmnt_command': '/usr/local/bin/findmnt'}}
  477. flexmock(module.logger).should_receive('warning').once()
  478. flexmock(module).should_receive('get_subvolumes').with_args(
  479. 'btrfs',
  480. patterns,
  481. ).and_return(
  482. (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),),
  483. ).once()
  484. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  485. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  486. )
  487. flexmock(module).should_receive('snapshot_subvolume').with_args(
  488. 'btrfs',
  489. '/mnt/subvol1',
  490. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  491. ).once()
  492. flexmock(module).should_receive('make_snapshot_exclude_pattern').with_args(
  493. '/mnt/subvol1',
  494. ).and_return(
  495. Pattern(
  496. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
  497. Pattern_type.NO_RECURSE,
  498. Pattern_style.FNMATCH,
  499. ),
  500. )
  501. flexmock(module).should_receive('make_borg_snapshot_pattern').with_args(
  502. '/mnt/subvol1',
  503. object,
  504. ).and_return(Pattern('/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1'))
  505. flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').with_args(
  506. object,
  507. Pattern('/mnt/subvol1'),
  508. module.borgmatic.borg.pattern.Pattern(
  509. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  510. source=module.borgmatic.borg.pattern.Pattern_source.HOOK,
  511. ),
  512. ).once()
  513. flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').with_args(
  514. object,
  515. module.borgmatic.borg.pattern.Pattern(
  516. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1/.borgmatic-snapshot-1234',
  517. Pattern_type.NO_RECURSE,
  518. Pattern_style.FNMATCH,
  519. ),
  520. ).once()
  521. assert (
  522. module.dump_data_sources(
  523. hook_config=config['btrfs'],
  524. config=config,
  525. config_paths=('test.yaml',),
  526. borgmatic_runtime_directory='/run/borgmatic',
  527. patterns=patterns,
  528. dry_run=False,
  529. )
  530. == []
  531. )
  532. assert config == {
  533. 'btrfs': {
  534. 'findmnt_command': '/usr/local/bin/findmnt',
  535. },
  536. }
  537. def test_dump_data_sources_with_dry_run_skips_snapshot_and_patterns_update():
  538. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  539. config = {'btrfs': {}}
  540. flexmock(module).should_receive('get_subvolumes').and_return(
  541. (module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),),
  542. )
  543. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  544. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  545. )
  546. flexmock(module).should_receive('snapshot_subvolume').never()
  547. flexmock(module).should_receive('make_snapshot_exclude_pattern').never()
  548. flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
  549. flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
  550. assert (
  551. module.dump_data_sources(
  552. hook_config=config['btrfs'],
  553. config=config,
  554. config_paths=('test.yaml',),
  555. borgmatic_runtime_directory='/run/borgmatic',
  556. patterns=patterns,
  557. dry_run=True,
  558. )
  559. == []
  560. )
  561. assert config == {'btrfs': {}}
  562. def test_dump_data_sources_without_matching_subvolumes_skips_snapshot_and_patterns_update():
  563. patterns = [Pattern('/foo'), Pattern('/mnt/subvol1')]
  564. config = {'btrfs': {}}
  565. flexmock(module).should_receive('get_subvolumes').and_return(())
  566. flexmock(module).should_receive('make_snapshot_path').never()
  567. flexmock(module).should_receive('snapshot_subvolume').never()
  568. flexmock(module).should_receive('make_snapshot_exclude_pattern').never()
  569. flexmock(module.borgmatic.hooks.data_source.config).should_receive('replace_pattern').never()
  570. flexmock(module.borgmatic.hooks.data_source.config).should_receive('inject_pattern').never()
  571. assert (
  572. module.dump_data_sources(
  573. hook_config=config['btrfs'],
  574. config=config,
  575. config_paths=('test.yaml',),
  576. borgmatic_runtime_directory='/run/borgmatic',
  577. patterns=patterns,
  578. dry_run=False,
  579. )
  580. == []
  581. )
  582. assert config == {'btrfs': {}}
  583. def test_remove_data_source_dumps_deletes_snapshots():
  584. config = {'btrfs': {}}
  585. flexmock(module).should_receive('get_subvolumes').and_return(
  586. (
  587. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  588. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  589. ),
  590. )
  591. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  592. '/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1',
  593. )
  594. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  595. '/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2',
  596. )
  597. flexmock(module.borgmatic.config.paths).should_receive(
  598. 'replace_temporary_subdirectory_with_glob',
  599. ).with_args(
  600. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  601. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  602. ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
  603. flexmock(module.borgmatic.config.paths).should_receive(
  604. 'replace_temporary_subdirectory_with_glob',
  605. ).with_args(
  606. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
  607. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  608. ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
  609. flexmock(module.glob).should_receive('glob').with_args(
  610. '/mnt/subvol1/.borgmatic-*/mnt/subvol1',
  611. ).and_return(
  612. (
  613. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  614. '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
  615. ),
  616. )
  617. flexmock(module.glob).should_receive('glob').with_args(
  618. '/mnt/subvol2/.borgmatic-*/mnt/subvol2',
  619. ).and_return(
  620. (
  621. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
  622. '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
  623. ),
  624. )
  625. flexmock(module.os.path).should_receive('isdir').with_args(
  626. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  627. ).and_return(True)
  628. flexmock(module.os.path).should_receive('isdir').with_args(
  629. '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
  630. ).and_return(True)
  631. flexmock(module.os.path).should_receive('isdir').with_args(
  632. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
  633. ).and_return(True)
  634. flexmock(module.os.path).should_receive('isdir').with_args(
  635. '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
  636. ).and_return(False)
  637. flexmock(module).should_receive('delete_snapshot').with_args(
  638. 'btrfs',
  639. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  640. ).once()
  641. flexmock(module).should_receive('delete_snapshot').with_args(
  642. 'btrfs',
  643. '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
  644. ).once()
  645. flexmock(module).should_receive('delete_snapshot').with_args(
  646. 'btrfs',
  647. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
  648. ).once()
  649. flexmock(module).should_receive('delete_snapshot').with_args(
  650. 'btrfs',
  651. '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
  652. ).never()
  653. flexmock(module.os.path).should_receive('isdir').with_args(
  654. '/mnt/subvol1/.borgmatic-snapshot-1234',
  655. ).and_return(True)
  656. flexmock(module.os.path).should_receive('isdir').with_args(
  657. '/mnt/subvol1/.borgmatic-snapshot-5678',
  658. ).and_return(True)
  659. flexmock(module.os.path).should_receive('isdir').with_args(
  660. '/mnt/subvol2/.borgmatic-snapshot-1234',
  661. ).and_return(True)
  662. flexmock(module.os.path).should_receive('isdir').with_args(
  663. '/mnt/subvol2/.borgmatic-snapshot-5678',
  664. ).and_return(True)
  665. flexmock(module.shutil).should_receive('rmtree').with_args(
  666. '/mnt/subvol1/.borgmatic-snapshot-1234',
  667. ).once()
  668. flexmock(module.shutil).should_receive('rmtree').with_args(
  669. '/mnt/subvol1/.borgmatic-snapshot-5678',
  670. ).once()
  671. flexmock(module.shutil).should_receive('rmtree').with_args(
  672. '/mnt/subvol2/.borgmatic-snapshot-1234',
  673. ).once()
  674. flexmock(module.shutil).should_receive('rmtree').with_args(
  675. '/mnt/subvol2/.borgmatic-snapshot-5678',
  676. ).never()
  677. module.remove_data_source_dumps(
  678. hook_config=config['btrfs'],
  679. config=config,
  680. borgmatic_runtime_directory='/run/borgmatic',
  681. patterns=flexmock(),
  682. dry_run=False,
  683. )
  684. def test_remove_data_source_dumps_without_hook_configuration_bails():
  685. flexmock(module).should_receive('get_subvolumes').never()
  686. flexmock(module).should_receive('make_snapshot_path').never()
  687. flexmock(module.borgmatic.config.paths).should_receive(
  688. 'replace_temporary_subdirectory_with_glob',
  689. ).never()
  690. flexmock(module).should_receive('delete_snapshot').never()
  691. flexmock(module.shutil).should_receive('rmtree').never()
  692. module.remove_data_source_dumps(
  693. hook_config=None,
  694. config={'source_directories': '/mnt/subvolume'},
  695. borgmatic_runtime_directory='/run/borgmatic',
  696. patterns=flexmock(),
  697. dry_run=False,
  698. )
  699. def test_remove_data_source_dumps_with_get_subvolumes_file_not_found_error_bails():
  700. config = {'btrfs': {}}
  701. flexmock(module).should_receive('get_subvolumes').and_raise(FileNotFoundError)
  702. flexmock(module).should_receive('make_snapshot_path').never()
  703. flexmock(module.borgmatic.config.paths).should_receive(
  704. 'replace_temporary_subdirectory_with_glob',
  705. ).never()
  706. flexmock(module).should_receive('delete_snapshot').never()
  707. flexmock(module.shutil).should_receive('rmtree').never()
  708. module.remove_data_source_dumps(
  709. hook_config=config['btrfs'],
  710. config=config,
  711. borgmatic_runtime_directory='/run/borgmatic',
  712. patterns=flexmock(),
  713. dry_run=False,
  714. )
  715. def test_remove_data_source_dumps_with_get_subvolumes_called_process_error_bails():
  716. config = {'btrfs': {}}
  717. flexmock(module).should_receive('get_subvolumes').and_raise(
  718. module.subprocess.CalledProcessError(1, 'command', 'error'),
  719. )
  720. flexmock(module).should_receive('make_snapshot_path').never()
  721. flexmock(module.borgmatic.config.paths).should_receive(
  722. 'replace_temporary_subdirectory_with_glob',
  723. ).never()
  724. flexmock(module).should_receive('delete_snapshot').never()
  725. flexmock(module.shutil).should_receive('rmtree').never()
  726. module.remove_data_source_dumps(
  727. hook_config=config['btrfs'],
  728. config=config,
  729. borgmatic_runtime_directory='/run/borgmatic',
  730. patterns=flexmock(),
  731. dry_run=False,
  732. )
  733. def test_remove_data_source_dumps_with_dry_run_skips_deletes():
  734. config = {'btrfs': {}}
  735. flexmock(module).should_receive('get_subvolumes').and_return(
  736. (
  737. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  738. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  739. ),
  740. )
  741. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  742. '/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1',
  743. )
  744. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  745. '/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2',
  746. )
  747. flexmock(module.borgmatic.config.paths).should_receive(
  748. 'replace_temporary_subdirectory_with_glob',
  749. ).with_args(
  750. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  751. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  752. ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
  753. flexmock(module.borgmatic.config.paths).should_receive(
  754. 'replace_temporary_subdirectory_with_glob',
  755. ).with_args(
  756. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
  757. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  758. ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
  759. flexmock(module.glob).should_receive('glob').with_args(
  760. '/mnt/subvol1/.borgmatic-*/mnt/subvol1',
  761. ).and_return(
  762. (
  763. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  764. '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
  765. ),
  766. )
  767. flexmock(module.glob).should_receive('glob').with_args(
  768. '/mnt/subvol2/.borgmatic-*/mnt/subvol2',
  769. ).and_return(
  770. (
  771. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
  772. '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
  773. ),
  774. )
  775. flexmock(module.os.path).should_receive('isdir').with_args(
  776. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  777. ).and_return(True)
  778. flexmock(module.os.path).should_receive('isdir').with_args(
  779. '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
  780. ).and_return(True)
  781. flexmock(module.os.path).should_receive('isdir').with_args(
  782. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
  783. ).and_return(True)
  784. flexmock(module.os.path).should_receive('isdir').with_args(
  785. '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
  786. ).and_return(False)
  787. flexmock(module).should_receive('delete_snapshot').never()
  788. flexmock(module.shutil).should_receive('rmtree').never()
  789. module.remove_data_source_dumps(
  790. hook_config=config['btrfs'],
  791. config=config,
  792. borgmatic_runtime_directory='/run/borgmatic',
  793. patterns=flexmock(),
  794. dry_run=True,
  795. )
  796. def test_remove_data_source_dumps_without_subvolumes_skips_deletes():
  797. config = {'btrfs': {}}
  798. flexmock(module).should_receive('get_subvolumes').and_return(())
  799. flexmock(module).should_receive('make_snapshot_path').never()
  800. flexmock(module.borgmatic.config.paths).should_receive(
  801. 'replace_temporary_subdirectory_with_glob',
  802. ).never()
  803. flexmock(module).should_receive('delete_snapshot').never()
  804. flexmock(module.shutil).should_receive('rmtree').never()
  805. module.remove_data_source_dumps(
  806. hook_config=config['btrfs'],
  807. config=config,
  808. borgmatic_runtime_directory='/run/borgmatic',
  809. patterns=flexmock(),
  810. dry_run=False,
  811. )
  812. def test_remove_data_source_without_snapshots_skips_deletes():
  813. config = {'btrfs': {}}
  814. flexmock(module).should_receive('get_subvolumes').and_return(
  815. (
  816. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  817. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  818. ),
  819. )
  820. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  821. '/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1',
  822. )
  823. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  824. '/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2',
  825. )
  826. flexmock(module.borgmatic.config.paths).should_receive(
  827. 'replace_temporary_subdirectory_with_glob',
  828. ).with_args(
  829. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  830. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  831. ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
  832. flexmock(module.borgmatic.config.paths).should_receive(
  833. 'replace_temporary_subdirectory_with_glob',
  834. ).with_args(
  835. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
  836. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  837. ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
  838. flexmock(module.glob).should_receive('glob').and_return(())
  839. flexmock(module.os.path).should_receive('isdir').never()
  840. flexmock(module).should_receive('delete_snapshot').never()
  841. flexmock(module.shutil).should_receive('rmtree').never()
  842. module.remove_data_source_dumps(
  843. hook_config=config['btrfs'],
  844. config=config,
  845. borgmatic_runtime_directory='/run/borgmatic',
  846. patterns=flexmock(),
  847. dry_run=False,
  848. )
  849. def test_remove_data_source_dumps_with_delete_snapshot_file_not_found_error_bails():
  850. config = {'btrfs': {}}
  851. flexmock(module).should_receive('get_subvolumes').and_return(
  852. (
  853. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  854. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  855. ),
  856. )
  857. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  858. '/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1',
  859. )
  860. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  861. '/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2',
  862. )
  863. flexmock(module.borgmatic.config.paths).should_receive(
  864. 'replace_temporary_subdirectory_with_glob',
  865. ).with_args(
  866. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  867. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  868. ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
  869. flexmock(module.borgmatic.config.paths).should_receive(
  870. 'replace_temporary_subdirectory_with_glob',
  871. ).with_args(
  872. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
  873. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  874. ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
  875. flexmock(module.glob).should_receive('glob').with_args(
  876. '/mnt/subvol1/.borgmatic-*/mnt/subvol1',
  877. ).and_return(
  878. (
  879. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  880. '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
  881. ),
  882. )
  883. flexmock(module.glob).should_receive('glob').with_args(
  884. '/mnt/subvol2/.borgmatic-*/mnt/subvol2',
  885. ).and_return(
  886. (
  887. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
  888. '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
  889. ),
  890. )
  891. flexmock(module.os.path).should_receive('isdir').with_args(
  892. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  893. ).and_return(True)
  894. flexmock(module.os.path).should_receive('isdir').with_args(
  895. '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
  896. ).and_return(True)
  897. flexmock(module.os.path).should_receive('isdir').with_args(
  898. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
  899. ).and_return(True)
  900. flexmock(module.os.path).should_receive('isdir').with_args(
  901. '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
  902. ).and_return(False)
  903. flexmock(module).should_receive('delete_snapshot').and_raise(FileNotFoundError)
  904. flexmock(module.shutil).should_receive('rmtree').never()
  905. module.remove_data_source_dumps(
  906. hook_config=config['btrfs'],
  907. config=config,
  908. borgmatic_runtime_directory='/run/borgmatic',
  909. patterns=flexmock(),
  910. dry_run=False,
  911. )
  912. def test_remove_data_source_dumps_with_delete_snapshot_called_process_error_bails():
  913. config = {'btrfs': {}}
  914. flexmock(module).should_receive('get_subvolumes').and_return(
  915. (
  916. module.Subvolume('/mnt/subvol1', contained_patterns=(Pattern('/mnt/subvol1'),)),
  917. module.Subvolume('/mnt/subvol2', contained_patterns=(Pattern('/mnt/subvol2'),)),
  918. ),
  919. )
  920. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol1').and_return(
  921. '/mnt/subvol1/.borgmatic-snapshot-1234/./mnt/subvol1',
  922. )
  923. flexmock(module).should_receive('make_snapshot_path').with_args('/mnt/subvol2').and_return(
  924. '/mnt/subvol2/.borgmatic-snapshot-1234/./mnt/subvol2',
  925. )
  926. flexmock(module.borgmatic.config.paths).should_receive(
  927. 'replace_temporary_subdirectory_with_glob',
  928. ).with_args(
  929. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  930. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  931. ).and_return('/mnt/subvol1/.borgmatic-*/mnt/subvol1')
  932. flexmock(module.borgmatic.config.paths).should_receive(
  933. 'replace_temporary_subdirectory_with_glob',
  934. ).with_args(
  935. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
  936. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  937. ).and_return('/mnt/subvol2/.borgmatic-*/mnt/subvol2')
  938. flexmock(module.glob).should_receive('glob').with_args(
  939. '/mnt/subvol1/.borgmatic-*/mnt/subvol1',
  940. ).and_return(
  941. (
  942. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  943. '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
  944. ),
  945. )
  946. flexmock(module.glob).should_receive('glob').with_args(
  947. '/mnt/subvol2/.borgmatic-*/mnt/subvol2',
  948. ).and_return(
  949. (
  950. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
  951. '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
  952. ),
  953. )
  954. flexmock(module.os.path).should_receive('isdir').with_args(
  955. '/mnt/subvol1/.borgmatic-snapshot-1234/mnt/subvol1',
  956. ).and_return(True)
  957. flexmock(module.os.path).should_receive('isdir').with_args(
  958. '/mnt/subvol1/.borgmatic-snapshot-5678/mnt/subvol1',
  959. ).and_return(True)
  960. flexmock(module.os.path).should_receive('isdir').with_args(
  961. '/mnt/subvol2/.borgmatic-snapshot-1234/mnt/subvol2',
  962. ).and_return(True)
  963. flexmock(module.os.path).should_receive('isdir').with_args(
  964. '/mnt/subvol2/.borgmatic-snapshot-5678/mnt/subvol2',
  965. ).and_return(False)
  966. flexmock(module).should_receive('delete_snapshot').and_raise(
  967. module.subprocess.CalledProcessError(1, 'command', 'error'),
  968. )
  969. flexmock(module.shutil).should_receive('rmtree').never()
  970. module.remove_data_source_dumps(
  971. hook_config=config['btrfs'],
  972. config=config,
  973. borgmatic_runtime_directory='/run/borgmatic',
  974. patterns=flexmock(),
  975. dry_run=False,
  976. )
  977. def test_remove_data_source_dumps_with_root_subvolume_skips_duplicate_removal():
  978. config = {'btrfs': {}}
  979. flexmock(module).should_receive('get_subvolumes').and_return(
  980. (module.Subvolume('/', contained_patterns=(Pattern('/etc'),)),),
  981. )
  982. flexmock(module).should_receive('make_snapshot_path').with_args('/').and_return(
  983. '/.borgmatic-snapshot-1234',
  984. )
  985. flexmock(module.borgmatic.config.paths).should_receive(
  986. 'replace_temporary_subdirectory_with_glob',
  987. ).with_args(
  988. '/.borgmatic-snapshot-1234',
  989. temporary_directory_prefix=module.BORGMATIC_SNAPSHOT_PREFIX,
  990. ).and_return('/.borgmatic-*')
  991. flexmock(module.glob).should_receive('glob').with_args('/.borgmatic-*').and_return(
  992. ('/.borgmatic-snapshot-1234', '/.borgmatic-snapshot-5678'),
  993. )
  994. flexmock(module.os.path).should_receive('isdir').with_args(
  995. '/.borgmatic-snapshot-1234'
  996. ).and_return(
  997. True,
  998. ).and_return(False)
  999. flexmock(module.os.path).should_receive('isdir').with_args(
  1000. '/.borgmatic-snapshot-5678'
  1001. ).and_return(
  1002. True,
  1003. ).and_return(False)
  1004. flexmock(module).should_receive('delete_snapshot').with_args(
  1005. 'btrfs', '/.borgmatic-snapshot-1234'
  1006. ).once()
  1007. flexmock(module).should_receive('delete_snapshot').with_args(
  1008. 'btrfs', '/.borgmatic-snapshot-5678'
  1009. ).once()
  1010. flexmock(module.os.path).should_receive('isdir').with_args('').and_return(False)
  1011. flexmock(module.shutil).should_receive('rmtree').never()
  1012. module.remove_data_source_dumps(
  1013. hook_config=config['btrfs'],
  1014. config=config,
  1015. borgmatic_runtime_directory='/run/borgmatic',
  1016. patterns=flexmock(),
  1017. dry_run=False,
  1018. )