test_lvm.py 39 KB

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