test_lvm.py 40 KB

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