test_zfs.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. import os
  2. import pytest
  3. from flexmock import flexmock
  4. from borgmatic.hooks.data_source import zfs as module
  5. def test_get_datasets_to_backup_filters_datasets_by_source_directories():
  6. flexmock(module.borgmatic.execute).should_receive(
  7. 'execute_command_and_capture_output'
  8. ).and_return(
  9. 'dataset\t/dataset\t-\nother\t/other\t-',
  10. )
  11. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  12. 'get_contained_directories'
  13. ).with_args('/dataset', object).and_return(('/dataset',))
  14. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  15. 'get_contained_directories'
  16. ).with_args('/other', object).and_return(())
  17. assert module.get_datasets_to_backup(
  18. 'zfs', source_directories=('/foo', '/dataset', '/bar')
  19. ) == (
  20. module.Dataset(
  21. name='dataset', mount_point='/dataset', contained_source_directories=('/dataset',)
  22. ),
  23. )
  24. def test_get_datasets_to_backup_filters_datasets_by_user_property():
  25. flexmock(module.borgmatic.execute).should_receive(
  26. 'execute_command_and_capture_output'
  27. ).and_return(
  28. 'dataset\t/dataset\tauto\nother\t/other\t-',
  29. )
  30. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  31. 'get_contained_directories'
  32. ).with_args('/dataset', object).never()
  33. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  34. 'get_contained_directories'
  35. ).with_args('/other', object).and_return(())
  36. assert module.get_datasets_to_backup('zfs', source_directories=('/foo', '/bar')) == (
  37. module.Dataset(
  38. name='dataset',
  39. mount_point='/dataset',
  40. auto_backup=True,
  41. contained_source_directories=('/dataset',),
  42. ),
  43. )
  44. def test_get_datasets_to_backup_with_invalid_list_output_raises():
  45. flexmock(module.borgmatic.execute).should_receive(
  46. 'execute_command_and_capture_output'
  47. ).and_return(
  48. 'dataset',
  49. )
  50. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  51. 'get_contained_directories'
  52. ).never()
  53. with pytest.raises(ValueError, match='zfs'):
  54. module.get_datasets_to_backup('zfs', source_directories=('/foo', '/bar'))
  55. def test_get_all_dataset_mount_points_does_not_filter_datasets():
  56. flexmock(module.borgmatic.execute).should_receive(
  57. 'execute_command_and_capture_output'
  58. ).and_return(
  59. '/dataset\n/other',
  60. )
  61. flexmock(module.borgmatic.hooks.data_source.snapshot).should_receive(
  62. 'get_contained_directories'
  63. ).and_return(('/dataset',))
  64. assert module.get_all_dataset_mount_points('zfs') == (
  65. ('/dataset'),
  66. ('/other'),
  67. )
  68. def test_dump_data_sources_snapshots_and_mounts_and_updates_source_directories():
  69. flexmock(module).should_receive('get_datasets_to_backup').and_return(
  70. (
  71. flexmock(
  72. name='dataset',
  73. mount_point='/mnt/dataset',
  74. contained_source_directories=('/mnt/dataset/subdir',),
  75. )
  76. )
  77. )
  78. flexmock(module.os).should_receive('getpid').and_return(1234)
  79. full_snapshot_name = 'dataset@borgmatic-1234'
  80. flexmock(module).should_receive('snapshot_dataset').with_args(
  81. 'zfs',
  82. full_snapshot_name,
  83. ).once()
  84. snapshot_mount_path = '/run/borgmatic/zfs_snapshots/./mnt/dataset'
  85. flexmock(module).should_receive('mount_snapshot').with_args(
  86. 'mount',
  87. full_snapshot_name,
  88. module.os.path.normpath(snapshot_mount_path),
  89. ).once()
  90. source_directories = ['/mnt/dataset/subdir']
  91. assert (
  92. module.dump_data_sources(
  93. hook_config={},
  94. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  95. log_prefix='test',
  96. config_paths=('test.yaml',),
  97. borgmatic_runtime_directory='/run/borgmatic',
  98. source_directories=source_directories,
  99. dry_run=False,
  100. )
  101. == []
  102. )
  103. assert source_directories == [os.path.join(snapshot_mount_path, 'subdir')]
  104. def test_dump_data_sources_with_no_datasets_skips_snapshots():
  105. flexmock(module).should_receive('get_datasets_to_backup').and_return(())
  106. flexmock(module.os).should_receive('getpid').and_return(1234)
  107. flexmock(module).should_receive('snapshot_dataset').never()
  108. flexmock(module).should_receive('mount_snapshot').never()
  109. source_directories = ['/mnt/dataset']
  110. assert (
  111. module.dump_data_sources(
  112. hook_config={},
  113. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  114. log_prefix='test',
  115. config_paths=('test.yaml',),
  116. borgmatic_runtime_directory='/run/borgmatic',
  117. source_directories=source_directories,
  118. dry_run=False,
  119. )
  120. == []
  121. )
  122. assert source_directories == ['/mnt/dataset']
  123. def test_dump_data_sources_uses_custom_commands():
  124. flexmock(module).should_receive('get_datasets_to_backup').and_return(
  125. (
  126. flexmock(
  127. name='dataset',
  128. mount_point='/mnt/dataset',
  129. contained_source_directories=('/mnt/dataset/subdir',),
  130. )
  131. )
  132. )
  133. flexmock(module.os).should_receive('getpid').and_return(1234)
  134. full_snapshot_name = 'dataset@borgmatic-1234'
  135. flexmock(module).should_receive('snapshot_dataset').with_args(
  136. '/usr/local/bin/zfs',
  137. full_snapshot_name,
  138. ).once()
  139. snapshot_mount_path = '/run/borgmatic/zfs_snapshots/./mnt/dataset'
  140. flexmock(module).should_receive('mount_snapshot').with_args(
  141. '/usr/local/bin/mount',
  142. full_snapshot_name,
  143. module.os.path.normpath(snapshot_mount_path),
  144. ).once()
  145. source_directories = ['/mnt/dataset/subdir']
  146. hook_config = {
  147. 'zfs_command': '/usr/local/bin/zfs',
  148. 'mount_command': '/usr/local/bin/mount',
  149. }
  150. assert (
  151. module.dump_data_sources(
  152. hook_config=hook_config,
  153. config={
  154. 'source_directories': source_directories,
  155. 'zfs': hook_config,
  156. },
  157. log_prefix='test',
  158. config_paths=('test.yaml',),
  159. borgmatic_runtime_directory='/run/borgmatic',
  160. source_directories=source_directories,
  161. dry_run=False,
  162. )
  163. == []
  164. )
  165. assert source_directories == [os.path.join(snapshot_mount_path, 'subdir')]
  166. def test_dump_data_sources_with_dry_run_skips_commands_and_does_not_touch_source_directories():
  167. flexmock(module).should_receive('get_datasets_to_backup').and_return(
  168. (flexmock(name='dataset', mount_point='/mnt/dataset'),)
  169. )
  170. flexmock(module.os).should_receive('getpid').and_return(1234)
  171. flexmock(module).should_receive('snapshot_dataset').never()
  172. flexmock(module).should_receive('mount_snapshot').never()
  173. source_directories = ['/mnt/dataset']
  174. assert (
  175. module.dump_data_sources(
  176. hook_config={},
  177. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  178. log_prefix='test',
  179. config_paths=('test.yaml',),
  180. borgmatic_runtime_directory='/run/borgmatic',
  181. source_directories=source_directories,
  182. dry_run=True,
  183. )
  184. == []
  185. )
  186. assert source_directories == ['/mnt/dataset']
  187. def test_dump_data_sources_ignores_mismatch_between_source_directories_and_contained_source_directories():
  188. flexmock(module).should_receive('get_datasets_to_backup').and_return(
  189. (
  190. flexmock(
  191. name='dataset',
  192. mount_point='/mnt/dataset',
  193. contained_source_directories=('/mnt/dataset/subdir',),
  194. )
  195. )
  196. )
  197. flexmock(module.os).should_receive('getpid').and_return(1234)
  198. full_snapshot_name = 'dataset@borgmatic-1234'
  199. flexmock(module).should_receive('snapshot_dataset').with_args(
  200. 'zfs',
  201. full_snapshot_name,
  202. ).once()
  203. snapshot_mount_path = '/run/borgmatic/zfs_snapshots/./mnt/dataset'
  204. flexmock(module).should_receive('mount_snapshot').with_args(
  205. 'mount',
  206. full_snapshot_name,
  207. module.os.path.normpath(snapshot_mount_path),
  208. ).once()
  209. source_directories = ['/hmm']
  210. assert (
  211. module.dump_data_sources(
  212. hook_config={},
  213. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  214. log_prefix='test',
  215. config_paths=('test.yaml',),
  216. borgmatic_runtime_directory='/run/borgmatic',
  217. source_directories=source_directories,
  218. dry_run=False,
  219. )
  220. == []
  221. )
  222. assert source_directories == ['/hmm', os.path.join(snapshot_mount_path, 'subdir')]
  223. def test_get_all_snapshots_parses_list_output():
  224. flexmock(module.borgmatic.execute).should_receive(
  225. 'execute_command_and_capture_output'
  226. ).and_return(
  227. 'dataset1@borgmatic-1234\ndataset2@borgmatic-4567',
  228. )
  229. assert module.get_all_snapshots('zfs') == ('dataset1@borgmatic-1234', 'dataset2@borgmatic-4567')
  230. def test_remove_data_source_dumps_unmounts_and_destroys_snapshots():
  231. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  232. flexmock(module.borgmatic.config.paths).should_receive(
  233. 'replace_temporary_subdirectory_with_glob'
  234. ).and_return('/run/borgmatic')
  235. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  236. flexmock(module.os.path).should_receive('isdir').and_return(True)
  237. flexmock(module.shutil).should_receive('rmtree')
  238. flexmock(module).should_receive('unmount_snapshot').with_args(
  239. 'umount', '/run/borgmatic/zfs_snapshots/mnt/dataset'
  240. ).once()
  241. flexmock(module).should_receive('get_all_snapshots').and_return(
  242. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
  243. )
  244. flexmock(module).should_receive('destroy_snapshot').with_args(
  245. 'zfs', 'dataset@borgmatic-1234'
  246. ).once()
  247. module.remove_data_source_dumps(
  248. hook_config={},
  249. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  250. log_prefix='test',
  251. borgmatic_runtime_directory='/run/borgmatic',
  252. dry_run=False,
  253. )
  254. def test_remove_data_source_dumps_use_custom_commands():
  255. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  256. flexmock(module.borgmatic.config.paths).should_receive(
  257. 'replace_temporary_subdirectory_with_glob'
  258. ).and_return('/run/borgmatic')
  259. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  260. flexmock(module.os.path).should_receive('isdir').and_return(True)
  261. flexmock(module.shutil).should_receive('rmtree')
  262. flexmock(module).should_receive('unmount_snapshot').with_args(
  263. '/usr/local/bin/umount', '/run/borgmatic/zfs_snapshots/mnt/dataset'
  264. ).once()
  265. flexmock(module).should_receive('get_all_snapshots').and_return(
  266. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
  267. )
  268. flexmock(module).should_receive('destroy_snapshot').with_args(
  269. '/usr/local/bin/zfs', 'dataset@borgmatic-1234'
  270. ).once()
  271. hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
  272. module.remove_data_source_dumps(
  273. hook_config=hook_config,
  274. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  275. log_prefix='test',
  276. borgmatic_runtime_directory='/run/borgmatic',
  277. dry_run=False,
  278. )
  279. def test_remove_data_source_dumps_bails_for_missing_hook_configuration():
  280. flexmock(module).should_receive('get_all_dataset_mount_points').never()
  281. flexmock(module.borgmatic.config.paths).should_receive(
  282. 'replace_temporary_subdirectory_with_glob'
  283. ).never()
  284. module.remove_data_source_dumps(
  285. hook_config=None,
  286. config={'source_directories': '/mnt/dataset'},
  287. log_prefix='test',
  288. borgmatic_runtime_directory='/run/borgmatic',
  289. dry_run=False,
  290. )
  291. def test_remove_data_source_dumps_bails_for_missing_zfs_command():
  292. flexmock(module).should_receive('get_all_dataset_mount_points').and_raise(FileNotFoundError)
  293. flexmock(module.borgmatic.config.paths).should_receive(
  294. 'replace_temporary_subdirectory_with_glob'
  295. ).never()
  296. hook_config = {'zfs_command': 'wtf'}
  297. module.remove_data_source_dumps(
  298. hook_config=hook_config,
  299. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  300. log_prefix='test',
  301. borgmatic_runtime_directory='/run/borgmatic',
  302. dry_run=False,
  303. )
  304. def test_remove_data_source_dumps_bails_for_zfs_command_error():
  305. flexmock(module).should_receive('get_all_dataset_mount_points').and_raise(
  306. module.subprocess.CalledProcessError(1, 'wtf')
  307. )
  308. flexmock(module.borgmatic.config.paths).should_receive(
  309. 'replace_temporary_subdirectory_with_glob'
  310. ).never()
  311. hook_config = {'zfs_command': 'wtf'}
  312. module.remove_data_source_dumps(
  313. hook_config=hook_config,
  314. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  315. log_prefix='test',
  316. borgmatic_runtime_directory='/run/borgmatic',
  317. dry_run=False,
  318. )
  319. def test_remove_data_source_dumps_bails_for_missing_umount_command():
  320. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  321. flexmock(module.borgmatic.config.paths).should_receive(
  322. 'replace_temporary_subdirectory_with_glob'
  323. ).and_return('/run/borgmatic')
  324. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  325. flexmock(module.os.path).should_receive('isdir').and_return(True)
  326. flexmock(module.shutil).should_receive('rmtree')
  327. flexmock(module).should_receive('unmount_snapshot').with_args(
  328. '/usr/local/bin/umount', '/run/borgmatic/zfs_snapshots/mnt/dataset'
  329. ).and_raise(FileNotFoundError)
  330. flexmock(module).should_receive('get_all_snapshots').never()
  331. flexmock(module).should_receive('destroy_snapshot').never()
  332. hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
  333. module.remove_data_source_dumps(
  334. hook_config=hook_config,
  335. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  336. log_prefix='test',
  337. borgmatic_runtime_directory='/run/borgmatic',
  338. dry_run=False,
  339. )
  340. def test_remove_data_source_dumps_bails_for_umount_command_error():
  341. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  342. flexmock(module.borgmatic.config.paths).should_receive(
  343. 'replace_temporary_subdirectory_with_glob'
  344. ).and_return('/run/borgmatic')
  345. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  346. flexmock(module.os.path).should_receive('isdir').and_return(True)
  347. flexmock(module.shutil).should_receive('rmtree')
  348. flexmock(module).should_receive('unmount_snapshot').with_args(
  349. '/usr/local/bin/umount', '/run/borgmatic/zfs_snapshots/mnt/dataset'
  350. ).and_raise(module.subprocess.CalledProcessError(1, 'wtf'))
  351. flexmock(module).should_receive('get_all_snapshots').never()
  352. flexmock(module).should_receive('destroy_snapshot').never()
  353. hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
  354. module.remove_data_source_dumps(
  355. hook_config=hook_config,
  356. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  357. log_prefix='test',
  358. borgmatic_runtime_directory='/run/borgmatic',
  359. dry_run=False,
  360. )
  361. def test_remove_data_source_dumps_skips_unmount_snapshot_directories_that_are_not_actually_directories():
  362. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  363. flexmock(module.borgmatic.config.paths).should_receive(
  364. 'replace_temporary_subdirectory_with_glob'
  365. ).and_return('/run/borgmatic')
  366. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  367. flexmock(module.os.path).should_receive('isdir').and_return(False)
  368. flexmock(module.shutil).should_receive('rmtree').never()
  369. flexmock(module).should_receive('unmount_snapshot').never()
  370. flexmock(module).should_receive('get_all_snapshots').and_return(
  371. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
  372. )
  373. flexmock(module).should_receive('destroy_snapshot').with_args(
  374. 'zfs', 'dataset@borgmatic-1234'
  375. ).once()
  376. module.remove_data_source_dumps(
  377. hook_config={},
  378. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  379. log_prefix='test',
  380. borgmatic_runtime_directory='/run/borgmatic',
  381. dry_run=False,
  382. )
  383. def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_that_are_not_actually_directories():
  384. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  385. flexmock(module.borgmatic.config.paths).should_receive(
  386. 'replace_temporary_subdirectory_with_glob'
  387. ).and_return('/run/borgmatic')
  388. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  389. flexmock(module.os.path).should_receive('isdir').with_args(
  390. '/run/borgmatic/zfs_snapshots'
  391. ).and_return(True)
  392. flexmock(module.os.path).should_receive('isdir').with_args(
  393. '/run/borgmatic/zfs_snapshots/mnt/dataset'
  394. ).and_return(False)
  395. flexmock(module.shutil).should_receive('rmtree')
  396. flexmock(module).should_receive('unmount_snapshot').never()
  397. flexmock(module).should_receive('get_all_snapshots').and_return(
  398. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
  399. )
  400. flexmock(module).should_receive('destroy_snapshot').with_args(
  401. 'zfs', 'dataset@borgmatic-1234'
  402. ).once()
  403. module.remove_data_source_dumps(
  404. hook_config={},
  405. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  406. log_prefix='test',
  407. borgmatic_runtime_directory='/run/borgmatic',
  408. dry_run=False,
  409. )
  410. def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_after_rmtree_succeeds():
  411. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  412. flexmock(module.borgmatic.config.paths).should_receive(
  413. 'replace_temporary_subdirectory_with_glob'
  414. ).and_return('/run/borgmatic')
  415. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  416. flexmock(module.os.path).should_receive('isdir').with_args(
  417. '/run/borgmatic/zfs_snapshots'
  418. ).and_return(True)
  419. flexmock(module.os.path).should_receive('isdir').with_args(
  420. '/run/borgmatic/zfs_snapshots/mnt/dataset'
  421. ).and_return(True).and_return(False)
  422. flexmock(module.shutil).should_receive('rmtree')
  423. flexmock(module).should_receive('unmount_snapshot').never()
  424. flexmock(module).should_receive('get_all_snapshots').and_return(
  425. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
  426. )
  427. flexmock(module).should_receive('destroy_snapshot').with_args(
  428. 'zfs', 'dataset@borgmatic-1234'
  429. ).once()
  430. module.remove_data_source_dumps(
  431. hook_config={},
  432. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  433. log_prefix='test',
  434. borgmatic_runtime_directory='/run/borgmatic',
  435. dry_run=False,
  436. )
  437. def test_remove_data_source_dumps_with_dry_run_skips_unmount_and_destroy():
  438. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  439. flexmock(module.borgmatic.config.paths).should_receive(
  440. 'replace_temporary_subdirectory_with_glob'
  441. ).and_return('/run/borgmatic')
  442. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  443. flexmock(module.os.path).should_receive('isdir').and_return(True)
  444. flexmock(module.shutil).should_receive('rmtree').never()
  445. flexmock(module).should_receive('unmount_snapshot').never()
  446. flexmock(module).should_receive('get_all_snapshots').and_return(
  447. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
  448. )
  449. flexmock(module).should_receive('destroy_snapshot').never()
  450. module.remove_data_source_dumps(
  451. hook_config={},
  452. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  453. log_prefix='test',
  454. borgmatic_runtime_directory='/run/borgmatic',
  455. dry_run=True,
  456. )