diff options
author | Lars Wirzenius <liw@liw.fi> | 2011-04-05 18:55:58 +0100 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2011-04-05 18:55:58 +0100 |
commit | e6efa258b332c93bf665f4d1529bf68a45f63cd5 (patch) | |
tree | 90a72fd97df9398aa7f06baf626f9a3bd91f82a4 | |
parent | 17c0d9062ce45848571d91bc73b68adb9c1b13bc (diff) | |
parent | adabf29468a3f0b3cacf23de09dde5bf0f0dec2a (diff) | |
download | obnam-e6efa258b332c93bf665f4d1529bf68a45f63cd5.tar.gz |
Merge basic encryption support.
33 files changed, 767 insertions, 93 deletions
diff --git a/blackboxtest b/blackboxtest index 663d9f9f..b90f4ede 100755 --- a/blackboxtest +++ b/blackboxtest @@ -215,7 +215,7 @@ class ObnamTestCase(unittest.TestCase): fs = obnamlib.LocalFS(self.repo) s = obnamlib.Repository(fs, obnamlib.DEFAULT_NODE_SIZE, obnamlib.DEFAULT_UPLOAD_QUEUE_SIZE, - obnamlib.DEFAULT_LRU_SIZE) + obnamlib.DEFAULT_LRU_SIZE, None) s.open_client(self.client_name) return s @@ -281,6 +281,21 @@ to keep hourly backups for three days, daily backups for a week, weekly backups for a month, and monthly backups for a year. +.\" +.SS "Using encryption" +.B obnam +can encrypt all the data it writes to the backup repository. +It uses +.BR gpg (1) +to do the encryption. +You need to create a key pair using +.B "gpg --gen-key" +(or use an existing one), +and then tell +.B obnam +about it using the +.B \-\-encrypt\-with +option. .\"--------------------------------------------------------------------- .SH OPTIONS .\" @@ -419,6 +434,12 @@ command will keep. Everything else will be removed. See above for a description of .IR POLICY . +.TP +.BR \-\-encrypt\-with =\fIKEYID +Encrypt data stored in the backup repository using +.B gpg +and the key specified with +.IR KEYID . .\" ------------------------------------------------------------------ .SH "EXIT STATUS" .B obnam diff --git a/obnamlib/__init__.py b/obnamlib/__init__.py index 6aa5cb86..b8157123 100644 --- a/obnamlib/__init__.py +++ b/obnamlib/__init__.py @@ -39,7 +39,16 @@ MAX_ID = 2**64 - 1 from sizeparse import SizeSyntaxError, UnitNameError, ByteSizeParser -from hooks import Hook, HookManager +from encryption import (generate_symmetric_key, + encrypt_symmetric, + decrypt_symmetric, + get_public_key, + Keyring, + SecretKeyring, + encrypt_with_keyring, + decrypt_with_secret_keys) + +from hooks import Hook, FilterHook, HookManager from cfg import Configuration from interp import Interpreter from pluginbase import ObnamPlugin diff --git a/obnamlib/app.py b/obnamlib/app.py index 5b80abc1..247661ee 100644 --- a/obnamlib/app.py +++ b/obnamlib/app.py @@ -27,8 +27,21 @@ class App(object): '''Main program for backup program.''' def __init__(self): - self.hooks = obnamlib.HookManager() + + self.setup_config() + + self.pm = obnamlib.PluginManager() + self.pm.locations = [self.plugins_dir()] + self.pm.plugin_arguments = (self,) + self.interp = obnamlib.Interpreter() + self.register_command = self.interp.register + + self.setup_hooks() + + self.fsf = obnamlib.VfsFactory() + + def setup_config(self): self.config = obnamlib.Configuration([]) self.config.new_string(['log'], 'name of log file (%default)') self.config['log'] = 'obnam.log' @@ -74,23 +87,23 @@ class App(object): self.config.new_list(['trace'], 'add to filename patters for which trace ' 'debugging logging happens') - - self.pm = obnamlib.PluginManager() - self.pm.locations = [self.plugins_dir()] - self.pm.plugin_arguments = (self,) - self.interp = obnamlib.Interpreter() - self.register_command = self.interp.register + def deduce_client_name(self): + return socket.gethostname() + def setup_hooks(self): + self.hooks = obnamlib.HookManager() self.hooks.new('plugins-loaded') self.hooks.new('config-loaded') self.hooks.new('shutdown') - - self.fsf = obnamlib.VfsFactory() - - def deduce_client_name(self): - return socket.gethostname() - + + # The Repository class defines some hooks, but the class + # won't be instantiated until much after plugins are enabled, + # and since all hooks must be defined when plugins are enabled, + # we create one instance here, which will immediately be destroyed. + # FIXME: This is fugly. + obnamlib.Repository(None, 1000, 1000, 100, self.hooks) + def plugins_dir(self): return os.path.join(os.path.dirname(obnamlib.__file__), 'plugins') @@ -131,3 +144,13 @@ class App(object): self.hooks.call('shutdown') logging.info('Obnam ends') + def open_repository(self, create=False): # pragma: no cover + repopath = self.config['repository'] + repofs = self.fsf.new(repopath, create=create) + repofs.connect() + return obnamlib.Repository(repofs, + self.config['node-size'], + self.config['upload-queue-size'], + self.config['lru-size'], + self.hooks) + diff --git a/obnamlib/checksumtree.py b/obnamlib/checksumtree.py index 614b15ee..dee8b8b4 100644 --- a/obnamlib/checksumtree.py +++ b/obnamlib/checksumtree.py @@ -29,11 +29,11 @@ class ChecksumTree(obnamlib.RepositoryTree): ''' def __init__(self, fs, name, checksum_length, node_size, - upload_queue_size, lru_size): + upload_queue_size, lru_size, hooks): self.fmt = '!%dsQQ' % checksum_length key_bytes = len(self.key('', 0, 0)) obnamlib.RepositoryTree.__init__(self, fs, name, key_bytes, node_size, - upload_queue_size, lru_size) + upload_queue_size, lru_size, hooks) self.keep_just_one_tree = True def key(self, checksum, chunk_id, client_id): diff --git a/obnamlib/checksumtree_tests.py b/obnamlib/checksumtree_tests.py index 90eca8a2..9fe8d3d9 100644 --- a/obnamlib/checksumtree_tests.py +++ b/obnamlib/checksumtree_tests.py @@ -27,11 +27,13 @@ class ChecksumTreeTests(unittest.TestCase): def setUp(self): self.tempdir = tempfile.mkdtemp() fs = obnamlib.LocalFS(self.tempdir) + self.hooks = obnamlib.HookManager() + self.hooks.new('repository-toplevel-init') self.checksum = hashlib.md5('foo').digest() self.tree = obnamlib.ChecksumTree(fs, 'x', len(self.checksum), obnamlib.DEFAULT_NODE_SIZE, obnamlib.DEFAULT_UPLOAD_QUEUE_SIZE, - obnamlib.DEFAULT_LRU_SIZE) + obnamlib.DEFAULT_LRU_SIZE, self) def tearDown(self): self.tree.commit() diff --git a/obnamlib/chunklist.py b/obnamlib/chunklist.py index 625b1b63..4f8abd2a 100644 --- a/obnamlib/chunklist.py +++ b/obnamlib/chunklist.py @@ -32,11 +32,11 @@ class ChunkList(obnamlib.RepositoryTree): ''' - def __init__(self, fs, node_size, upload_queue_size, lru_size): + def __init__(self, fs, node_size, upload_queue_size, lru_size, hooks): self.key_bytes = len(self.key(0)) obnamlib.RepositoryTree.__init__(self, fs, 'chunklist', self.key_bytes, node_size, upload_queue_size, - lru_size) + lru_size, hooks) self.keep_just_one_tree = True def key(self, chunk_id): diff --git a/obnamlib/chunklist_tests.py b/obnamlib/chunklist_tests.py index 60ff05cb..642732a0 100644 --- a/obnamlib/chunklist_tests.py +++ b/obnamlib/chunklist_tests.py @@ -26,10 +26,12 @@ class ChunkListTests(unittest.TestCase): def setUp(self): self.tempdir = tempfile.mkdtemp() fs = obnamlib.LocalFS(self.tempdir) + self.hooks = obnamlib.HookManager() + self.hooks.new('repository-toplevel-init') self.list = obnamlib.ChunkList(fs, obnamlib.DEFAULT_NODE_SIZE, obnamlib.DEFAULT_UPLOAD_QUEUE_SIZE, - obnamlib.DEFAULT_LRU_SIZE) + obnamlib.DEFAULT_LRU_SIZE, self) def tearDown(self): shutil.rmtree(self.tempdir) diff --git a/obnamlib/clientlist.py b/obnamlib/clientlist.py index f23b8243..c25ef6f2 100644 --- a/obnamlib/clientlist.py +++ b/obnamlib/clientlist.py @@ -36,7 +36,7 @@ class ClientList(obnamlib.RepositoryTree): ''' - def __init__(self, fs, node_size, upload_queue_size, lru_size): + def __init__(self, fs, node_size, upload_queue_size, lru_size, hooks): self.hash_len = len(self.hashfunc('')) self.fmt = '!%dsQ' % self.hash_len self.key_bytes = len(self.key('', 0)) @@ -44,7 +44,7 @@ class ClientList(obnamlib.RepositoryTree): self.maxkey = self.hashkey('\xff' * self.hash_len, obnamlib.MAX_ID) obnamlib.RepositoryTree.__init__(self, fs, 'clientlist', self.key_bytes, node_size, - upload_queue_size, lru_size) + upload_queue_size, lru_size, hooks) self.keep_just_one_tree = True def hashfunc(self, string): diff --git a/obnamlib/clientlist_tests.py b/obnamlib/clientlist_tests.py index 8c83502b..d54f46ed 100644 --- a/obnamlib/clientlist_tests.py +++ b/obnamlib/clientlist_tests.py @@ -26,10 +26,12 @@ class ClientListTests(unittest.TestCase): def setUp(self): self.tempdir = tempfile.mkdtemp() fs = obnamlib.LocalFS(self.tempdir) + self.hooks = obnamlib.HookManager() + self.hooks.new('repository-toplevel-init') self.list = obnamlib.ClientList(fs, obnamlib.DEFAULT_NODE_SIZE, obnamlib.DEFAULT_UPLOAD_QUEUE_SIZE, - obnamlib.DEFAULT_LRU_SIZE) + obnamlib.DEFAULT_LRU_SIZE, self) def tearDown(self): shutil.rmtree(self.tempdir) diff --git a/obnamlib/clientmetadatatree.py b/obnamlib/clientmetadatatree.py index 2a897729..8ea7001e 100644 --- a/obnamlib/clientmetadatatree.py +++ b/obnamlib/clientmetadatatree.py @@ -65,11 +65,12 @@ class ClientMetadataTree(obnamlib.RepositoryTree): TYPE_MAX = 255 SUBKEY_MAX = struct.pack('!Q', obnamlib.MAX_ID) - def __init__(self, fs, client_dir, node_size, upload_queue_size, lru_size): + def __init__(self, fs, client_dir, node_size, upload_queue_size, lru_size, + hooks): key_bytes = len(self.hashkey(0, self.hash_name(''), 0, 0)) obnamlib.RepositoryTree.__init__(self, fs, client_dir, key_bytes, node_size, upload_queue_size, - lru_size) + lru_size, hooks) self.genhash = self.hash_name('generation') self.known_generations = dict() self.chunkids_per_key = max(1, diff --git a/obnamlib/clientmetadatatree_tests.py b/obnamlib/clientmetadatatree_tests.py index 28e997ae..35f1f79e 100644 --- a/obnamlib/clientmetadatatree_tests.py +++ b/obnamlib/clientmetadatatree_tests.py @@ -27,10 +27,12 @@ class ClientMetadataTreeTests(unittest.TestCase): def setUp(self): self.tempdir = tempfile.mkdtemp() fs = obnamlib.LocalFS(self.tempdir) + self.hooks = obnamlib.HookManager() + self.hooks.new('repository-toplevel-init') self.client = obnamlib.ClientMetadataTree(fs, 'clientid', obnamlib.DEFAULT_NODE_SIZE, obnamlib.DEFAULT_UPLOAD_QUEUE_SIZE, - obnamlib.DEFAULT_LRU_SIZE) + obnamlib.DEFAULT_LRU_SIZE, self) def tearDown(self): shutil.rmtree(self.tempdir) @@ -138,10 +140,13 @@ class ClientMetadataTreeFileOpsTests(unittest.TestCase): def setUp(self): self.tempdir = tempfile.mkdtemp() fs = obnamlib.LocalFS(self.tempdir) + self.hooks = obnamlib.HookManager() + self.hooks.new('repository-toplevel-init') self.client = obnamlib.ClientMetadataTree(fs, 'clientid', obnamlib.DEFAULT_NODE_SIZE, obnamlib.DEFAULT_UPLOAD_QUEUE_SIZE, - obnamlib.DEFAULT_LRU_SIZE) + obnamlib.DEFAULT_LRU_SIZE, + self) self.client.start_generation() self.clientid = self.client.get_generation_id(self.client.tree) self.file_metadata = obnamlib.Metadata(st_mode=stat.S_IFREG | 0666) diff --git a/obnamlib/encryption.py b/obnamlib/encryption.py new file mode 100644 index 00000000..8c064f61 --- /dev/null +++ b/obnamlib/encryption.py @@ -0,0 +1,226 @@ +# Copyright 2011 Lars Wirzenius +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import os +import shutil +import subprocess +import tempfile + + +def generate_symmetric_key(numbits, filename='/dev/random'): + '''Generate a random key of at least numbits for symmetric encryption.''' + + bytes = (numbits + 7) / 8 + f = open(filename, 'rb') + key = f.read(bytes) + f.close() + + # Passphrase should not contain newlines. Hex encode? + + return key + + +def _gpg_pipe(args, data, passphrase): + '''Pipe things through gpg. + + With the right args, this can be either an encryption or a decryption + operation. + + For safety, we give the passphrase to gpg via a file descriptor. + The argument list is modified to include the relevant options for that. + + The data is fed to gpg via a temporary file, readable only by + the owner, to avoid congested pipes. + + ''' + + # Open pipe for passphrase, and write it there. If passphrase is + # very long (more than 4 KiB by default), this might block. A better + # implementation would be to have a loop around select(2) to do pipe + # I/O when it can be done without blocking. Patches most welcome. + + keypipe = os.pipe() + os.write(keypipe[1], passphrase + '\n') + os.close(keypipe[1]) + + # Write the data to temporary file. Remove its name at once, so that + # if we crash, it gets removed automatically by the kernel. + + datafd, dataname = tempfile.mkstemp() + os.remove(dataname) + os.write(datafd, data) + os.lseek(datafd, 0, 0) + + # Actually run gpg. + + argv = ['gpg', '--passphrase-fd', str(keypipe[0]), '-q', '--batch'] + args + p = subprocess.Popen(argv, stdin=datafd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, err = p.communicate() + + os.close(keypipe[0]) + os.close(datafd) + + # Return output data, or deal with errors. + if p.returncode: # pragma: no cover + raise Exception(err) + + return out + + +def encrypt_symmetric(cleartext, key): + '''Encrypt data with symmetric encryption.''' + return _gpg_pipe(['-c'], cleartext, key) + + +def decrypt_symmetric(encrypted, key): + '''Decrypt encrypted data with symmetric encryption.''' + return _gpg_pipe(['-d'], encrypted, key) + + +def _gpg(args, stdin='', gpghome=None): + '''Run gpg and return its output.''' + + env = dict() + env.update(os.environ) + if gpghome is not None: + env['GNUPGHOME'] = gpghome + + argv = ['gpg', '-q', '--batch'] + args + p = subprocess.Popen(argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, env=env) + out, err = p.communicate(stdin) + + # Return output data, or deal with errors. + if p.returncode: # pragma: no cover + raise Exception(err) + + return out + + +def get_public_key(keyid, gpghome=None): + '''Return the ASCII armored export form of a given public key.''' + return _gpg(['--export', '--armor', keyid], gpghome=gpghome) + + + +class Keyring(object): + + '''A simplistic representation of GnuPG keyrings. + + Just enough functionality for obnam's purposes. + + ''' + + _keyring_name = 'pubring.gpg' + + def __init__(self, encoded=''): + self._encoded = encoded + self._gpghome = None + self._keyids = None + + def _setup(self): + self._gpghome = tempfile.mkdtemp() + f = open(self._keyring, 'wb') + f.write(self._encoded) + f.close() + + def _cleanup(self): + shutil.rmtree(self._gpghome) + self._gpghome = None + + @property + def _keyring(self): + return os.path.join(self._gpghome, self._keyring_name) + + def _real_keyids(self): + output = self.gpg(False, ['--list-keys', '--with-colons']) + + keyids = [] + for line in output.splitlines(): + fields = line.split(':') + if len(fields) >= 5 and fields[0] == 'pub': + keyids.append(fields[4]) + return keyids + + def keyids(self): + if self._keyids is None: + self._keyids = self._real_keyids() + return self._keyids + + def __str__(self): + return self._encoded + + def __contains__(self, keyid): + return keyid in self.keyids() + + def _reread_keyring(self): + f = open(self._keyring, 'rb') + self._encoded = f.read() + f.close() + self._keyids = None + + def add(self, key): + self.gpg(True, ['--import'], stdin=key) + + def remove(self, keyid): + self.gpg(True, ['--delete-key', '--yes', keyid]) + + def gpg(self, reread, *args, **kwargs): + self._setup() + kwargs['gpghome'] = self._gpghome + try: + result = _gpg(*args, **kwargs) + except BaseException: # pragma: no cover + self._cleanup() + raise + else: + if reread: + self._reread_keyring() + self._cleanup() + return result + + +class SecretKeyring(Keyring): + + '''Same as Keyring, but for secret keys.''' + + _keyring_name = 'secring.gpg' + + def _real_keyids(self): + output = self.gpg(False, ['--list-secret-keys', '--with-colons']) + + keyids = [] + for line in output.splitlines(): + fields = line.split(':') + if len(fields) >= 5 and fields[0] == 'sec': + keyids.append(fields[4]) + return keyids + + +def encrypt_with_keyring(cleartext, keyring): + '''Encrypt data with all keys in a keyring.''' + recipients = [] + for keyid in keyring.keyids(): + recipients += ['-r', keyid] + return keyring.gpg(False, ['-e', '--trust-model', 'always'] + recipients, + stdin=cleartext) + + +def decrypt_with_secret_keys(encrypted, gpghome=None): + '''Decrypt data using secret keys GnuPG finds on its own.''' + return _gpg(['-d'], stdin=encrypted, gpghome=gpghome) + diff --git a/obnamlib/encryption_tests.py b/obnamlib/encryption_tests.py new file mode 100644 index 00000000..ece3a13b --- /dev/null +++ b/obnamlib/encryption_tests.py @@ -0,0 +1,154 @@ +# Copyright 2011 Lars Wirzenius +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import os +import shutil +import subprocess +import tempfile +import unittest + +import obnamlib + + +def cat(filename): + f = open(filename, 'rb') + data = f.read() + f.close() + return data + + +class SymmetricEncryptionTests(unittest.TestCase): + + # We don't test the quality of keys or encryption here. Doing that is + # hard to do well, and we'll just assume that reading /dev/random + # for keys, and using gpg for encryption, is going to work well. + # In these tests, we care about making sure we use the tools right, + # not that the tools themselves work right. + + def test_generates_key_of_correct_length(self): + numbits = 16 + key = obnamlib.generate_symmetric_key(numbits, filename='/dev/zero') + self.assertEqual(len(key) * 8, numbits) + + def test_generates_key_with_size_rounded_up(self): + numbits = 15 + key = obnamlib.generate_symmetric_key(numbits, filename='/dev/zero') + self.assertEqual(len(key), 2) + + def test_encrypts_into_different_string_than_cleartext(self): + cleartext = 'hello world' + key = 'sekr1t' + encrypted = obnamlib.encrypt_symmetric(cleartext, key) + self.assertNotEqual(cleartext, encrypted) + + def test_encrypt_decrypt_round_trip(self): + cleartext = 'hello, world' + key = 'sekr1t' + encrypted = obnamlib.encrypt_symmetric(cleartext, key) + decrypted = obnamlib.decrypt_symmetric(encrypted, key) + self.assertEqual(decrypted, cleartext) + + +class GetPublicKeyTests(unittest.TestCase): + + def setUp(self): + self.dirname = tempfile.mkdtemp() + self.gpghome = os.path.join(self.dirname, 'gpghome') + shutil.copytree('test-gpghome', self.gpghome) + self.keyid = '1B321347' + + def tearDown(self): + shutil.rmtree(self.dirname) + + def test_exports_key(self): + key = obnamlib.get_public_key(self.keyid, gpghome=self.gpghome) + self.assert_('-----BEGIN PGP PUBLIC KEY BLOCK-----' in key) + + +class KeyringTests(unittest.TestCase): + + def setUp(self): + self.keyring = obnamlib.Keyring() + self.keyid = '3B1802F81B321347' + self.key = ''' +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.10 (GNU/Linux) + +mI0ETY8gwwEEAMrSXBIJseIv9miuwnYlCd7CQCzNb8nHYkpo4o1nEQD3k/h7xj9m +/0Gd5kLfF+WLwAxSJYb41JjaKs0FeUexSGNePdNFxn2CCZ4moHH19tTlWGfqCNz7 +vcYQpSbPix+zhR7uNqilxtsIrx1iyYwh7L2VKf/KMJ7yXbT+jbAj7fqBABEBAAG0 +CFRlc3QgS2V5iLgEEwECACIFAk2PIMMCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B +AheAAAoJEDsYAvgbMhNHlEED/1UkiLJ8R3phMRnjLtn+5JobYvOi7WEubnRv1rnN +MC4MyhFiLux7Z8p3xwt1Pf2GqL7q1dD91NOx+6KS3d1PFmiM/i1fYalZPbzm1gNr +8sFK2Gxsnd7mmYf2wKIo335Bk21SCmGcNKvmKW2M6ckzPT0q/RZ2hhY9JhHUiLG4 +Lu3muI0ETY8gwwEEAMQoiBCQYky52pDamnH5c7FngCM72AkNq/z0+DHqY202gksd +Vy63TF7UGIsiCLvY787vPm62sOqYO0uI6PV5xVDGyJh4oI/g2zgNkhXRZrIB1Q+T +THp7qSmwQUZv8T+HfgxLiaXDq6oV/HWLElcMQ9ClZ3Sxzlu3ZQHrtmY5XridABEB +AAGInwQYAQIACQUCTY8gwwIbDAAKCRA7GAL4GzITR4hgBAClEurTj5n0/21pWZH0 +Ljmokwa3FM++OZxO7shc1LIVNiAKfLiPigU+XbvSeVWTeajKkvj5LCVxKQiRSiYB +Z85TYTo06kHvDCYQmFOSGrLsZxMyJCfHML5spF9+bej5cepmuNVIdJK5vlgDiVr3 +uWUO7gMi+AlnxbfXVCTEgw3xhg== +=j+6W +-----END PGP PUBLIC KEY BLOCK----- +''' + + def test_has_no_keys_initially(self): + self.assertEqual(self.keyring.keyids(), []) + self.assertEqual(str(self.keyring), '') + + def test_gets_no_keys_from_empty_encoded(self): + keyring = obnamlib.Keyring(encoded='') + self.assertEqual(keyring.keyids(), []) + + def test_adds_key(self): + self.keyring.add(self.key) + self.assertEqual(self.keyring.keyids(), [self.keyid]) + self.assert_(self.keyid in self.keyring) + + def test_removes_key(self): + self.keyring.add(self.key) + self.keyring.remove(self.keyid) + self.assertEqual(self.keyring.keyids(), []) + + def test_export_import_roundtrip_works(self): + self.keyring.add(self.key) + exported = str(self.keyring) + keyring2 = obnamlib.Keyring(exported) + self.assertEqual(keyring2.keyids(), [self.keyid]) + + +class SecretKeyringTests(unittest.TestCase): + + def test_lists_correct_key(self): + keyid = '3B1802F81B321347' + seckeys = obnamlib.SecretKeyring(cat('test-gpghome/secring.gpg')) + self.assertEqual(seckeys.keyids(), [keyid]) + + +class PublicKeyEncryptionTests(unittest.TestCase): + + def test_roundtrip_works(self): + cleartext = 'hello, world' + passphrase = 'password1' + keyring = obnamlib.Keyring(cat('test-gpghome/pubring.gpg')) + seckeys = obnamlib.SecretKeyring(cat('test-gpghome/secring.gpg')) + + encrypted = obnamlib.encrypt_with_keyring(cleartext, keyring) + decrypted = obnamlib.decrypt_with_secret_keys(encrypted, + gpghome='test-gpghome') + + self.assertEqual(decrypted, cleartext) + diff --git a/obnamlib/hooks.py b/obnamlib/hooks.py index 6ac09eff..46a73071 100644 --- a/obnamlib/hooks.py +++ b/obnamlib/hooks.py @@ -55,6 +55,25 @@ class Hook(object): self.callbacks.remove(callback_id) +class FilterHook(Hook): + + '''A hook which filters data through callbacks. + + Every hook of this type accepts a piece of data as its first argument + Each callback gets the return value of the previous one as its + argument. The caller gets the value of the final callback. + + Other arguments (with or without keywords) are passed as-is to + each callback. + + ''' + + def call_callbacks(self, data, *args, **kwargs): + for callback in self.callbacks: + data = callback(data, *args, **kwargs) + return data + + class HookManager(object): '''Manage the set of hooks the application defines.''' @@ -72,6 +91,11 @@ class HookManager(object): if name not in self.hooks: self.hooks[name] = Hook() + def new_filter(self, name): + '''Create a new filter hook.''' + if name not in self.hooks: + self.hooks[name] = FilterHook() + def add_callback(self, name, callback): '''Add a callback to a named hook.''' return self.hooks[name].add_callback(callback) @@ -82,5 +106,5 @@ class HookManager(object): def call(self, name, *args, **kwargs): '''Call callbacks for a named hook, using given arguments.''' - self.hooks[name].call_callbacks(*args, **kwargs) + return self.hooks[name].call_callbacks(*args, **kwargs) diff --git a/obnamlib/hooks_tests.py b/obnamlib/hooks_tests.py index 8f534abb..3235925d 100644 --- a/obnamlib/hooks_tests.py +++ b/obnamlib/hooks_tests.py @@ -50,7 +50,32 @@ class HookTests(unittest.TestCase): cb_id = self.hook.add_callback(self.callback) self.hook.remove_callback(cb_id) self.assertEqual(self.hook.callbacks, []) + + +class FilterHookTests(unittest.TestCase): + + def setUp(self): + self.hook = obnamlib.FilterHook() + + def callback(self, data, *args, **kwargs): + self.args = args + self.kwargs = kwargs + return data + ['callback'] + + def test_returns_argument_if_no_callbacks(self): + self.assertEqual(self.hook.call_callbacks(['foo']), ['foo']) + def test_calls_callback_and_returns_modified_data(self): + self.hook.add_callback(self.callback) + data = self.hook.call_callbacks([]) + self.assertEqual(data, ['callback']) + + def test_calls_callback_with_extra_args(self): + self.hook.add_callback(self.callback) + self.hook.call_callbacks(['data'], 'extra', kwextra='kwextra') + self.assertEqual(self.args, ('extra',)) + self.assertEqual(self.kwargs, { 'kwextra': 'kwextra' }) + class HookManagerTests(unittest.TestCase): @@ -69,6 +94,10 @@ class HookManagerTests(unittest.TestCase): def test_adds_new_hook(self): self.assert_(self.hooks.hooks.has_key('foo')) + def test_adds_new_filter_hook(self): + self.hooks.new_filter('bar') + self.assert_('bar' in self.hooks.hooks) + def test_adds_callback(self): self.hooks.add_callback('foo', self.callback) self.assertEqual(self.hooks.hooks['foo'].callbacks, [self.callback]) @@ -84,3 +113,8 @@ class HookManagerTests(unittest.TestCase): self.assertEqual(self.args, ('bar',)) self.assertEqual(self.kwargs, { 'kwarg': 'foobar' }) + def test_call_returns_value_of_callbacks(self): + self.hooks.new_filter('bar') + self.hooks.add_callback('bar', lambda data: data + 1) + self.assertEqual(self.hooks.call('bar', 1), 2) + diff --git a/obnamlib/plugins/backup_plugin.py b/obnamlib/plugins/backup_plugin.py index b4ec1f19..493a16e0 100644 --- a/obnamlib/plugins/backup_plugin.py +++ b/obnamlib/plugins/backup_plugin.py @@ -55,12 +55,7 @@ class BackupPlugin(obnamlib.ObnamPlugin): roots = self.app.config['root'] + args - repopath = self.app.config['repository'] - repofs = self.app.fsf.new(repopath, create=True) - repofs.connect() - self.repo = obnamlib.Repository(repofs, self.app.config['node-size'], - self.app.config['upload-queue-size'], - self.app.config['lru-size']) + self.repo = self.app.open_repository(create=True) client_name = self.app.config['client-name'] if client_name not in self.repo.list_clients(): @@ -107,13 +102,13 @@ class BackupPlugin(obnamlib.ObnamPlugin): self.app.hooks.call('error-message', 'Could not back up %s: %s' % (pathname, e.strerror)) - if repofs.bytes_written - last_checkpoint >= interval: + if self.repo.fs.bytes_written - last_checkpoint >= interval: logging.info('Making checkpoint') self.backup_parents('.') self.repo.commit_client(checkpoint=True) self.repo.lock_client(client_name) self.repo.start_generation() - last_checkpoint = repofs.bytes_written + last_checkpoint = self.repo.fs.bytes_written self.dump_memory_profile('at end of checkpoint') self.backup_parents('.') @@ -122,7 +117,7 @@ class BackupPlugin(obnamlib.ObnamPlugin): self.fs.close() self.repo.commit_client() - repofs.close() + self.repo.fs.close() logging.info('Backup finished.') self.dump_memory_profile('at end of backup run') diff --git a/obnamlib/plugins/encryption_plugin.py b/obnamlib/plugins/encryption_plugin.py new file mode 100644 index 00000000..30753e53 --- /dev/null +++ b/obnamlib/plugins/encryption_plugin.py @@ -0,0 +1,120 @@ +# Copyright (C) 2011 Lars Wirzenius +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import os + +import obnamlib + + +class EncryptionPlugin(obnamlib.ObnamPlugin): + + symmetric_key_bits = 8 + + def enable(self): + self.app.config.new_string(['encrypt-with'], + 'PGP key with which to encrypt data ' + 'in the backup repository') + + hooks = [ + ('repository-toplevel-init', self.toplevel_init), + ('repository-read-data', self.toplevel_read_data), + ('repository-write-data', self.toplevel_write_data), + ] + for name, callback in hooks: + self.app.hooks.add_callback(name, callback) + + self._pubkey = None + + @property + def keyid(self): + return self.app.config['encrypt-with'] + + @property + def pubkey(self): + if self._pubkey is None: + self._pubkey = obnamlib.get_public_key(self.keyid) + return self._pubkey + + def toplevel_init(self, repo, toplevel): + '''Initialize a new toplevel for encryption.''' + + if not self.keyid: + return + + pubkeys = obnamlib.Keyring() + pubkeys.add(self.pubkey) + + symmetric_key = obnamlib.generate_symmetric_key(self.symmetric_key_bits) + encrypted = obnamlib.encrypt_with_keyring(symmetric_key, pubkeys) + repo.fs.fs.write_file(os.path.join(toplevel, 'key'), encrypted) + + encoded = str(pubkeys) + encrypted = obnamlib.encrypt_symmetric(encoded, symmetric_key) + repo.fs.fs.write_file(os.path.join(toplevel, 'userkeys'), encrypted) + + def toplevel_read_data(self, encrypted, repo, toplevel): + if not self.keyid: + return encrypted + symmetric_key = self.get_symmetric_key(repo, toplevel) + return obnamlib.decrypt_symmetric(encrypted, symmetric_key) + + def toplevel_write_data(self, cleartext, repo, toplevel): + if not self.keyid: + return cleartext + symmetric_key = self.get_symmetric_key(repo, toplevel) + return obnamlib.encrypt_symmetric(cleartext, symmetric_key) + + def get_symmetric_key(self, repo, toplevel): + encoded = repo.fs.fs.cat(os.path.join(toplevel, 'key')) + return obnamlib.decrypt_with_secret_keys(encoded) + + def read_keyring(self, repo, toplevel): + encrypted = repo.fs.fs.cat(os.path.join(toplevel, 'userkeys')) + encoded = self.toplevel_read_data(encrypted, repo, toplevel) + return obnamlib.Keyring(encoded=encoded) + + def write_keyring(self, repo, toplevel, keyring): + encoded = str(keyring) + encrypted = self.toplevel_write_data(encoded, repo, toplevel) + pathname = os.path.join(toplevel, 'userkeys') + repo.fs.fs.overwrite_file(pathname, encrypted) + + def add_to_userkeys(self, repo, toplevel, public_key): + userkeys = self.read_keyring(repo, toplevel) + userkeys.add(public_key) + self.write_keyring(repo, toplevel, userkeys) + + def remove_from_userkeys(self, repo, toplevel, keyid): + userkeys = self.read_keyring(repo, toplevel) + if keyid in userkeys: + userkeys.remove(keyid) + self.write_keyring(repo, toplevel, userkeys) + + def add_client(self, repo, client_public_key): + self.add_to_userkeys(repo, 'metadata', client_public_key) + self.add_to_userkeys(repo, 'clientlist', client_public_key) + self.add_to_userkeys(repo, 'chunks', client_public_key) + self.add_to_userkeys(repo, 'chunksums', client_public_key) + # client will add itself to the clientlist and create its own toplevel + + def remove_client(self, repo, client_keyid): + # client may remove itself, since it has access to the symmetric keys + # we assume the client-specific toplevel has already been removed + self.remove_from_userkeys(repo, 'chunksums', client_keyid) + self.remove_from_userkeys(repo, 'chunks', client_keyid) + self.remove_from_userkeys(repo, 'clientlist', client_keyid) + self.remove_from_userkeys(repo, 'metadata', client_keyid) + diff --git a/obnamlib/plugins/force_lock_plugin.py b/obnamlib/plugins/force_lock_plugin.py index 83368fd9..ddf0971e 100644 --- a/obnamlib/plugins/force_lock_plugin.py +++ b/obnamlib/plugins/force_lock_plugin.py @@ -35,11 +35,7 @@ class ForceLockPlugin(obnamlib.ObnamPlugin): logging.info('Repository: %s' % repourl) logging.info('Client: %s' % client_name) - repofs = self.app.fsf.new(repourl) - repofs.connect() - repo = obnamlib.Repository(repofs, self.app.config['node-size'], - self.app.config['upload-queue-size'], - self.app.config['lru-size']) + repo = self.app.open_repository() if client_name not in repo.list_clients(): logging.warning('Client does not exist in repository.') @@ -48,9 +44,9 @@ class ForceLockPlugin(obnamlib.ObnamPlugin): client_id = repo.clientlist.get_client_id(client_name) client_dir = repo.client_dir(client_id) lockname = os.path.join(client_dir, 'lock') - if repofs.exists(lockname): + if repo.fs.exists(lockname): logging.info('Removing lockfile %s' % lockname) - repofs.remove(lockname) + repo.fs.remove(lockname) else: logging.info('Client is not locked') diff --git a/obnamlib/plugins/forget_plugin.py b/obnamlib/plugins/forget_plugin.py index 8fdcc2f6..2ef31441 100644 --- a/obnamlib/plugins/forget_plugin.py +++ b/obnamlib/plugins/forget_plugin.py @@ -33,11 +33,7 @@ class ForgetPlugin(obnamlib.ObnamPlugin): self.app.config.require('repository') self.app.config.require('client-name') - fs = self.app.fsf.new(self.app.config['repository']) - fs.connect() - self.repo = obnamlib.Repository(fs, self.app.config['node-size'], - self.app.config['upload-queue-size'], - self.app.config['lru-size']) + self.repo = self.app.open_repository() self.repo.lock_client(self.app.config['client-name']) if args: diff --git a/obnamlib/plugins/fsck_plugin.py b/obnamlib/plugins/fsck_plugin.py index 031f83af..89f5155a 100644 --- a/obnamlib/plugins/fsck_plugin.py +++ b/obnamlib/plugins/fsck_plugin.py @@ -28,13 +28,7 @@ class FsckPlugin(obnamlib.ObnamPlugin): def fsck(self, args): self.app.config.require('repository') logging.debug('fsck on %s' % self.app.config['repository']) - - repofs = self.app.fsf.new(self.app.config['repository']) - repofs.connect() - self.repo = obnamlib.Repository(repofs, self.app.config['node-size'], - self.app.config['upload-queue-size'], - self.app.config['lru-size']) - + self.repo = self.app.open_repository() self.check_root() def check_root(self): diff --git a/obnamlib/plugins/restore_plugin.py b/obnamlib/plugins/restore_plugin.py index e0e34a13..8640ad3d 100644 --- a/obnamlib/plugins/restore_plugin.py +++ b/obnamlib/plugins/restore_plugin.py @@ -80,11 +80,7 @@ class RestorePlugin(obnamlib.ObnamPlugin): logging.debug('no args given, so restoring everything') args = ['/'] - repofs = self.app.fsf.new(self.app.config['repository']) - repofs.connect() - self.repo = obnamlib.Repository(repofs, self.app.config['node-size'], - self.app.config['upload-queue-size'], - self.app.config['lru-size']) + self.repo = self.app.open_repository() self.repo.open_client(self.app.config['client-name']) self.fs = self.app.fsf.new(self.app.config['to'], create=True) self.fs.connect() diff --git a/obnamlib/plugins/show_plugin.py b/obnamlib/plugins/show_plugin.py index 9f14f71c..387001de 100644 --- a/obnamlib/plugins/show_plugin.py +++ b/obnamlib/plugins/show_plugin.py @@ -40,11 +40,7 @@ class ShowPlugin(obnamlib.ObnamPlugin): def open_repository(self): self.app.config.require('repository') self.app.config.require('client-name') - fs = self.app.fsf.new(self.app.config['repository']) - fs.connect() - self.repo = obnamlib.Repository(fs, self.app.config['node-size'], - self.app.config['upload-queue-size'], - self.app.config['lru-size']) + self.repo = self.app.open_repository() self.repo.open_client(self.app.config['client-name']) def clients(self, args): diff --git a/obnamlib/plugins/verify_plugin.py b/obnamlib/plugins/verify_plugin.py index 1d6b2755..4b3bd3ea 100644 --- a/obnamlib/plugins/verify_plugin.py +++ b/obnamlib/plugins/verify_plugin.py @@ -49,11 +49,7 @@ class VerifyPlugin(obnamlib.ObnamPlugin): args = ['/'] logging.debug('verifying what: %s' % repr(args)) - fs = self.app.fsf.new(self.app.config['repository']) - fs.connect() - self.repo = obnamlib.Repository(fs, self.app.config['node-size'], - self.app.config['upload-queue-size'], - self.app.config['lru-size']) + self.repo = self.app.open_repository() self.repo.open_client(self.app.config['client-name']) self.fs = self.app.fsf.new(args[0]) self.fs.connect() diff --git a/obnamlib/repo.py b/obnamlib/repo.py index 95b82e22..c0423f01 100644 --- a/obnamlib/repo.py +++ b/obnamlib/repo.py @@ -129,6 +129,44 @@ def decode_metadata(encoded): return obnamlib.Metadata(**args) +class HookedFS(object): + + '''A class to filter read/written data through hooks.''' + + def __init__(self, repo, fs, hooks): + self.repo = repo + self.fs = fs + self.hooks = hooks + + def __getattr__(self, name): + return getattr(self.fs, name) + + def _get_toplevel(self, filename): + parts = filename.split(os.sep) + if len(parts) > 1: + return parts[0] + else: # pragma: no cover + raise obnamlib.Error('File at repository root: %s' % filename) + + def cat(self, filename): + data = self.fs.cat(filename) + toplevel = self._get_toplevel(filename) + return self.hooks.call('repository-read-data', data, + repo=self.repo, toplevel=toplevel) + + def write_file(self, filename, data): + toplevel = self._get_toplevel(filename) + data = self.hooks.call('repository-write-data', data, + repo=self.repo, toplevel=toplevel) + self.fs.write_file(filename, data) + + def overwrite_file(self, filename, data): + toplevel = self._get_toplevel(filename) + data = self.hooks.call('repository-write-data', data, + repo=self.repo, toplevel=toplevel) + self.fs.overwrite_file(filename, data) + + class Repository(object): '''Repository for backup data. @@ -164,14 +202,16 @@ class Repository(object): format_version = 1 - def __init__(self, fs, node_size, upload_queue_size, lru_size): - self.fs = fs + def __init__(self, fs, node_size, upload_queue_size, lru_size, hooks): + self.setup_hooks(hooks or obnamlib.HookManager()) + self.fs = HookedFS(self, fs, self.hooks) self.node_size = node_size self.upload_queue_size = upload_queue_size self.lru_size = lru_size self.got_root_lock = False - self.clientlist = obnamlib.ClientList(fs, node_size, upload_queue_size, - lru_size) + self.clientlist = obnamlib.ClientList(self.fs, node_size, + upload_queue_size, + lru_size, self) self.got_client_lock = False self.client_lockfile = None self.current_client = None @@ -181,14 +221,22 @@ class Repository(object): self.removed_clients = [] self.removed_generations = [] self.client = None - self.chunklist = obnamlib.ChunkList(fs, node_size, upload_queue_size, - lru_size) - self.chunksums = obnamlib.ChecksumTree(fs, 'chunksums', + self.chunklist = obnamlib.ChunkList(self.fs, node_size, + upload_queue_size, + lru_size, self) + self.chunksums = obnamlib.ChecksumTree(self.fs, 'chunksums', len(self.checksum('')), node_size, upload_queue_size, - lru_size) + lru_size, self) self.prev_chunkid = None + def setup_hooks(self, hooks): + self.hooks = hooks + + self.hooks.new('repository-toplevel-init') + self.hooks.new_filter('repository-read-data') + self.hooks.new_filter('repository-write-data') + def checksum(self, data): '''Return checksum of data. @@ -236,7 +284,7 @@ class Repository(object): self.check_format_version() try: - self.fs.write_file('root.lock', '') + self.fs.fs.write_file('root.lock', '') except OSError, e: if e.errno == errno.EEXIST: raise LockFail('Lock file root.lock already exists') @@ -285,6 +333,9 @@ class Repository(object): def _write_format_version(self, version): '''Write the desired format version to the repository.''' + if not self.fs.exists('metadata'): + self.fs.mkdir('metadata') + self.hooks.call('repository-toplevel-init', self, 'metadata') self.fs.overwrite_file('metadata/format', '%s\n' % version) def check_format_version(self): @@ -334,7 +385,10 @@ class Repository(object): if client_id is None: raise LockFail('client %s does not exit' % client_name) - client_dir = self.client_dir(client_id) + client_dir = self.client_dir(client_id) + if not self.fs.exists(client_dir): + self.fs.mkdir(client_dir) + self.hooks.call('repository-toplevel-init', self, client_dir) lockname = os.path.join(client_dir, 'lock') try: self.fs.write_file(lockname, '') @@ -350,7 +404,7 @@ class Repository(object): self.client = obnamlib.ClientMetadataTree(self.fs, client_dir, self.node_size, self.upload_queue_size, - self.lru_size) + self.lru_size, self) self.client.init_forest() @require_client_lock @@ -393,7 +447,7 @@ class Repository(object): self.client = obnamlib.ClientMetadataTree(self.fs, client_dir, self.node_size, self.upload_queue_size, - self.lru_size) + self.lru_size, self) self.client.init_forest() @require_open_client @@ -536,6 +590,9 @@ class Repository(object): break self.prev_chunkid = random_chunkid() # pragma: no cover self.prev_chunkid = chunkid + if not self.fs.exists('chunks'): + self.fs.mkdir('chunks') + self.hooks.call('repository-toplevel-init', self, 'chunks') dirname = os.path.dirname(filename) if not self.fs.exists(dirname): self.fs.makedirs(dirname) diff --git a/obnamlib/repo_tests.py b/obnamlib/repo_tests.py index 1d260b9e..67f3e30a 100644 --- a/obnamlib/repo_tests.py +++ b/obnamlib/repo_tests.py @@ -68,12 +68,12 @@ class RepositoryRootNodeTests(unittest.TestCase): self.fs = obnamlib.LocalFS(self.tempdir) self.repo = obnamlib.Repository(self.fs, obnamlib.DEFAULT_NODE_SIZE, obnamlib.DEFAULT_UPLOAD_QUEUE_SIZE, - obnamlib.DEFAULT_LRU_SIZE) + obnamlib.DEFAULT_LRU_SIZE, None) self.otherfs = obnamlib.LocalFS(self.tempdir) self.other = obnamlib.Repository(self.fs, obnamlib.DEFAULT_NODE_SIZE, obnamlib.DEFAULT_UPLOAD_QUEUE_SIZE, - obnamlib.DEFAULT_LRU_SIZE) + obnamlib.DEFAULT_LRU_SIZE, None) def tearDown(self): shutil.rmtree(self.tempdir) @@ -185,7 +185,7 @@ class RepositoryRootNodeTests(unittest.TestCase): self.repo.commit_root() s2 = obnamlib.Repository(self.fs, obnamlib.DEFAULT_NODE_SIZE, obnamlib.DEFAULT_UPLOAD_QUEUE_SIZE, - obnamlib.DEFAULT_LRU_SIZE) + obnamlib.DEFAULT_LRU_SIZE, None) self.assertEqual(s2.list_clients(), ['foo']) def test_adding_existing_client_fails(self): @@ -252,7 +252,7 @@ class RepositoryClientTests(unittest.TestCase): self.fs = obnamlib.LocalFS(self.tempdir) self.repo = obnamlib.Repository(self.fs, obnamlib.DEFAULT_NODE_SIZE, obnamlib.DEFAULT_UPLOAD_QUEUE_SIZE, - obnamlib.DEFAULT_LRU_SIZE) + obnamlib.DEFAULT_LRU_SIZE, None) self.repo.lock_root() self.repo.add_client('client_name') self.repo.commit_root() @@ -261,7 +261,7 @@ class RepositoryClientTests(unittest.TestCase): self.other = obnamlib.Repository(self.otherfs, obnamlib.DEFAULT_NODE_SIZE, obnamlib.DEFAULT_UPLOAD_QUEUE_SIZE, - obnamlib.DEFAULT_LRU_SIZE) + obnamlib.DEFAULT_LRU_SIZE, None) self.dir_meta = obnamlib.Metadata() self.dir_meta.st_mode = stat.S_IFDIR | 0777 @@ -536,7 +536,7 @@ class RepositoryChunkTests(unittest.TestCase): self.fs = obnamlib.LocalFS(self.tempdir) self.repo = obnamlib.Repository(self.fs, obnamlib.DEFAULT_NODE_SIZE, obnamlib.DEFAULT_UPLOAD_QUEUE_SIZE, - obnamlib.DEFAULT_LRU_SIZE) + obnamlib.DEFAULT_LRU_SIZE, None) self.repo.lock_root() self.repo.add_client('client_name') self.repo.commit_root() @@ -605,7 +605,7 @@ class RepositoryGetSetChunksTests(unittest.TestCase): self.fs = obnamlib.LocalFS(self.tempdir) self.repo = obnamlib.Repository(self.fs, obnamlib.DEFAULT_NODE_SIZE, obnamlib.DEFAULT_UPLOAD_QUEUE_SIZE, - obnamlib.DEFAULT_LRU_SIZE) + obnamlib.DEFAULT_LRU_SIZE, None) self.repo.lock_root() self.repo.add_client('client_name') self.repo.commit_root() @@ -646,7 +646,7 @@ class RepositoryGenspecTests(unittest.TestCase): fs = obnamlib.LocalFS(repodir) self.repo = obnamlib.Repository(fs, obnamlib.DEFAULT_NODE_SIZE, obnamlib.DEFAULT_UPLOAD_QUEUE_SIZE, - obnamlib.DEFAULT_LRU_SIZE) + obnamlib.DEFAULT_LRU_SIZE, None) self.repo.lock_root() self.repo.add_client('client_name') self.repo.commit_root() diff --git a/obnamlib/repo_tree.py b/obnamlib/repo_tree.py index d57898d0..d24d92c2 100644 --- a/obnamlib/repo_tree.py +++ b/obnamlib/repo_tree.py @@ -35,13 +35,14 @@ class RepositoryTree(object): ''' def __init__(self, fs, dirname, key_bytes, node_size, upload_queue_size, - lru_size): + lru_size, repo): self.fs = fs self.dirname = dirname self.key_bytes = key_bytes self.node_size = node_size self.upload_queue_size = upload_queue_size self.lru_size = lru_size + self.repo = repo self.forest = None self.tree = None self.keep_just_one_tree = False @@ -62,6 +63,8 @@ class RepositoryTree(object): def start_changes(self): if not self.fs.exists(self.dirname): self.fs.mkdir(self.dirname) + self.repo.hooks.call('repository-toplevel-init', self.repo, + self.dirname) self.init_forest() assert self.forest is not None if self.tree is None: diff --git a/test-encrypted-repo b/test-encrypted-repo new file mode 100755 index 00000000..f62d06db --- /dev/null +++ b/test-encrypted-repo @@ -0,0 +1,21 @@ +#!/bin/sh + +set -e + +cmd="./obnam --repository=temp.repo --log=temp.log --log-level=debug" +cmd="$cmd --encrypt-with=1B321347" + +rm -rf temp.gpghome temp.data temp.repo temp.restored temp.log + +cp -a test-gpghome temp.gpghome +export GNUPGHOME=temp.gpghome + +cp -a debian temp.data +summain -r temp.data > temp.data.manifest + +$cmd backup temp.data +$cmd generations +$cmd restore --generation latest --to temp.restored +summain -r temp.restored/$(pwd)/temp.data > temp.restored.manifest +diff -u temp.data.manifest temp.restored.manifest + diff --git a/test-gpghome/pubring.gpg b/test-gpghome/pubring.gpg Binary files differnew file mode 100644 index 00000000..443501c5 --- /dev/null +++ b/test-gpghome/pubring.gpg diff --git a/test-gpghome/random_seed b/test-gpghome/random_seed Binary files differnew file mode 100644 index 00000000..ec7856e8 --- /dev/null +++ b/test-gpghome/random_seed diff --git a/test-gpghome/secring.gpg b/test-gpghome/secring.gpg Binary files differnew file mode 100644 index 00000000..392a864c --- /dev/null +++ b/test-gpghome/secring.gpg diff --git a/test-gpghome/trustdb.gpg b/test-gpghome/trustdb.gpg Binary files differnew file mode 100644 index 00000000..866db949 --- /dev/null +++ b/test-gpghome/trustdb.gpg diff --git a/without-tests b/without-tests index 844b9e49..cbad0065 100644 --- a/without-tests +++ b/without-tests @@ -22,3 +22,4 @@ ./obnamlib/plugins/__init__.py ./obnamlib/repo_tree.py +./obnamlib/plugins/encryption_plugin.py |