archiver.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. import os
  2. from io import StringIO
  3. import stat
  4. import subprocess
  5. import sys
  6. import shutil
  7. import tempfile
  8. import time
  9. import unittest
  10. from hashlib import sha256
  11. from attic import xattr
  12. from attic.archive import Archive
  13. from attic.archiver import Archiver
  14. from attic.helpers import Manifest
  15. from attic.repository import Repository
  16. from attic.testsuite import AtticTestCase
  17. from attic.crypto import bytes_to_long, num_aes_blocks
  18. try:
  19. import llfuse
  20. has_llfuse = True
  21. except ImportError:
  22. has_llfuse = False
  23. src_dir = os.path.join(os.getcwd(), os.path.dirname(__file__), '..')
  24. class changedir:
  25. def __init__(self, dir):
  26. self.dir = dir
  27. def __enter__(self):
  28. self.old = os.getcwd()
  29. os.chdir(self.dir)
  30. def __exit__(self, *args, **kw):
  31. os.chdir(self.old)
  32. class ArchiverTestCaseBase(AtticTestCase):
  33. prefix = ''
  34. def setUp(self):
  35. os.environ['ATTIC_CHECK_I_KNOW_WHAT_I_AM_DOING'] = '1'
  36. self.archiver = Archiver()
  37. self.tmpdir = tempfile.mkdtemp()
  38. self.repository_path = os.path.join(self.tmpdir, 'repository')
  39. self.repository_location = self.prefix + self.repository_path
  40. self.input_path = os.path.join(self.tmpdir, 'input')
  41. self.output_path = os.path.join(self.tmpdir, 'output')
  42. self.keys_path = os.path.join(self.tmpdir, 'keys')
  43. self.cache_path = os.path.join(self.tmpdir, 'cache')
  44. self.exclude_file_path = os.path.join(self.tmpdir, 'excludes')
  45. os.environ['ATTIC_KEYS_DIR'] = self.keys_path
  46. os.environ['ATTIC_CACHE_DIR'] = self.cache_path
  47. os.mkdir(self.input_path)
  48. os.mkdir(self.output_path)
  49. os.mkdir(self.keys_path)
  50. os.mkdir(self.cache_path)
  51. with open(self.exclude_file_path, 'wb') as fd:
  52. fd.write(b'input/file2\n# A commment line, then a blank line\n\n')
  53. self._old_wd = os.getcwd()
  54. os.chdir(self.tmpdir)
  55. def tearDown(self):
  56. shutil.rmtree(self.tmpdir)
  57. os.chdir(self._old_wd)
  58. def attic(self, *args, **kw):
  59. exit_code = kw.get('exit_code', 0)
  60. fork = kw.get('fork', False)
  61. if fork:
  62. try:
  63. output = subprocess.check_output((sys.executable, '-m', 'attic.archiver') + args)
  64. ret = 0
  65. except subprocess.CalledProcessError as e:
  66. output = e.output
  67. ret = e.returncode
  68. output = os.fsdecode(output)
  69. if ret != exit_code:
  70. print(output)
  71. self.assert_equal(exit_code, ret)
  72. return output
  73. args = list(args)
  74. stdout, stderr = sys.stdout, sys.stderr
  75. try:
  76. output = StringIO()
  77. sys.stdout = sys.stderr = output
  78. ret = self.archiver.run(args)
  79. sys.stdout, sys.stderr = stdout, stderr
  80. if ret != exit_code:
  81. print(output.getvalue())
  82. self.assert_equal(exit_code, ret)
  83. return output.getvalue()
  84. finally:
  85. sys.stdout, sys.stderr = stdout, stderr
  86. def create_src_archive(self, name):
  87. self.attic('create', self.repository_location + '::' + name, src_dir)
  88. class ArchiverTestCase(ArchiverTestCaseBase):
  89. def create_regual_file(self, name, size=0):
  90. filename = os.path.join(self.input_path, name)
  91. if not os.path.exists(os.path.dirname(filename)):
  92. os.makedirs(os.path.dirname(filename))
  93. with open(filename, 'wb') as fd:
  94. fd.write(b'X' * size)
  95. def create_test_files(self):
  96. """Create a minimal test case including all supported file types
  97. """
  98. # File
  99. self.create_regual_file('empty', size=0)
  100. self.create_regual_file('file1', size=1024 * 80)
  101. # Directory
  102. self.create_regual_file('dir2/file2', size=1024 * 80)
  103. # File owner
  104. os.chown('input/file1', 100, 200)
  105. # File mode
  106. os.chmod('input/file1', 0o7755)
  107. os.chmod('input/dir2', 0o555)
  108. # Block device
  109. os.mknod('input/bdev', 0o600 | stat.S_IFBLK, os.makedev(10, 20))
  110. # Char device
  111. os.mknod('input/cdev', 0o600 | stat.S_IFCHR, os.makedev(30, 40))
  112. if xattr.is_enabled():
  113. xattr.setxattr(os.path.join(self.input_path, 'file1'), 'user.foo', b'bar')
  114. # Hard link
  115. os.link(os.path.join(self.input_path, 'file1'),
  116. os.path.join(self.input_path, 'hardlink'))
  117. # Symlink
  118. os.symlink('somewhere', os.path.join(self.input_path, 'link1'))
  119. # FIFO node
  120. os.mkfifo(os.path.join(self.input_path, 'fifo1'))
  121. def test_basic_functionality(self):
  122. self.create_test_files()
  123. self.attic('init', self.repository_location)
  124. self.attic('create', self.repository_location + '::test', 'input')
  125. self.attic('create', self.repository_location + '::test.2', 'input')
  126. with changedir('output'):
  127. self.attic('extract', self.repository_location + '::test')
  128. self.assert_equal(len(self.attic('list', self.repository_location).splitlines()), 2)
  129. self.assert_equal(len(self.attic('list', self.repository_location + '::test').splitlines()), 10)
  130. self.assert_dirs_equal('input', 'output/input')
  131. info_output = self.attic('info', self.repository_location + '::test')
  132. shutil.rmtree(self.cache_path)
  133. info_output2 = self.attic('info', self.repository_location + '::test')
  134. # info_output2 starts with some "initializing cache" text but should
  135. # end the same way as info_output
  136. assert info_output2.endswith(info_output)
  137. def test_extract_include_exclude(self):
  138. self.attic('init', self.repository_location)
  139. self.create_regual_file('file1', size=1024 * 80)
  140. self.create_regual_file('file2', size=1024 * 80)
  141. self.create_regual_file('file3', size=1024 * 80)
  142. self.create_regual_file('file4', size=1024 * 80)
  143. self.attic('create', '--exclude=input/file4', self.repository_location + '::test', 'input')
  144. with changedir('output'):
  145. self.attic('extract', self.repository_location + '::test', 'input/file1', )
  146. self.assert_equal(sorted(os.listdir('output/input')), ['file1'])
  147. with changedir('output'):
  148. self.attic('extract', '--exclude=input/file2', self.repository_location + '::test')
  149. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3'])
  150. with changedir('output'):
  151. self.attic('extract', '--exclude-from=' + self.exclude_file_path, self.repository_location + '::test')
  152. self.assert_equal(sorted(os.listdir('output/input')), ['file1', 'file3'])
  153. def test_path_normalization(self):
  154. self.attic('init', self.repository_location)
  155. self.create_regual_file('dir1/dir2/file', size=1024 * 80)
  156. with changedir('input/dir1/dir2'):
  157. self.attic('create', self.repository_location + '::test', '../../../input/dir1/../dir1/dir2/..')
  158. output = self.attic('list', self.repository_location + '::test')
  159. self.assert_not_in('..', output)
  160. self.assert_in(' input/dir1/dir2/file', output)
  161. def test_repeated_files(self):
  162. self.create_regual_file('file1', size=1024 * 80)
  163. self.attic('init', self.repository_location)
  164. self.attic('create', self.repository_location + '::test', 'input', 'input')
  165. def test_overwrite(self):
  166. self.create_regual_file('file1', size=1024 * 80)
  167. self.create_regual_file('dir2/file2', size=1024 * 80)
  168. self.attic('init', self.repository_location)
  169. self.attic('create', self.repository_location + '::test', 'input')
  170. # Overwriting regular files and directories should be supported
  171. os.mkdir('output/input')
  172. os.mkdir('output/input/file1')
  173. os.mkdir('output/input/dir2')
  174. with changedir('output'):
  175. self.attic('extract', self.repository_location + '::test')
  176. self.assert_dirs_equal('input', 'output/input')
  177. # But non-empty dirs should fail
  178. os.unlink('output/input/file1')
  179. os.mkdir('output/input/file1')
  180. os.mkdir('output/input/file1/dir')
  181. with changedir('output'):
  182. self.attic('extract', self.repository_location + '::test', exit_code=1)
  183. def test_delete(self):
  184. self.create_regual_file('file1', size=1024 * 80)
  185. self.create_regual_file('dir2/file2', size=1024 * 80)
  186. self.attic('init', self.repository_location)
  187. self.attic('create', self.repository_location + '::test', 'input')
  188. self.attic('create', self.repository_location + '::test.2', 'input')
  189. self.attic('extract', '--dry-run', self.repository_location + '::test')
  190. self.attic('extract', '--dry-run', self.repository_location + '::test.2')
  191. self.attic('delete', self.repository_location + '::test')
  192. self.attic('extract', '--dry-run', self.repository_location + '::test.2')
  193. self.attic('delete', self.repository_location + '::test.2')
  194. # Make sure all data except the manifest has been deleted
  195. repository = Repository(self.repository_path)
  196. self.assert_equal(len(repository), 1)
  197. def test_corrupted_repository(self):
  198. self.attic('init', self.repository_location)
  199. self.create_src_archive('test')
  200. self.attic('extract', '--dry-run', self.repository_location + '::test')
  201. self.attic('check', self.repository_location)
  202. name = sorted(os.listdir(os.path.join(self.tmpdir, 'repository', 'data', '0')), reverse=True)[0]
  203. fd = open(os.path.join(self.tmpdir, 'repository', 'data', '0', name), 'r+')
  204. fd.seek(100)
  205. fd.write('XXXX')
  206. fd.close()
  207. self.attic('check', self.repository_location, exit_code=1)
  208. def test_readonly_repository(self):
  209. self.attic('init', self.repository_location)
  210. self.create_src_archive('test')
  211. os.system('chmod -R ugo-w ' + self.repository_path)
  212. try:
  213. self.attic('extract', '--dry-run', self.repository_location + '::test')
  214. finally:
  215. # Restore permissions so shutil.rmtree is able to delete it
  216. os.system('chmod -R u+w ' + self.repository_path)
  217. def test_cmdline_compatibility(self):
  218. self.create_regual_file('file1', size=1024 * 80)
  219. self.attic('init', self.repository_location)
  220. self.attic('create', self.repository_location + '::test', 'input')
  221. output = self.attic('verify', '-v', self.repository_location + '::test')
  222. self.assert_in('"attic verify" has been deprecated', output)
  223. output = self.attic('prune', self.repository_location, '--hourly=1')
  224. self.assert_in('"--hourly" has been deprecated. Use "--keep-hourly" instead', output)
  225. def test_prune_repository(self):
  226. self.attic('init', self.repository_location)
  227. self.attic('create', self.repository_location + '::test1', src_dir)
  228. self.attic('create', self.repository_location + '::test2', src_dir)
  229. output = self.attic('prune', '-v', '--dry-run', self.repository_location, '--keep-daily=2')
  230. self.assert_in('Keeping archive "test2"', output)
  231. self.assert_in('Would prune "test1"', output)
  232. output = self.attic('list', self.repository_location)
  233. self.assert_in('test1', output)
  234. self.assert_in('test2', output)
  235. self.attic('prune', self.repository_location, '--keep-daily=2')
  236. output = self.attic('list', self.repository_location)
  237. self.assert_not_in('test1', output)
  238. self.assert_in('test2', output)
  239. def test_usage(self):
  240. self.assert_raises(SystemExit, lambda: self.attic())
  241. self.assert_raises(SystemExit, lambda: self.attic('-h'))
  242. @unittest.skipUnless(has_llfuse, 'llfuse not installed')
  243. def test_mount(self):
  244. mountpoint = os.path.join(self.tmpdir, 'mountpoint')
  245. os.mkdir(mountpoint)
  246. self.attic('init', self.repository_location)
  247. self.create_test_files()
  248. self.attic('create', self.repository_location + '::archive', 'input')
  249. try:
  250. self.attic('mount', self.repository_location + '::archive', mountpoint, fork=True)
  251. self.wait_for_mount(mountpoint)
  252. self.assert_dirs_equal(self.input_path, os.path.join(mountpoint, 'input'))
  253. finally:
  254. if sys.platform.startswith('linux'):
  255. os.system('fusermount -u ' + mountpoint)
  256. else:
  257. os.system('umount ' + mountpoint)
  258. os.rmdir(mountpoint)
  259. # Give the daemon some time to exit
  260. time.sleep(.2)
  261. def verify_aes_counter_uniqueness(self, method):
  262. seen = set() # Chunks already seen
  263. used = set() # counter values already used
  264. def verify_uniqueness():
  265. repository = Repository(self.repository_path)
  266. for key, _ in repository.get_read_only_index(repository.get_transaction_id()).iteritems():
  267. data = repository.get(key)
  268. hash = sha256(data).digest()
  269. if not hash in seen:
  270. seen.add(hash)
  271. num_blocks = num_aes_blocks(len(data) - 41)
  272. nonce = bytes_to_long(data[33:41])
  273. for counter in range(nonce, nonce + num_blocks):
  274. self.assert_not_in(counter, used)
  275. used.add(counter)
  276. self.create_test_files()
  277. os.environ['ATTIC_PASSPHRASE'] = 'passphrase'
  278. self.attic('init', '--encryption=' + method, self.repository_location)
  279. verify_uniqueness()
  280. self.attic('create', self.repository_location + '::test', 'input')
  281. verify_uniqueness()
  282. self.attic('create', self.repository_location + '::test.2', 'input')
  283. verify_uniqueness()
  284. self.attic('delete', self.repository_location + '::test.2')
  285. verify_uniqueness()
  286. self.assert_equal(used, set(range(len(used))))
  287. def test_aes_counter_uniqueness_keyfile(self):
  288. self.verify_aes_counter_uniqueness('keyfile')
  289. def test_aes_counter_uniqueness_passphrase(self):
  290. self.verify_aes_counter_uniqueness('passphrase')
  291. class ArchiverCheckTestCase(ArchiverTestCaseBase):
  292. def setUp(self):
  293. super(ArchiverCheckTestCase, self).setUp()
  294. self.attic('init', self.repository_location)
  295. self.create_src_archive('archive1')
  296. self.create_src_archive('archive2')
  297. def open_archive(self, name):
  298. repository = Repository(self.repository_path)
  299. manifest, key = Manifest.load(repository)
  300. archive = Archive(repository, key, manifest, name)
  301. return archive, repository
  302. def test_missing_file_chunk(self):
  303. archive, repository = self.open_archive('archive1')
  304. for item in archive.iter_items():
  305. if item[b'path'].endswith('testsuite/archiver.py'):
  306. repository.delete(item[b'chunks'][-1][0])
  307. break
  308. repository.commit()
  309. self.attic('check', self.repository_location, exit_code=1)
  310. self.attic('check', '--repair', self.repository_location, exit_code=0)
  311. self.attic('check', self.repository_location, exit_code=0)
  312. def test_missing_archive_item_chunk(self):
  313. archive, repository = self.open_archive('archive1')
  314. repository.delete(archive.metadata[b'items'][-1])
  315. repository.commit()
  316. self.attic('check', self.repository_location, exit_code=1)
  317. self.attic('check', '--repair', self.repository_location, exit_code=0)
  318. self.attic('check', self.repository_location, exit_code=0)
  319. def test_missing_archive_metadata(self):
  320. archive, repository = self.open_archive('archive1')
  321. repository.delete(archive.id)
  322. repository.commit()
  323. self.attic('check', self.repository_location, exit_code=1)
  324. self.attic('check', '--repair', self.repository_location, exit_code=0)
  325. self.attic('check', self.repository_location, exit_code=0)
  326. def test_missing_manifest(self):
  327. archive, repository = self.open_archive('archive1')
  328. repository.delete(Manifest.MANIFEST_ID)
  329. repository.commit()
  330. self.attic('check', self.repository_location, exit_code=1)
  331. self.attic('check', '--repair', '--progress', self.repository_location, exit_code=0)
  332. self.attic('check', '--progress', self.repository_location, exit_code=0)
  333. def test_extra_chunks(self):
  334. self.attic('check', self.repository_location, exit_code=0)
  335. repository = Repository(self.repository_location)
  336. repository.put(b'01234567890123456789012345678901', b'xxxx')
  337. repository.commit()
  338. repository.close()
  339. self.attic('check', self.repository_location, exit_code=1)
  340. self.attic('check', self.repository_location, exit_code=1)
  341. self.attic('check', '--repair', self.repository_location, exit_code=0)
  342. self.attic('check', self.repository_location, exit_code=0)
  343. self.attic('extract', '--dry-run', self.repository_location + '::archive1', exit_code=0)
  344. class RemoteArchiverTestCase(ArchiverTestCase):
  345. prefix = '__testsuite__:'