upgrader.py 6.8 KB

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