|
@@ -0,0 +1,242 @@
|
|
|
|
+import logging
|
|
|
|
+import shutil
|
|
|
|
+from unittest.mock import patch
|
|
|
|
+
|
|
|
|
+from ...archive import ChunkBuffer
|
|
|
|
+from ...constants import * # NOQA
|
|
|
|
+from ...helpers import bin_to_hex
|
|
|
|
+from ...helpers import msgpack
|
|
|
|
+from ...manifest import Manifest
|
|
|
|
+from ...repository import Repository
|
|
|
|
+from . import ArchiverTestCaseBase, RK_ENCRYPTION
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+class ArchiverCheckTestCase(ArchiverTestCaseBase):
|
|
|
|
+ def setUp(self):
|
|
|
|
+ super().setUp()
|
|
|
|
+ with patch.object(ChunkBuffer, "BUFFER_SIZE", 10):
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "rcreate", RK_ENCRYPTION)
|
|
|
|
+ self.create_src_archive("archive1")
|
|
|
|
+ self.create_src_archive("archive2")
|
|
|
|
+
|
|
|
|
+ def test_check_usage(self):
|
|
|
|
+ output = self.cmd(f"--repo={self.repository_location}", "check", "-v", "--progress", exit_code=0)
|
|
|
|
+ self.assert_in("Starting repository check", output)
|
|
|
|
+ self.assert_in("Starting archive consistency check", output)
|
|
|
|
+ self.assert_in("Checking segments", output)
|
|
|
|
+ # reset logging to new process default to avoid need for fork=True on next check
|
|
|
|
+ logging.getLogger("borg.output.progress").setLevel(logging.NOTSET)
|
|
|
|
+ output = self.cmd(f"--repo={self.repository_location}", "check", "-v", "--repository-only", exit_code=0)
|
|
|
|
+ self.assert_in("Starting repository check", output)
|
|
|
|
+ self.assert_not_in("Starting archive consistency check", output)
|
|
|
|
+ self.assert_not_in("Checking segments", output)
|
|
|
|
+ output = self.cmd(f"--repo={self.repository_location}", "check", "-v", "--archives-only", exit_code=0)
|
|
|
|
+ self.assert_not_in("Starting repository check", output)
|
|
|
|
+ self.assert_in("Starting archive consistency check", output)
|
|
|
|
+ output = self.cmd(
|
|
|
|
+ f"--repo={self.repository_location}",
|
|
|
|
+ "check",
|
|
|
|
+ "-v",
|
|
|
|
+ "--archives-only",
|
|
|
|
+ "--glob-archives=archive2",
|
|
|
|
+ exit_code=0,
|
|
|
|
+ )
|
|
|
|
+ self.assert_not_in("archive1", output)
|
|
|
|
+ output = self.cmd(
|
|
|
|
+ f"--repo={self.repository_location}", "check", "-v", "--archives-only", "--first=1", exit_code=0
|
|
|
|
+ )
|
|
|
|
+ self.assert_in("archive1", output)
|
|
|
|
+ self.assert_not_in("archive2", output)
|
|
|
|
+ output = self.cmd(
|
|
|
|
+ f"--repo={self.repository_location}", "check", "-v", "--archives-only", "--last=1", exit_code=0
|
|
|
|
+ )
|
|
|
|
+ self.assert_not_in("archive1", output)
|
|
|
|
+ self.assert_in("archive2", output)
|
|
|
|
+
|
|
|
|
+ def test_missing_file_chunk(self):
|
|
|
|
+ archive, repository = self.open_archive("archive1")
|
|
|
|
+ with repository:
|
|
|
|
+ for item in archive.iter_items():
|
|
|
|
+ if item.path.endswith("testsuite/archiver/__init__.py"):
|
|
|
|
+ valid_chunks = item.chunks
|
|
|
|
+ killed_chunk = valid_chunks[-1]
|
|
|
|
+ repository.delete(killed_chunk.id)
|
|
|
|
+ break
|
|
|
|
+ else:
|
|
|
|
+ self.fail("should not happen")
|
|
|
|
+ repository.commit(compact=False)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=1)
|
|
|
|
+ output = self.cmd(f"--repo={self.repository_location}", "check", "--repair", exit_code=0)
|
|
|
|
+ self.assert_in("New missing file chunk detected", output)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=0)
|
|
|
|
+ output = self.cmd(
|
|
|
|
+ f"--repo={self.repository_location}", "list", "archive1", "--format={health}#{path}{LF}", exit_code=0
|
|
|
|
+ )
|
|
|
|
+ self.assert_in("broken#", output)
|
|
|
|
+ # check that the file in the old archives has now a different chunk list without the killed chunk
|
|
|
|
+ for archive_name in ("archive1", "archive2"):
|
|
|
|
+ archive, repository = self.open_archive(archive_name)
|
|
|
|
+ with repository:
|
|
|
|
+ for item in archive.iter_items():
|
|
|
|
+ if item.path.endswith("testsuite/archiver/__init__.py"):
|
|
|
|
+ self.assert_not_equal(valid_chunks, item.chunks)
|
|
|
|
+ self.assert_not_in(killed_chunk, item.chunks)
|
|
|
|
+ break
|
|
|
|
+ else:
|
|
|
|
+ self.fail("should not happen")
|
|
|
|
+ # do a fresh backup (that will include the killed chunk)
|
|
|
|
+ with patch.object(ChunkBuffer, "BUFFER_SIZE", 10):
|
|
|
|
+ self.create_src_archive("archive3")
|
|
|
|
+ # check should be able to heal the file now:
|
|
|
|
+ output = self.cmd(f"--repo={self.repository_location}", "check", "-v", "--repair", exit_code=0)
|
|
|
|
+ self.assert_in("Healed previously missing file chunk", output)
|
|
|
|
+ self.assert_in("testsuite/archiver/__init__.py: Completely healed previously damaged file!", output)
|
|
|
|
+ # check that the file in the old archives has the correct chunks again
|
|
|
|
+ for archive_name in ("archive1", "archive2"):
|
|
|
|
+ archive, repository = self.open_archive(archive_name)
|
|
|
|
+ with repository:
|
|
|
|
+ for item in archive.iter_items():
|
|
|
|
+ if item.path.endswith("testsuite/archiver/__init__.py"):
|
|
|
|
+ self.assert_equal(valid_chunks, item.chunks)
|
|
|
|
+ break
|
|
|
|
+ else:
|
|
|
|
+ self.fail("should not happen")
|
|
|
|
+ # list is also all-healthy again
|
|
|
|
+ output = self.cmd(
|
|
|
|
+ f"--repo={self.repository_location}", "list", "archive1", "--format={health}#{path}{LF}", exit_code=0
|
|
|
|
+ )
|
|
|
|
+ self.assert_not_in("broken#", output)
|
|
|
|
+
|
|
|
|
+ def test_missing_archive_item_chunk(self):
|
|
|
|
+ archive, repository = self.open_archive("archive1")
|
|
|
|
+ with repository:
|
|
|
|
+ repository.delete(archive.metadata.items[0])
|
|
|
|
+ repository.commit(compact=False)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=1)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", "--repair", exit_code=0)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=0)
|
|
|
|
+
|
|
|
|
+ def test_missing_archive_metadata(self):
|
|
|
|
+ archive, repository = self.open_archive("archive1")
|
|
|
|
+ with repository:
|
|
|
|
+ repository.delete(archive.id)
|
|
|
|
+ repository.commit(compact=False)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=1)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", "--repair", exit_code=0)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=0)
|
|
|
|
+
|
|
|
|
+ def test_missing_manifest(self):
|
|
|
|
+ archive, repository = self.open_archive("archive1")
|
|
|
|
+ with repository:
|
|
|
|
+ repository.delete(Manifest.MANIFEST_ID)
|
|
|
|
+ repository.commit(compact=False)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=1)
|
|
|
|
+ output = self.cmd(f"--repo={self.repository_location}", "check", "-v", "--repair", exit_code=0)
|
|
|
|
+ self.assert_in("archive1", output)
|
|
|
|
+ self.assert_in("archive2", output)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=0)
|
|
|
|
+
|
|
|
|
+ def test_corrupted_manifest(self):
|
|
|
|
+ archive, repository = self.open_archive("archive1")
|
|
|
|
+ with repository:
|
|
|
|
+ manifest = repository.get(Manifest.MANIFEST_ID)
|
|
|
|
+ corrupted_manifest = manifest + b"corrupted!"
|
|
|
|
+ repository.put(Manifest.MANIFEST_ID, corrupted_manifest)
|
|
|
|
+ repository.commit(compact=False)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=1)
|
|
|
|
+ output = self.cmd(f"--repo={self.repository_location}", "check", "-v", "--repair", exit_code=0)
|
|
|
|
+ self.assert_in("archive1", output)
|
|
|
|
+ self.assert_in("archive2", output)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=0)
|
|
|
|
+
|
|
|
|
+ def test_manifest_rebuild_corrupted_chunk(self):
|
|
|
|
+ archive, repository = self.open_archive("archive1")
|
|
|
|
+ with repository:
|
|
|
|
+ manifest = repository.get(Manifest.MANIFEST_ID)
|
|
|
|
+ corrupted_manifest = manifest + b"corrupted!"
|
|
|
|
+ repository.put(Manifest.MANIFEST_ID, corrupted_manifest)
|
|
|
|
+
|
|
|
|
+ chunk = repository.get(archive.id)
|
|
|
|
+ corrupted_chunk = chunk + b"corrupted!"
|
|
|
|
+ repository.put(archive.id, corrupted_chunk)
|
|
|
|
+ repository.commit(compact=False)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=1)
|
|
|
|
+ output = self.cmd(f"--repo={self.repository_location}", "check", "-v", "--repair", exit_code=0)
|
|
|
|
+ self.assert_in("archive2", output)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=0)
|
|
|
|
+
|
|
|
|
+ def test_manifest_rebuild_duplicate_archive(self):
|
|
|
|
+ archive, repository = self.open_archive("archive1")
|
|
|
|
+ repo_objs = archive.repo_objs
|
|
|
|
+
|
|
|
|
+ with repository:
|
|
|
|
+ manifest = repository.get(Manifest.MANIFEST_ID)
|
|
|
|
+ corrupted_manifest = manifest + b"corrupted!"
|
|
|
|
+ repository.put(Manifest.MANIFEST_ID, corrupted_manifest)
|
|
|
|
+
|
|
|
|
+ archive = msgpack.packb(
|
|
|
|
+ {
|
|
|
|
+ "cmdline": [],
|
|
|
|
+ "item_ptrs": [],
|
|
|
|
+ "hostname": "foo",
|
|
|
|
+ "username": "bar",
|
|
|
|
+ "name": "archive1",
|
|
|
|
+ "time": "2016-12-15T18:49:51.849711",
|
|
|
|
+ "version": 2,
|
|
|
|
+ }
|
|
|
|
+ )
|
|
|
|
+ archive_id = repo_objs.id_hash(archive)
|
|
|
|
+ repository.put(archive_id, repo_objs.format(archive_id, {}, archive))
|
|
|
|
+ repository.commit(compact=False)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=1)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", "--repair", exit_code=0)
|
|
|
|
+ output = self.cmd(f"--repo={self.repository_location}", "rlist")
|
|
|
|
+ self.assert_in("archive1", output)
|
|
|
|
+ self.assert_in("archive1.1", output)
|
|
|
|
+ self.assert_in("archive2", output)
|
|
|
|
+
|
|
|
|
+ def test_extra_chunks(self):
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=0)
|
|
|
|
+ with Repository(self.repository_location, exclusive=True) as repository:
|
|
|
|
+ repository.put(b"01234567890123456789012345678901", b"xxxx")
|
|
|
|
+ repository.commit(compact=False)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=1)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=1)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", "--repair", exit_code=0)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=0)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "extract", "archive1", "--dry-run", exit_code=0)
|
|
|
|
+
|
|
|
|
+ def _test_verify_data(self, *init_args):
|
|
|
|
+ shutil.rmtree(self.repository_path)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "rcreate", *init_args)
|
|
|
|
+ self.create_src_archive("archive1")
|
|
|
|
+ archive, repository = self.open_archive("archive1")
|
|
|
|
+ with repository:
|
|
|
|
+ for item in archive.iter_items():
|
|
|
|
+ if item.path.endswith("testsuite/archiver/__init__.py"):
|
|
|
|
+ chunk = item.chunks[-1]
|
|
|
|
+ data = repository.get(chunk.id)
|
|
|
|
+ data = data[0:100] + b"x" + data[101:]
|
|
|
|
+ repository.put(chunk.id, data)
|
|
|
|
+ break
|
|
|
|
+ repository.commit(compact=False)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=0)
|
|
|
|
+ output = self.cmd(f"--repo={self.repository_location}", "check", "--verify-data", exit_code=1)
|
|
|
|
+ assert bin_to_hex(chunk.id) + ", integrity error" in output
|
|
|
|
+ # repair (heal is tested in another test)
|
|
|
|
+ output = self.cmd(f"--repo={self.repository_location}", "check", "--repair", "--verify-data", exit_code=0)
|
|
|
|
+ assert bin_to_hex(chunk.id) + ", integrity error" in output
|
|
|
|
+ assert "testsuite/archiver/__init__.py: New missing file chunk detected" in output
|
|
|
|
+
|
|
|
|
+ def test_verify_data(self):
|
|
|
|
+ self._test_verify_data(RK_ENCRYPTION)
|
|
|
|
+
|
|
|
|
+ def test_verify_data_unencrypted(self):
|
|
|
|
+ self._test_verify_data("--encryption", "none")
|
|
|
|
+
|
|
|
|
+ def test_empty_repository(self):
|
|
|
|
+ with Repository(self.repository_location, exclusive=True) as repository:
|
|
|
|
+ for id_ in repository.list():
|
|
|
|
+ repository.delete(id_)
|
|
|
|
+ repository.commit(compact=False)
|
|
|
|
+ self.cmd(f"--repo={self.repository_location}", "check", exit_code=1)
|