test_zfs.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  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_zfs_command():
  280. flexmock(module).should_receive('get_all_dataset_mount_points').and_raise(FileNotFoundError)
  281. flexmock(module.borgmatic.config.paths).should_receive(
  282. 'replace_temporary_subdirectory_with_glob'
  283. ).never()
  284. hook_config = {'zfs_command': 'wtf'}
  285. module.remove_data_source_dumps(
  286. hook_config=hook_config,
  287. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  288. log_prefix='test',
  289. borgmatic_runtime_directory='/run/borgmatic',
  290. dry_run=False,
  291. )
  292. def test_remove_data_source_dumps_bails_for_zfs_command_error():
  293. flexmock(module).should_receive('get_all_dataset_mount_points').and_raise(
  294. module.subprocess.CalledProcessError(1, 'wtf')
  295. )
  296. flexmock(module.borgmatic.config.paths).should_receive(
  297. 'replace_temporary_subdirectory_with_glob'
  298. ).never()
  299. hook_config = {'zfs_command': 'wtf'}
  300. module.remove_data_source_dumps(
  301. hook_config=hook_config,
  302. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  303. log_prefix='test',
  304. borgmatic_runtime_directory='/run/borgmatic',
  305. dry_run=False,
  306. )
  307. def test_remove_data_source_dumps_bails_for_missing_umount_command():
  308. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  309. flexmock(module.borgmatic.config.paths).should_receive(
  310. 'replace_temporary_subdirectory_with_glob'
  311. ).and_return('/run/borgmatic')
  312. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  313. flexmock(module.os.path).should_receive('isdir').and_return(True)
  314. flexmock(module.shutil).should_receive('rmtree')
  315. flexmock(module).should_receive('unmount_snapshot').with_args(
  316. '/usr/local/bin/umount', '/run/borgmatic/zfs_snapshots/mnt/dataset'
  317. ).and_raise(FileNotFoundError)
  318. flexmock(module).should_receive('get_all_snapshots').never()
  319. flexmock(module).should_receive('destroy_snapshot').never()
  320. hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
  321. module.remove_data_source_dumps(
  322. hook_config=hook_config,
  323. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  324. log_prefix='test',
  325. borgmatic_runtime_directory='/run/borgmatic',
  326. dry_run=False,
  327. )
  328. def test_remove_data_source_dumps_bails_for_umount_command_error():
  329. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  330. flexmock(module.borgmatic.config.paths).should_receive(
  331. 'replace_temporary_subdirectory_with_glob'
  332. ).and_return('/run/borgmatic')
  333. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  334. flexmock(module.os.path).should_receive('isdir').and_return(True)
  335. flexmock(module.shutil).should_receive('rmtree')
  336. flexmock(module).should_receive('unmount_snapshot').with_args(
  337. '/usr/local/bin/umount', '/run/borgmatic/zfs_snapshots/mnt/dataset'
  338. ).and_raise(module.subprocess.CalledProcessError(1, 'wtf'))
  339. flexmock(module).should_receive('get_all_snapshots').never()
  340. flexmock(module).should_receive('destroy_snapshot').never()
  341. hook_config = {'zfs_command': '/usr/local/bin/zfs', 'umount_command': '/usr/local/bin/umount'}
  342. module.remove_data_source_dumps(
  343. hook_config=hook_config,
  344. config={'source_directories': '/mnt/dataset', 'zfs': hook_config},
  345. log_prefix='test',
  346. borgmatic_runtime_directory='/run/borgmatic',
  347. dry_run=False,
  348. )
  349. def test_remove_data_source_dumps_skips_unmount_snapshot_directories_that_are_not_actually_directories():
  350. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  351. flexmock(module.borgmatic.config.paths).should_receive(
  352. 'replace_temporary_subdirectory_with_glob'
  353. ).and_return('/run/borgmatic')
  354. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  355. flexmock(module.os.path).should_receive('isdir').and_return(False)
  356. flexmock(module.shutil).should_receive('rmtree').never()
  357. flexmock(module).should_receive('unmount_snapshot').never()
  358. flexmock(module).should_receive('get_all_snapshots').and_return(
  359. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
  360. )
  361. flexmock(module).should_receive('destroy_snapshot').with_args(
  362. 'zfs', 'dataset@borgmatic-1234'
  363. ).once()
  364. module.remove_data_source_dumps(
  365. hook_config={},
  366. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  367. log_prefix='test',
  368. borgmatic_runtime_directory='/run/borgmatic',
  369. dry_run=False,
  370. )
  371. def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_that_are_not_actually_directories():
  372. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  373. flexmock(module.borgmatic.config.paths).should_receive(
  374. 'replace_temporary_subdirectory_with_glob'
  375. ).and_return('/run/borgmatic')
  376. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  377. flexmock(module.os.path).should_receive('isdir').with_args(
  378. '/run/borgmatic/zfs_snapshots'
  379. ).and_return(True)
  380. flexmock(module.os.path).should_receive('isdir').with_args(
  381. '/run/borgmatic/zfs_snapshots/mnt/dataset'
  382. ).and_return(False)
  383. flexmock(module.shutil).should_receive('rmtree')
  384. flexmock(module).should_receive('unmount_snapshot').never()
  385. flexmock(module).should_receive('get_all_snapshots').and_return(
  386. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
  387. )
  388. flexmock(module).should_receive('destroy_snapshot').with_args(
  389. 'zfs', 'dataset@borgmatic-1234'
  390. ).once()
  391. module.remove_data_source_dumps(
  392. hook_config={},
  393. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  394. log_prefix='test',
  395. borgmatic_runtime_directory='/run/borgmatic',
  396. dry_run=False,
  397. )
  398. def test_remove_data_source_dumps_skips_unmount_snapshot_mount_paths_after_rmtree_succeeds():
  399. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  400. flexmock(module.borgmatic.config.paths).should_receive(
  401. 'replace_temporary_subdirectory_with_glob'
  402. ).and_return('/run/borgmatic')
  403. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  404. flexmock(module.os.path).should_receive('isdir').with_args(
  405. '/run/borgmatic/zfs_snapshots'
  406. ).and_return(True)
  407. flexmock(module.os.path).should_receive('isdir').with_args(
  408. '/run/borgmatic/zfs_snapshots/mnt/dataset'
  409. ).and_return(True).and_return(False)
  410. flexmock(module.shutil).should_receive('rmtree')
  411. flexmock(module).should_receive('unmount_snapshot').never()
  412. flexmock(module).should_receive('get_all_snapshots').and_return(
  413. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
  414. )
  415. flexmock(module).should_receive('destroy_snapshot').with_args(
  416. 'zfs', 'dataset@borgmatic-1234'
  417. ).once()
  418. module.remove_data_source_dumps(
  419. hook_config={},
  420. config={'source_directories': '/mnt/dataset', 'zfs': {}},
  421. log_prefix='test',
  422. borgmatic_runtime_directory='/run/borgmatic',
  423. dry_run=False,
  424. )
  425. def test_remove_data_source_dumps_with_dry_run_skips_unmount_and_destroy():
  426. flexmock(module).should_receive('get_all_dataset_mount_points').and_return(('/mnt/dataset',))
  427. flexmock(module.borgmatic.config.paths).should_receive(
  428. 'replace_temporary_subdirectory_with_glob'
  429. ).and_return('/run/borgmatic')
  430. flexmock(module.glob).should_receive('glob').replace_with(lambda path: [path])
  431. flexmock(module.os.path).should_receive('isdir').and_return(True)
  432. flexmock(module.shutil).should_receive('rmtree').never()
  433. flexmock(module).should_receive('unmount_snapshot').never()
  434. flexmock(module).should_receive('get_all_snapshots').and_return(
  435. ('dataset@borgmatic-1234', 'dataset@other', 'other@other', 'invalid')
  436. )
  437. flexmock(module).should_receive('destroy_snapshot').never()
  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=True,
  444. )