test_lvm.py 39 KB

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