mount_cmds.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. import errno
  2. import os
  3. import stat
  4. import sys
  5. import pytest
  6. from ... import xattr, platform
  7. from ...constants import * # NOQA
  8. from ...locking import Lock
  9. from ...helpers import flags_noatime, flags_normal
  10. from .. import has_lchflags, llfuse
  11. from .. import changedir, no_selinux, same_ts_ns
  12. from .. import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported
  13. from ..platform import fakeroot_detected
  14. from . import RK_ENCRYPTION, cmd, assert_dirs_equal, create_regular_file, create_src_archive, open_archive
  15. from . import src_file, requires_hardlinks, _extract_hardlinks_setup, fuse_mount, create_test_files
  16. def pytest_generate_tests(metafunc):
  17. # Generate tests for different scenarios: local repository, remote repository, and using the borg binary.
  18. if "archivers" in metafunc.fixturenames:
  19. metafunc.parametrize("archivers", ["archiver", "remote_archiver", "binary_archiver"])
  20. @requires_hardlinks
  21. @pytest.mark.skipif(not llfuse, reason="llfuse not installed")
  22. def test_fuse_mount_hardlinks(archivers, request):
  23. archiver = request.getfixturevalue(archivers)
  24. repo_location = archiver.repository_location
  25. _extract_hardlinks_setup(archiver)
  26. mountpoint = os.path.join(archiver.tmpdir, "mountpoint")
  27. # we need to get rid of permissions checking because fakeroot causes issues with it.
  28. # On all platforms, borg defaults to "default_permissions" and we need to get rid of it via "ignore_permissions".
  29. # On macOS (darwin), we additionally need "defer_permissions" to switch off the checks in osxfuse.
  30. if sys.platform == "darwin":
  31. ignore_perms = ["-o", "ignore_permissions,defer_permissions"]
  32. else:
  33. ignore_perms = ["-o", "ignore_permissions"]
  34. with fuse_mount(repo_location, mountpoint, "-a", "test", "--strip-components=2", *ignore_perms), changedir(
  35. os.path.join(mountpoint, "test")
  36. ):
  37. assert os.stat("hardlink").st_nlink == 2
  38. assert os.stat("subdir/hardlink").st_nlink == 2
  39. assert open("subdir/hardlink", "rb").read() == b"123456"
  40. assert os.stat("aaaa").st_nlink == 2
  41. assert os.stat("source2").st_nlink == 2
  42. with fuse_mount(repo_location, mountpoint, "input/dir1", "-a", "test", *ignore_perms), changedir(
  43. os.path.join(mountpoint, "test")
  44. ):
  45. assert os.stat("input/dir1/hardlink").st_nlink == 2
  46. assert os.stat("input/dir1/subdir/hardlink").st_nlink == 2
  47. assert open("input/dir1/subdir/hardlink", "rb").read() == b"123456"
  48. assert os.stat("input/dir1/aaaa").st_nlink == 2
  49. assert os.stat("input/dir1/source2").st_nlink == 2
  50. with fuse_mount(repo_location, mountpoint, "-a", "test", *ignore_perms), changedir(
  51. os.path.join(mountpoint, "test")
  52. ):
  53. assert os.stat("input/source").st_nlink == 4
  54. assert os.stat("input/abba").st_nlink == 4
  55. assert os.stat("input/dir1/hardlink").st_nlink == 4
  56. assert os.stat("input/dir1/subdir/hardlink").st_nlink == 4
  57. assert open("input/dir1/subdir/hardlink", "rb").read() == b"123456"
  58. @pytest.mark.skipif(not llfuse, reason="llfuse not installed")
  59. def test_fuse(archivers, request):
  60. archiver = request.getfixturevalue(archivers)
  61. if archiver.EXE and fakeroot_detected():
  62. pytest.skip("test_fuse with the binary is not compatible with fakeroot")
  63. repo_location, input_path = archiver.repository_location, archiver.input_path
  64. def has_noatime(some_file):
  65. atime_before = os.stat(some_file).st_atime_ns
  66. try:
  67. os.close(os.open(some_file, flags_noatime))
  68. except PermissionError:
  69. return False
  70. else:
  71. atime_after = os.stat(some_file).st_atime_ns
  72. noatime_used = flags_noatime != flags_normal
  73. return noatime_used and atime_before == atime_after
  74. cmd(archiver, f"--repo={repo_location}", "rcreate", RK_ENCRYPTION)
  75. create_test_files(input_path)
  76. have_noatime = has_noatime("input/file1")
  77. cmd(archiver, f"--repo={repo_location}", "create", "--exclude-nodump", "--atime", "archive", "input")
  78. cmd(archiver, f"--repo={repo_location}", "create", "--exclude-nodump", "--atime", "archive2", "input")
  79. if has_lchflags:
  80. # remove the file that we did not back up, so input and output become equal
  81. os.remove(os.path.join("input", "flagfile"))
  82. mountpoint = os.path.join(archiver.tmpdir, "mountpoint")
  83. # mount the whole repository, archive contents shall show up in archivename subdirectories of mountpoint:
  84. with fuse_mount(repo_location, mountpoint):
  85. # flags are not supported by the FUSE mount
  86. # we also ignore xattrs here, they are tested separately
  87. assert_dirs_equal(
  88. input_path, os.path.join(mountpoint, "archive", "input"), ignore_flags=True, ignore_xattrs=True
  89. )
  90. assert_dirs_equal(
  91. input_path, os.path.join(mountpoint, "archive2", "input"), ignore_flags=True, ignore_xattrs=True
  92. )
  93. with fuse_mount(repo_location, mountpoint, "-a", "archive"):
  94. assert_dirs_equal(
  95. input_path, os.path.join(mountpoint, "archive", "input"), ignore_flags=True, ignore_xattrs=True
  96. )
  97. # regular file
  98. in_fn = "input/file1"
  99. out_fn = os.path.join(mountpoint, "archive", "input", "file1")
  100. # stat
  101. sti1 = os.stat(in_fn)
  102. sto1 = os.stat(out_fn)
  103. assert sti1.st_mode == sto1.st_mode
  104. assert sti1.st_uid == sto1.st_uid
  105. assert sti1.st_gid == sto1.st_gid
  106. assert sti1.st_size == sto1.st_size
  107. if have_noatime:
  108. assert same_ts_ns(sti1.st_atime * 1e9, sto1.st_atime * 1e9)
  109. assert same_ts_ns(sti1.st_ctime * 1e9, sto1.st_ctime * 1e9)
  110. assert same_ts_ns(sti1.st_mtime * 1e9, sto1.st_mtime * 1e9)
  111. if are_hardlinks_supported():
  112. # note: there is another hardlink to this, see below
  113. assert sti1.st_nlink == sto1.st_nlink == 2
  114. # read
  115. with open(in_fn, "rb") as in_f, open(out_fn, "rb") as out_f:
  116. assert in_f.read() == out_f.read()
  117. # hardlink (to 'input/file1')
  118. if are_hardlinks_supported():
  119. in_fn = "input/hardlink"
  120. out_fn = os.path.join(mountpoint, "archive", "input", "hardlink")
  121. sti2 = os.stat(in_fn)
  122. sto2 = os.stat(out_fn)
  123. assert sti2.st_nlink == sto2.st_nlink == 2
  124. assert sto1.st_ino == sto2.st_ino
  125. # symlink
  126. if are_symlinks_supported():
  127. in_fn = "input/link1"
  128. out_fn = os.path.join(mountpoint, "archive", "input", "link1")
  129. sti = os.stat(in_fn, follow_symlinks=False)
  130. sto = os.stat(out_fn, follow_symlinks=False)
  131. assert sti.st_size == len("somewhere")
  132. assert sto.st_size == len("somewhere")
  133. assert stat.S_ISLNK(sti.st_mode)
  134. assert stat.S_ISLNK(sto.st_mode)
  135. assert os.readlink(in_fn) == os.readlink(out_fn)
  136. # FIFO
  137. if are_fifos_supported():
  138. out_fn = os.path.join(mountpoint, "archive", "input", "fifo1")
  139. sto = os.stat(out_fn)
  140. assert stat.S_ISFIFO(sto.st_mode)
  141. # list/read xattrs
  142. try:
  143. in_fn = "input/fusexattr"
  144. out_fn = os.fsencode(os.path.join(mountpoint, "archive", "input", "fusexattr"))
  145. if not xattr.XATTR_FAKEROOT and xattr.is_enabled(input_path):
  146. assert sorted(no_selinux(xattr.listxattr(out_fn))) == [b"user.empty", b"user.foo"]
  147. assert xattr.getxattr(out_fn, b"user.foo") == b"bar"
  148. assert xattr.getxattr(out_fn, b"user.empty") == b""
  149. else:
  150. assert no_selinux(xattr.listxattr(out_fn)) == []
  151. try:
  152. xattr.getxattr(out_fn, b"user.foo")
  153. except OSError as e:
  154. assert e.errno == llfuse.ENOATTR
  155. else:
  156. assert False, "expected OSError(ENOATTR), but no error was raised"
  157. except OSError as err:
  158. if sys.platform.startswith(("nothing_here_now",)) and err.errno == errno.ENOTSUP:
  159. # some systems have no xattr support on FUSE
  160. pass
  161. else:
  162. raise
  163. @pytest.mark.skipif(not llfuse, reason="llfuse not installed")
  164. def test_fuse_versions_view(archivers, request):
  165. archiver = request.getfixturevalue(archivers)
  166. repo_location, input_path = archiver.repository_location, archiver.input_path
  167. cmd(archiver, f"--repo={repo_location}", "rcreate", RK_ENCRYPTION)
  168. create_regular_file(input_path, "test", contents=b"first")
  169. if are_hardlinks_supported():
  170. create_regular_file(input_path, "hardlink1", contents=b"123456")
  171. os.link("input/hardlink1", "input/hardlink2")
  172. os.link("input/hardlink1", "input/hardlink3")
  173. cmd(archiver, f"--repo={repo_location}", "create", "archive1", "input")
  174. create_regular_file(input_path, "test", contents=b"second")
  175. cmd(archiver, f"--repo={repo_location}", "create", "archive2", "input")
  176. mountpoint = os.path.join(archiver.tmpdir, "mountpoint")
  177. # mount the whole repository, archive contents shall show up in versioned view:
  178. with fuse_mount(repo_location, mountpoint, "-o", "versions"):
  179. path = os.path.join(mountpoint, "input", "test") # filename shows up as directory ...
  180. files = os.listdir(path)
  181. assert all(f.startswith("test.") for f in files) # ... with files test.xxxxx in there
  182. assert {b"first", b"second"} == {open(os.path.join(path, f), "rb").read() for f in files}
  183. if are_hardlinks_supported():
  184. hl1 = os.path.join(mountpoint, "input", "hardlink1", "hardlink1.00001")
  185. hl2 = os.path.join(mountpoint, "input", "hardlink2", "hardlink2.00001")
  186. hl3 = os.path.join(mountpoint, "input", "hardlink3", "hardlink3.00001")
  187. assert os.stat(hl1).st_ino == os.stat(hl2).st_ino == os.stat(hl3).st_ino
  188. assert open(hl3, "rb").read() == b"123456"
  189. # similar again, but exclude the 1st hardlink:
  190. with fuse_mount(repo_location, mountpoint, "-o", "versions", "-e", "input/hardlink1"):
  191. if are_hardlinks_supported():
  192. hl2 = os.path.join(mountpoint, "input", "hardlink2", "hardlink2.00001")
  193. hl3 = os.path.join(mountpoint, "input", "hardlink3", "hardlink3.00001")
  194. assert os.stat(hl2).st_ino == os.stat(hl3).st_ino
  195. assert open(hl3, "rb").read() == b"123456"
  196. @pytest.mark.skipif(not llfuse, reason="llfuse not installed")
  197. def test_fuse_allow_damaged_files(archivers, request):
  198. archiver = request.getfixturevalue(archivers)
  199. repo_location, repo_path = archiver.repository_location, archiver.repository_path
  200. cmd(archiver, f"--repo={repo_location}", "rcreate", RK_ENCRYPTION)
  201. create_src_archive(archiver, "archive")
  202. # Get rid of a chunk and repair it
  203. archive, repository = open_archive(repo_path, "archive")
  204. with repository:
  205. for item in archive.iter_items():
  206. if item.path.endswith(src_file):
  207. repository.delete(item.chunks[-1].id)
  208. path = item.path # store full path for later
  209. break
  210. else:
  211. assert False # missed the file
  212. repository.commit(compact=False)
  213. cmd(archiver, f"--repo={repo_location}", "check", "--repair", exit_code=0)
  214. mountpoint = os.path.join(archiver.tmpdir, "mountpoint")
  215. with fuse_mount(repo_location, mountpoint, "-a", "archive"):
  216. with pytest.raises(OSError) as excinfo:
  217. open(os.path.join(mountpoint, "archive", path))
  218. assert excinfo.value.errno == errno.EIO
  219. with fuse_mount(repo_location, mountpoint, "-a", "archive", "-o", "allow_damaged_files"):
  220. open(os.path.join(mountpoint, "archive", path)).close()
  221. @pytest.mark.skipif(not llfuse, reason="llfuse not installed")
  222. def test_fuse_mount_options(archivers, request):
  223. archiver = request.getfixturevalue(archivers)
  224. repo_location = archiver.repository_location
  225. cmd(archiver, f"--repo={repo_location}", "rcreate", RK_ENCRYPTION)
  226. create_src_archive(archiver, "arch11")
  227. create_src_archive(archiver, "arch12")
  228. create_src_archive(archiver, "arch21")
  229. create_src_archive(archiver, "arch22")
  230. mountpoint = os.path.join(archiver.tmpdir, "mountpoint")
  231. with fuse_mount(repo_location, mountpoint, "--first=2", "--sort=name"):
  232. assert sorted(os.listdir(os.path.join(mountpoint))) == ["arch11", "arch12"]
  233. with fuse_mount(repo_location, mountpoint, "--last=2", "--sort=name"):
  234. assert sorted(os.listdir(os.path.join(mountpoint))) == ["arch21", "arch22"]
  235. with fuse_mount(repo_location, mountpoint, "--match-archives=sh:arch1*"):
  236. assert sorted(os.listdir(os.path.join(mountpoint))) == ["arch11", "arch12"]
  237. with fuse_mount(repo_location, mountpoint, "--match-archives=sh:arch2*"):
  238. assert sorted(os.listdir(os.path.join(mountpoint))) == ["arch21", "arch22"]
  239. with fuse_mount(repo_location, mountpoint, "--match-archives=sh:arch*"):
  240. assert sorted(os.listdir(os.path.join(mountpoint))) == ["arch11", "arch12", "arch21", "arch22"]
  241. with fuse_mount(repo_location, mountpoint, "--match-archives=nope"):
  242. assert sorted(os.listdir(os.path.join(mountpoint))) == []
  243. @pytest.mark.skipif(not llfuse, reason="llfuse not installed")
  244. def test_migrate_lock_alive(archivers, request):
  245. archiver = request.getfixturevalue(archivers)
  246. if archiver.prefix == "ssh://__testsuite__":
  247. pytest.skip("only works locally")
  248. repo_location = archiver.repository_location
  249. """Both old_id and new_id must not be stale during lock migration / daemonization."""
  250. from functools import wraps
  251. import pickle
  252. import traceback
  253. # Check results are communicated from the borg mount background process
  254. # to the pytest process by means of a serialized dict object stored in this file.
  255. assert_data_file = os.path.join(archiver.tmpdir, "migrate_lock_assert_data.pickle")
  256. # Decorates Lock.migrate_lock() with process_alive() checks before and after.
  257. # (We don't want to mix testing code into runtime.)
  258. def write_assert_data(migrate_lock):
  259. @wraps(migrate_lock)
  260. def wrapper(self, old_id, new_id):
  261. wrapper.num_calls += 1
  262. assert_data = {
  263. "num_calls": wrapper.num_calls,
  264. "old_id": old_id,
  265. "new_id": new_id,
  266. "before": {
  267. "old_id_alive": platform.process_alive(*old_id),
  268. "new_id_alive": platform.process_alive(*new_id),
  269. },
  270. "exception": None,
  271. "exception.extr_tb": None,
  272. "after": {"old_id_alive": None, "new_id_alive": None},
  273. }
  274. try:
  275. with open(assert_data_file, "wb") as _out:
  276. pickle.dump(assert_data, _out)
  277. except:
  278. pass
  279. try:
  280. return migrate_lock(self, old_id, new_id)
  281. except BaseException as e:
  282. assert_data["exception"] = e
  283. assert_data["exception.extr_tb"] = traceback.extract_tb(e.__traceback__)
  284. finally:
  285. assert_data["after"].update(
  286. {"old_id_alive": platform.process_alive(*old_id), "new_id_alive": platform.process_alive(*new_id)}
  287. )
  288. try:
  289. with open(assert_data_file, "wb") as _out:
  290. pickle.dump(assert_data, _out)
  291. except:
  292. pass
  293. wrapper.num_calls = 0
  294. return wrapper
  295. # Decorate
  296. Lock.migrate_lock = write_assert_data(Lock.migrate_lock)
  297. try:
  298. cmd(archiver, f"--repo={repo_location}", "rcreate", "--encryption=none")
  299. create_src_archive(archiver, "arch")
  300. mountpoint = os.path.join(archiver.tmpdir, "mountpoint")
  301. # In order that the decoration is kept for the borg mount process, we must not spawn, but actually fork;
  302. # not to be confused with the forking in borg.helpers.daemonize() which is done as well.
  303. with fuse_mount(repo_location, mountpoint, os_fork=True):
  304. pass
  305. with open(assert_data_file, "rb") as _in:
  306. assert_data = pickle.load(_in)
  307. print(f"\nLock.migrate_lock(): assert_data = {assert_data!r}.", file=sys.stderr, flush=True)
  308. exception = assert_data["exception"]
  309. if exception is not None:
  310. extracted_tb = assert_data["exception.extr_tb"]
  311. print(
  312. "Lock.migrate_lock() raised an exception:\n",
  313. "Traceback (most recent call last):\n",
  314. *traceback.format_list(extracted_tb),
  315. *traceback.format_exception(exception.__class__, exception, None),
  316. sep="",
  317. end="",
  318. file=sys.stderr,
  319. flush=True,
  320. )
  321. assert assert_data["num_calls"] == 1, "Lock.migrate_lock() must be called exactly once."
  322. assert exception is None, "Lock.migrate_lock() may not raise an exception."
  323. assert_data_before = assert_data["before"]
  324. assert assert_data_before[
  325. "old_id_alive"
  326. ], "old_id must be alive (=must not be stale) when calling Lock.migrate_lock()."
  327. assert assert_data_before[
  328. "new_id_alive"
  329. ], "new_id must be alive (=must not be stale) when calling Lock.migrate_lock()."
  330. assert_data_after = assert_data["after"]
  331. assert assert_data_after[
  332. "old_id_alive"
  333. ], "old_id must be alive (=must not be stale) when Lock.migrate_lock() has returned."
  334. assert assert_data_after[
  335. "new_id_alive"
  336. ], "new_id must be alive (=must not be stale) when Lock.migrate_lock() has returned."
  337. finally:
  338. # Undecorate
  339. Lock.migrate_lock = Lock.migrate_lock.__wrapped__