summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2011-04-05 18:55:58 +0100
committerLars Wirzenius <liw@liw.fi>2011-04-05 18:55:58 +0100
commite6efa258b332c93bf665f4d1529bf68a45f63cd5 (patch)
tree90a72fd97df9398aa7f06baf626f9a3bd91f82a4
parent17c0d9062ce45848571d91bc73b68adb9c1b13bc (diff)
parentadabf29468a3f0b3cacf23de09dde5bf0f0dec2a (diff)
downloadobnam-e6efa258b332c93bf665f4d1529bf68a45f63cd5.tar.gz
Merge basic encryption support.
-rwxr-xr-xblackboxtest2
-rw-r--r--obnam.121
-rw-r--r--obnamlib/__init__.py11
-rw-r--r--obnamlib/app.py49
-rw-r--r--obnamlib/checksumtree.py4
-rw-r--r--obnamlib/checksumtree_tests.py4
-rw-r--r--obnamlib/chunklist.py4
-rw-r--r--obnamlib/chunklist_tests.py4
-rw-r--r--obnamlib/clientlist.py4
-rw-r--r--obnamlib/clientlist_tests.py4
-rw-r--r--obnamlib/clientmetadatatree.py5
-rw-r--r--obnamlib/clientmetadatatree_tests.py9
-rw-r--r--obnamlib/encryption.py226
-rw-r--r--obnamlib/encryption_tests.py154
-rw-r--r--obnamlib/hooks.py26
-rw-r--r--obnamlib/hooks_tests.py34
-rw-r--r--obnamlib/plugins/backup_plugin.py13
-rw-r--r--obnamlib/plugins/encryption_plugin.py120
-rw-r--r--obnamlib/plugins/force_lock_plugin.py10
-rw-r--r--obnamlib/plugins/forget_plugin.py6
-rw-r--r--obnamlib/plugins/fsck_plugin.py8
-rw-r--r--obnamlib/plugins/restore_plugin.py6
-rw-r--r--obnamlib/plugins/show_plugin.py6
-rw-r--r--obnamlib/plugins/verify_plugin.py6
-rw-r--r--obnamlib/repo.py81
-rw-r--r--obnamlib/repo_tests.py16
-rw-r--r--obnamlib/repo_tree.py5
-rwxr-xr-xtest-encrypted-repo21
-rw-r--r--test-gpghome/pubring.gpgbin0 -> 651 bytes
-rw-r--r--test-gpghome/random_seedbin0 -> 600 bytes
-rw-r--r--test-gpghome/secring.gpgbin0 -> 1314 bytes
-rw-r--r--test-gpghome/trustdb.gpgbin0 -> 1280 bytes
-rw-r--r--without-tests1
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
diff --git a/obnam.1 b/obnam.1
index 08ff4fa2..5cce71be 100644
--- a/obnam.1
+++ b/obnam.1
@@ -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
new file mode 100644
index 00000000..443501c5
--- /dev/null
+++ b/test-gpghome/pubring.gpg
Binary files differ
diff --git a/test-gpghome/random_seed b/test-gpghome/random_seed
new file mode 100644
index 00000000..ec7856e8
--- /dev/null
+++ b/test-gpghome/random_seed
Binary files differ
diff --git a/test-gpghome/secring.gpg b/test-gpghome/secring.gpg
new file mode 100644
index 00000000..392a864c
--- /dev/null
+++ b/test-gpghome/secring.gpg
Binary files differ
diff --git a/test-gpghome/trustdb.gpg b/test-gpghome/trustdb.gpg
new file mode 100644
index 00000000..866db949
--- /dev/null
+++ b/test-gpghome/trustdb.gpg
Binary files differ
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