upgrader.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import os
  2. import pytest
  3. try:
  4. import attic.repository
  5. import attic.key
  6. import attic.helpers
  7. except ImportError:
  8. attic = None
  9. from ..upgrader import AtticRepositoryUpgrader, AtticKeyfileKey
  10. from ..helpers import get_keys_dir
  11. from ..key import KeyfileKey
  12. from ..archiver import UMASK_DEFAULT
  13. from ..repository import Repository
  14. def repo_valid(path):
  15. """
  16. utility function to check if borg can open a repository
  17. :param path: the path to the repository
  18. :returns: if borg can check the repository
  19. """
  20. with Repository(str(path), exclusive=True, create=False) as repository:
  21. # can't check raises() because check() handles the error
  22. return repository.check()
  23. def key_valid(path):
  24. """
  25. check that the new keyfile is alright
  26. :param path: the path to the key file
  27. :returns: if the file starts with the borg magic string
  28. """
  29. keyfile = os.path.join(get_keys_dir(),
  30. os.path.basename(path))
  31. with open(keyfile, 'r') as f:
  32. return f.read().startswith(KeyfileKey.FILE_ID)
  33. @pytest.fixture()
  34. def attic_repo(tmpdir):
  35. """
  36. create an attic repo with some stuff in it
  37. :param tmpdir: path to the repository to be created
  38. :returns: a attic.repository.Repository object
  39. """
  40. attic_repo = attic.repository.Repository(str(tmpdir), create=True)
  41. # throw some stuff in that repo, copied from `RepositoryTestCase.test1`
  42. for x in range(100):
  43. attic_repo.put(('%-32d' % x).encode('ascii'), b'SOMEDATA')
  44. attic_repo.commit()
  45. attic_repo.close()
  46. return attic_repo
  47. @pytest.fixture(params=[True, False])
  48. def inplace(request):
  49. return request.param
  50. @pytest.mark.skipif(attic is None, reason='cannot find an attic install')
  51. def test_convert_segments(tmpdir, attic_repo, inplace):
  52. """test segment conversion
  53. this will load the given attic repository, list all the segments
  54. then convert them one at a time. we need to close the repo before
  55. conversion otherwise we have errors from borg
  56. :param tmpdir: a temporary directory to run the test in (builtin
  57. fixture)
  58. :param attic_repo: a populated attic repository (fixture)
  59. """
  60. # check should fail because of magic number
  61. assert not repo_valid(tmpdir)
  62. repository = AtticRepositoryUpgrader(str(tmpdir), create=False)
  63. with repository:
  64. segments = [filename for i, filename in repository.io.segment_iterator()]
  65. repository.convert_segments(segments, dryrun=False, inplace=inplace)
  66. repository.convert_cache(dryrun=False)
  67. assert repo_valid(tmpdir)
  68. class MockArgs:
  69. """
  70. mock attic location
  71. this is used to simulate a key location with a properly loaded
  72. repository object to create a key file
  73. """
  74. def __init__(self, path):
  75. self.repository = attic.helpers.Location(path)
  76. @pytest.fixture()
  77. def attic_key_file(attic_repo, tmpdir, monkeypatch):
  78. """
  79. create an attic key file from the given repo, in the keys
  80. subdirectory of the given tmpdir
  81. :param attic_repo: an attic.repository.Repository object (fixture
  82. define above)
  83. :param tmpdir: a temporary directory (a builtin fixture)
  84. :returns: the KeyfileKey object as returned by
  85. attic.key.KeyfileKey.create()
  86. """
  87. keys_dir = str(tmpdir.mkdir('keys'))
  88. # we use the repo dir for the created keyfile, because we do
  89. # not want to clutter existing keyfiles
  90. monkeypatch.setenv('ATTIC_KEYS_DIR', keys_dir)
  91. # we use the same directory for the converted files, which
  92. # will clutter the previously created one, which we don't care
  93. # about anyways. in real runs, the original key will be retained.
  94. monkeypatch.setenv('BORG_KEYS_DIR', keys_dir)
  95. monkeypatch.setenv('ATTIC_PASSPHRASE', 'test')
  96. return attic.key.KeyfileKey.create(attic_repo,
  97. MockArgs(keys_dir))
  98. @pytest.mark.skipif(attic is None, reason='cannot find an attic install')
  99. def test_keys(tmpdir, attic_repo, attic_key_file):
  100. """test key conversion
  101. test that we can convert the given key to a properly formatted
  102. borg key. assumes that the ATTIC_KEYS_DIR and BORG_KEYS_DIR have
  103. been properly populated by the attic_key_file fixture.
  104. :param tmpdir: a temporary directory (a builtin fixture)
  105. :param attic_repo: an attic.repository.Repository object (fixture
  106. define above)
  107. :param attic_key_file: an attic.key.KeyfileKey (fixture created above)
  108. """
  109. with AtticRepositoryUpgrader(str(tmpdir), create=False) as repository:
  110. keyfile = AtticKeyfileKey.find_key_file(repository)
  111. AtticRepositoryUpgrader.convert_keyfiles(keyfile, dryrun=False)
  112. assert key_valid(attic_key_file.path)
  113. @pytest.mark.skipif(attic is None, reason='cannot find an attic install')
  114. def test_convert_all(tmpdir, attic_repo, attic_key_file, inplace):
  115. """test all conversion steps
  116. this runs everything. mostly redundant test, since everything is
  117. done above. yet we expect a NotImplementedError because we do not
  118. convert caches yet.
  119. :param tmpdir: a temporary directory (a builtin fixture)
  120. :param attic_repo: an attic.repository.Repository object (fixture
  121. define above)
  122. :param attic_key_file: an attic.key.KeyfileKey (fixture created above)
  123. """
  124. # check should fail because of magic number
  125. assert not repo_valid(tmpdir)
  126. def stat_segment(path):
  127. return os.stat(os.path.join(path, 'data', '0', '0'))
  128. def first_inode(path):
  129. return stat_segment(path).st_ino
  130. orig_inode = first_inode(attic_repo.path)
  131. with AtticRepositoryUpgrader(str(tmpdir), create=False) as repository:
  132. # replicate command dispatch, partly
  133. os.umask(UMASK_DEFAULT)
  134. backup = repository.upgrade(dryrun=False, inplace=inplace)
  135. if inplace:
  136. assert backup is None
  137. assert first_inode(repository.path) == orig_inode
  138. else:
  139. assert backup
  140. assert first_inode(repository.path) != first_inode(backup)
  141. # i have seen cases where the copied tree has world-readable
  142. # permissions, which is wrong
  143. assert stat_segment(backup).st_mode & UMASK_DEFAULT == 0
  144. assert key_valid(attic_key_file.path)
  145. assert repo_valid(tmpdir)
  146. def test_hardlink(tmpdir, inplace):
  147. """test that we handle hard links properly
  148. that is, if we are in "inplace" mode, hardlinks should *not*
  149. change (ie. we write to the file directly, so we do not rewrite the
  150. whole file, and we do not re-create the file).
  151. if we are *not* in inplace mode, then the inode should change, as
  152. we are supposed to leave the original inode alone."""
  153. a = str(tmpdir.join('a'))
  154. with open(a, 'wb') as tmp:
  155. tmp.write(b'aXXX')
  156. b = str(tmpdir.join('b'))
  157. os.link(a, b)
  158. AtticRepositoryUpgrader.header_replace(b, b'a', b'b', inplace=inplace)
  159. if not inplace:
  160. assert os.stat(a).st_ino != os.stat(b).st_ino
  161. else:
  162. assert os.stat(a).st_ino == os.stat(b).st_ino
  163. with open(b, 'rb') as tmp:
  164. assert tmp.read() == b'bXXX'