test_lvm.py 39 KB

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