test_zfs.py 20 KB

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