test_lvm.py 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110
  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_lvm_configuration():
  612. flexmock(module).should_receive('get_logical_volumes').never()
  613. flexmock(module.borgmatic.config.paths).should_receive(
  614. 'replace_temporary_subdirectory_with_glob'
  615. ).never()
  616. flexmock(module).should_receive('unmount_snapshot').never()
  617. flexmock(module).should_receive('remove_snapshot').never()
  618. module.remove_data_source_dumps(
  619. hook_config=None,
  620. config={'source_directories': '/mnt/lvolume'},
  621. log_prefix='test',
  622. borgmatic_runtime_directory='/run/borgmatic',
  623. dry_run=False,
  624. )
  625. def test_remove_data_source_dumps_bails_for_missing_lsblk_command():
  626. config = {'lvm': {}}
  627. flexmock(module).should_receive('get_logical_volumes').and_raise(FileNotFoundError)
  628. flexmock(module.borgmatic.config.paths).should_receive(
  629. 'replace_temporary_subdirectory_with_glob'
  630. ).never()
  631. flexmock(module).should_receive('unmount_snapshot').never()
  632. flexmock(module).should_receive('remove_snapshot').never()
  633. module.remove_data_source_dumps(
  634. hook_config=config['lvm'],
  635. config=config,
  636. log_prefix='test',
  637. borgmatic_runtime_directory='/run/borgmatic',
  638. dry_run=False,
  639. )
  640. def test_remove_data_source_dumps_bails_for_lsblk_command_error():
  641. config = {'lvm': {}}
  642. flexmock(module).should_receive('get_logical_volumes').and_raise(
  643. module.subprocess.CalledProcessError(1, 'wtf')
  644. )
  645. flexmock(module.borgmatic.config.paths).should_receive(
  646. 'replace_temporary_subdirectory_with_glob'
  647. ).never()
  648. flexmock(module).should_receive('unmount_snapshot').never()
  649. flexmock(module).should_receive('remove_snapshot').never()
  650. module.remove_data_source_dumps(
  651. hook_config=config['lvm'],
  652. config=config,
  653. log_prefix='test',
  654. borgmatic_runtime_directory='/run/borgmatic',
  655. dry_run=False,
  656. )
  657. def test_remove_data_source_dumps_with_missing_snapshot_directory_skips_unmount():
  658. config = {'lvm': {}}
  659. flexmock(module).should_receive('get_logical_volumes').and_return(
  660. (
  661. module.Logical_volume(
  662. name='lvolume1',
  663. device_path='/dev/lvolume1',
  664. mount_point='/mnt/lvolume1',
  665. contained_source_directories=('/mnt/lvolume1/subdir',),
  666. ),
  667. module.Logical_volume(
  668. name='lvolume2',
  669. device_path='/dev/lvolume2',
  670. mount_point='/mnt/lvolume2',
  671. contained_source_directories=('/mnt/lvolume2',),
  672. ),
  673. )
  674. )
  675. flexmock(module.borgmatic.config.paths).should_receive(
  676. 'replace_temporary_subdirectory_with_glob'
  677. ).and_return('/run/borgmatic')
  678. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  679. flexmock(module.os.path).should_receive('isdir').with_args(
  680. '/run/borgmatic/lvm_snapshots'
  681. ).and_return(False)
  682. flexmock(module.shutil).should_receive('rmtree').never()
  683. flexmock(module).should_receive('unmount_snapshot').never()
  684. flexmock(module).should_receive('get_snapshots').and_return(
  685. (
  686. module.Snapshot('lvolume1_borgmatic-1234', '/dev/lvolume1'),
  687. module.Snapshot('lvolume2_borgmatic-1234', '/dev/lvolume2'),
  688. ),
  689. )
  690. flexmock(module).should_receive('remove_snapshot').with_args('lvremove', '/dev/lvolume1').once()
  691. flexmock(module).should_receive('remove_snapshot').with_args('lvremove', '/dev/lvolume2').once()
  692. module.remove_data_source_dumps(
  693. hook_config=config['lvm'],
  694. config=config,
  695. log_prefix='test',
  696. borgmatic_runtime_directory='/run/borgmatic',
  697. dry_run=False,
  698. )
  699. def test_remove_data_source_dumps_with_missing_snapshot_mount_path_skips_unmount():
  700. config = {'lvm': {}}
  701. flexmock(module).should_receive('get_logical_volumes').and_return(
  702. (
  703. module.Logical_volume(
  704. name='lvolume1',
  705. device_path='/dev/lvolume1',
  706. mount_point='/mnt/lvolume1',
  707. contained_source_directories=('/mnt/lvolume1/subdir',),
  708. ),
  709. module.Logical_volume(
  710. name='lvolume2',
  711. device_path='/dev/lvolume2',
  712. mount_point='/mnt/lvolume2',
  713. contained_source_directories=('/mnt/lvolume2',),
  714. ),
  715. )
  716. )
  717. flexmock(module.borgmatic.config.paths).should_receive(
  718. 'replace_temporary_subdirectory_with_glob'
  719. ).and_return('/run/borgmatic')
  720. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  721. flexmock(module.os.path).should_receive('isdir').with_args(
  722. '/run/borgmatic/lvm_snapshots'
  723. ).and_return(True)
  724. flexmock(module.os.path).should_receive('isdir').with_args(
  725. '/run/borgmatic/lvm_snapshots/mnt/lvolume1'
  726. ).and_return(False)
  727. flexmock(module.os.path).should_receive('isdir').with_args(
  728. '/run/borgmatic/lvm_snapshots/mnt/lvolume2'
  729. ).and_return(True)
  730. flexmock(module.shutil).should_receive('rmtree')
  731. flexmock(module).should_receive('unmount_snapshot').with_args(
  732. 'umount',
  733. '/run/borgmatic/lvm_snapshots/mnt/lvolume1',
  734. ).never()
  735. flexmock(module).should_receive('unmount_snapshot').with_args(
  736. 'umount',
  737. '/run/borgmatic/lvm_snapshots/mnt/lvolume2',
  738. ).once()
  739. flexmock(module).should_receive('get_snapshots').and_return(
  740. (
  741. module.Snapshot('lvolume1_borgmatic-1234', '/dev/lvolume1'),
  742. module.Snapshot('lvolume2_borgmatic-1234', '/dev/lvolume2'),
  743. ),
  744. )
  745. flexmock(module).should_receive('remove_snapshot').with_args('lvremove', '/dev/lvolume1').once()
  746. flexmock(module).should_receive('remove_snapshot').with_args('lvremove', '/dev/lvolume2').once()
  747. module.remove_data_source_dumps(
  748. hook_config=config['lvm'],
  749. config=config,
  750. log_prefix='test',
  751. borgmatic_runtime_directory='/run/borgmatic',
  752. dry_run=False,
  753. )
  754. def test_remove_data_source_dumps_with_successful_mount_point_removal_skips_unmount():
  755. config = {'lvm': {}}
  756. flexmock(module).should_receive('get_logical_volumes').and_return(
  757. (
  758. module.Logical_volume(
  759. name='lvolume1',
  760. device_path='/dev/lvolume1',
  761. mount_point='/mnt/lvolume1',
  762. contained_source_directories=('/mnt/lvolume1/subdir',),
  763. ),
  764. module.Logical_volume(
  765. name='lvolume2',
  766. device_path='/dev/lvolume2',
  767. mount_point='/mnt/lvolume2',
  768. contained_source_directories=('/mnt/lvolume2',),
  769. ),
  770. )
  771. )
  772. flexmock(module.borgmatic.config.paths).should_receive(
  773. 'replace_temporary_subdirectory_with_glob'
  774. ).and_return('/run/borgmatic')
  775. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  776. flexmock(module.os.path).should_receive('isdir').with_args(
  777. '/run/borgmatic/lvm_snapshots'
  778. ).and_return(True)
  779. flexmock(module.os.path).should_receive('isdir').with_args(
  780. '/run/borgmatic/lvm_snapshots/mnt/lvolume1'
  781. ).and_return(True).and_return(False)
  782. flexmock(module.os.path).should_receive('isdir').with_args(
  783. '/run/borgmatic/lvm_snapshots/mnt/lvolume2'
  784. ).and_return(True).and_return(True)
  785. flexmock(module.shutil).should_receive('rmtree')
  786. flexmock(module).should_receive('unmount_snapshot').with_args(
  787. 'umount',
  788. '/run/borgmatic/lvm_snapshots/mnt/lvolume1',
  789. ).never()
  790. flexmock(module).should_receive('unmount_snapshot').with_args(
  791. 'umount',
  792. '/run/borgmatic/lvm_snapshots/mnt/lvolume2',
  793. ).once()
  794. flexmock(module).should_receive('get_snapshots').and_return(
  795. (
  796. module.Snapshot('lvolume1_borgmatic-1234', '/dev/lvolume1'),
  797. module.Snapshot('lvolume2_borgmatic-1234', '/dev/lvolume2'),
  798. ),
  799. )
  800. flexmock(module).should_receive('remove_snapshot').with_args('lvremove', '/dev/lvolume1').once()
  801. flexmock(module).should_receive('remove_snapshot').with_args('lvremove', '/dev/lvolume2').once()
  802. module.remove_data_source_dumps(
  803. hook_config=config['lvm'],
  804. config=config,
  805. log_prefix='test',
  806. borgmatic_runtime_directory='/run/borgmatic',
  807. dry_run=False,
  808. )
  809. def test_remove_data_source_dumps_bails_for_missing_umount_command():
  810. config = {'lvm': {}}
  811. flexmock(module).should_receive('get_logical_volumes').and_return(
  812. (
  813. module.Logical_volume(
  814. name='lvolume1',
  815. device_path='/dev/lvolume1',
  816. mount_point='/mnt/lvolume1',
  817. contained_source_directories=('/mnt/lvolume1/subdir',),
  818. ),
  819. module.Logical_volume(
  820. name='lvolume2',
  821. device_path='/dev/lvolume2',
  822. mount_point='/mnt/lvolume2',
  823. contained_source_directories=('/mnt/lvolume2',),
  824. ),
  825. )
  826. )
  827. flexmock(module.borgmatic.config.paths).should_receive(
  828. 'replace_temporary_subdirectory_with_glob'
  829. ).and_return('/run/borgmatic')
  830. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  831. flexmock(module.os.path).should_receive('isdir').and_return(True)
  832. flexmock(module.shutil).should_receive('rmtree')
  833. flexmock(module).should_receive('unmount_snapshot').with_args(
  834. 'umount',
  835. '/run/borgmatic/lvm_snapshots/mnt/lvolume1',
  836. ).and_raise(FileNotFoundError)
  837. flexmock(module).should_receive('unmount_snapshot').with_args(
  838. 'umount',
  839. '/run/borgmatic/lvm_snapshots/mnt/lvolume2',
  840. ).never()
  841. flexmock(module).should_receive('get_snapshots').never()
  842. flexmock(module).should_receive('remove_snapshot').never()
  843. module.remove_data_source_dumps(
  844. hook_config=config['lvm'],
  845. config=config,
  846. log_prefix='test',
  847. borgmatic_runtime_directory='/run/borgmatic',
  848. dry_run=False,
  849. )
  850. def test_remove_data_source_dumps_bails_for_umount_command_error():
  851. config = {'lvm': {}}
  852. flexmock(module).should_receive('get_logical_volumes').and_return(
  853. (
  854. module.Logical_volume(
  855. name='lvolume1',
  856. device_path='/dev/lvolume1',
  857. mount_point='/mnt/lvolume1',
  858. contained_source_directories=('/mnt/lvolume1/subdir',),
  859. ),
  860. module.Logical_volume(
  861. name='lvolume2',
  862. device_path='/dev/lvolume2',
  863. mount_point='/mnt/lvolume2',
  864. contained_source_directories=('/mnt/lvolume2',),
  865. ),
  866. )
  867. )
  868. flexmock(module.borgmatic.config.paths).should_receive(
  869. 'replace_temporary_subdirectory_with_glob'
  870. ).and_return('/run/borgmatic')
  871. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  872. flexmock(module.os.path).should_receive('isdir').and_return(True)
  873. flexmock(module.shutil).should_receive('rmtree')
  874. flexmock(module).should_receive('unmount_snapshot').with_args(
  875. 'umount',
  876. '/run/borgmatic/lvm_snapshots/mnt/lvolume1',
  877. ).and_raise(module.subprocess.CalledProcessError(1, 'wtf'))
  878. flexmock(module).should_receive('unmount_snapshot').with_args(
  879. 'umount',
  880. '/run/borgmatic/lvm_snapshots/mnt/lvolume2',
  881. ).never()
  882. flexmock(module).should_receive('get_snapshots').never()
  883. flexmock(module).should_receive('remove_snapshot').never()
  884. module.remove_data_source_dumps(
  885. hook_config=config['lvm'],
  886. config=config,
  887. log_prefix='test',
  888. borgmatic_runtime_directory='/run/borgmatic',
  889. dry_run=False,
  890. )
  891. def test_remove_data_source_dumps_bails_for_missing_lvs_command():
  892. config = {'lvm': {}}
  893. flexmock(module).should_receive('get_logical_volumes').and_return(
  894. (
  895. module.Logical_volume(
  896. name='lvolume1',
  897. device_path='/dev/lvolume1',
  898. mount_point='/mnt/lvolume1',
  899. contained_source_directories=('/mnt/lvolume1/subdir',),
  900. ),
  901. module.Logical_volume(
  902. name='lvolume2',
  903. device_path='/dev/lvolume2',
  904. mount_point='/mnt/lvolume2',
  905. contained_source_directories=('/mnt/lvolume2',),
  906. ),
  907. )
  908. )
  909. flexmock(module.borgmatic.config.paths).should_receive(
  910. 'replace_temporary_subdirectory_with_glob'
  911. ).and_return('/run/borgmatic')
  912. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  913. flexmock(module.os.path).should_receive('isdir').and_return(True)
  914. flexmock(module.shutil).should_receive('rmtree')
  915. flexmock(module).should_receive('unmount_snapshot').with_args(
  916. 'umount',
  917. '/run/borgmatic/lvm_snapshots/mnt/lvolume1',
  918. ).once()
  919. flexmock(module).should_receive('unmount_snapshot').with_args(
  920. 'umount',
  921. '/run/borgmatic/lvm_snapshots/mnt/lvolume2',
  922. ).once()
  923. flexmock(module).should_receive('get_snapshots').and_raise(FileNotFoundError)
  924. flexmock(module).should_receive('remove_snapshot').never()
  925. module.remove_data_source_dumps(
  926. hook_config=config['lvm'],
  927. config=config,
  928. log_prefix='test',
  929. borgmatic_runtime_directory='/run/borgmatic',
  930. dry_run=False,
  931. )
  932. def test_remove_data_source_dumps_bails_for_lvs_command_error():
  933. config = {'lvm': {}}
  934. flexmock(module).should_receive('get_logical_volumes').and_return(
  935. (
  936. module.Logical_volume(
  937. name='lvolume1',
  938. device_path='/dev/lvolume1',
  939. mount_point='/mnt/lvolume1',
  940. contained_source_directories=('/mnt/lvolume1/subdir',),
  941. ),
  942. module.Logical_volume(
  943. name='lvolume2',
  944. device_path='/dev/lvolume2',
  945. mount_point='/mnt/lvolume2',
  946. contained_source_directories=('/mnt/lvolume2',),
  947. ),
  948. )
  949. )
  950. flexmock(module.borgmatic.config.paths).should_receive(
  951. 'replace_temporary_subdirectory_with_glob'
  952. ).and_return('/run/borgmatic')
  953. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  954. flexmock(module.os.path).should_receive('isdir').and_return(True)
  955. flexmock(module.shutil).should_receive('rmtree')
  956. flexmock(module).should_receive('unmount_snapshot').with_args(
  957. 'umount',
  958. '/run/borgmatic/lvm_snapshots/mnt/lvolume1',
  959. ).once()
  960. flexmock(module).should_receive('unmount_snapshot').with_args(
  961. 'umount',
  962. '/run/borgmatic/lvm_snapshots/mnt/lvolume2',
  963. ).once()
  964. flexmock(module).should_receive('get_snapshots').and_raise(
  965. module.subprocess.CalledProcessError(1, 'wtf')
  966. )
  967. flexmock(module).should_receive('remove_snapshot').never()
  968. module.remove_data_source_dumps(
  969. hook_config=config['lvm'],
  970. config=config,
  971. log_prefix='test',
  972. borgmatic_runtime_directory='/run/borgmatic',
  973. dry_run=False,
  974. )
  975. def test_remove_data_source_with_dry_run_skips_snapshot_unmount_and_delete():
  976. config = {'lvm': {}}
  977. flexmock(module).should_receive('get_logical_volumes').and_return(
  978. (
  979. module.Logical_volume(
  980. name='lvolume1',
  981. device_path='/dev/lvolume1',
  982. mount_point='/mnt/lvolume1',
  983. contained_source_directories=('/mnt/lvolume1/subdir',),
  984. ),
  985. module.Logical_volume(
  986. name='lvolume2',
  987. device_path='/dev/lvolume2',
  988. mount_point='/mnt/lvolume2',
  989. contained_source_directories=('/mnt/lvolume2',),
  990. ),
  991. )
  992. )
  993. flexmock(module.borgmatic.config.paths).should_receive(
  994. 'replace_temporary_subdirectory_with_glob'
  995. ).and_return('/run/borgmatic')
  996. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  997. flexmock(module.os.path).should_receive('isdir').and_return(True)
  998. flexmock(module.shutil).should_receive('rmtree').never()
  999. flexmock(module).should_receive('unmount_snapshot').never()
  1000. flexmock(module).should_receive('get_snapshots').and_return(
  1001. (
  1002. module.Snapshot('lvolume1_borgmatic-1234', '/dev/lvolume1'),
  1003. module.Snapshot('lvolume2_borgmatic-1234', '/dev/lvolume2'),
  1004. module.Snapshot('nonborgmatic', '/dev/nonborgmatic'),
  1005. ),
  1006. ).once()
  1007. flexmock(module).should_receive('remove_snapshot').never()
  1008. module.remove_data_source_dumps(
  1009. hook_config=config['lvm'],
  1010. config=config,
  1011. log_prefix='test',
  1012. borgmatic_runtime_directory='/run/borgmatic',
  1013. dry_run=True,
  1014. )