Merge lp:~mterry/duplicity/ropath.index into lp:duplicity/0.6

Proposed by Michael Terry
Status: Merged
Merged at revision: 869
Proposed branch: lp:~mterry/duplicity/ropath.index
Merge into: lp:duplicity/0.6
Diff against target: 364 lines (+131/-70)
4 files modified
bin/duplicity (+2/-1)
duplicity/diffdir.py (+4/-2)
duplicity/dup_temp.py (+5/-2)
testing/tests/restarttest.py (+120/-65)
To merge this branch: bzr merge lp:~mterry/duplicity/ropath.index
Reviewer Review Type Date Requested Status
duplicity-team Pending
Review via email: mp+127007@code.launchpad.net

Description of the change

This branch does two main things:

1) Skips base dir entries when compiling the list of deleted delta iters. (this gracefully recovers from the sort of situations that lead to bug 929067). I'm reasonably confident this is an uninvasive change, but please confirm.

2) Overwrites the sigtar file on backup-restart. This is because AFAICT, duplicity will rewrite the entire sigtar each restart. But we were opening the sigtar file as "ab", so we'd just dump the contents on top of the previous contents. Which was causing some confusion in bug 929067. If I'm wrong that we don't always rewrite the entire sigtar each time, this needs some rethink. Please also confirm that. :)

In addition, I add two tests for the above two changes and make some improvements elsewhere in the restarttest.py file while I was at it.

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/duplicity'
2--- bin/duplicity 2012-09-13 14:08:52 +0000
3+++ bin/duplicity 2012-09-28 15:53:25 +0000
4@@ -472,7 +472,8 @@
5 fh = dup_temp.get_fileobj_duppath(globals.archive_dir,
6 part_sig_filename,
7 perm_sig_filename,
8- remote_sig_filename)
9+ remote_sig_filename,
10+ overwrite=True)
11 return fh
12
13
14
15=== modified file 'duplicity/diffdir.py'
16--- duplicity/diffdir.py 2011-10-05 14:27:04 +0000
17+++ duplicity/diffdir.py 2012-09-28 15:53:25 +0000
18@@ -189,8 +189,10 @@
19 log.Debug(_("Comparing %s and %s") % (new_path and new_path.index,
20 sig_path and sig_path.index))
21 if not new_path or not new_path.type:
22- # file doesn't exist
23- if sig_path and sig_path.exists():
24+ # File doesn't exist (but ignore attempts to delete base dir;
25+ # old versions of duplicity could have written out the sigtar in
26+ # such a way as to fool us; LP: #929067)
27+ if sig_path and sig_path.exists() and sig_path.index != ():
28 # but signature says it did
29 log.Info(_("D %s") %
30 (sig_path.get_relative_path(),),
31
32=== modified file 'duplicity/dup_temp.py'
33--- duplicity/dup_temp.py 2011-10-08 15:55:26 +0000
34+++ duplicity/dup_temp.py 2012-09-28 15:53:25 +0000
35@@ -60,7 +60,7 @@
36 return fh
37
38
39-def get_fileobj_duppath(dirpath, partname, permname, remname):
40+def get_fileobj_duppath(dirpath, partname, permname, remname, overwrite=False):
41 """
42 Return a file object open for writing, will write to filename
43
44@@ -76,7 +76,10 @@
45 partname = partname, permname = permname, remname = remname)
46 else:
47 dp = path.DupPath(dirpath.name, index = (partname,))
48- fh = FileobjHooked(dp.filtered_open("ab"), tdp = None, dirpath = dirpath,
49+ mode = "ab"
50+ if overwrite:
51+ mode = "wb"
52+ fh = FileobjHooked(dp.filtered_open(mode), tdp = None, dirpath = dirpath,
53 partname = partname, permname = permname, remname = remname)
54
55 def rename_and_forget():
56
57=== modified file 'testing/tests/restarttest.py'
58--- testing/tests/restarttest.py 2011-12-18 12:49:41 +0000
59+++ testing/tests/restarttest.py 2012-09-28 15:53:25 +0000
60@@ -21,6 +21,8 @@
61
62 import helper
63 import sys, os, unittest
64+import glob
65+import subprocess
66
67 import duplicity.backend
68 from duplicity import path
69@@ -41,7 +43,9 @@
70
71 class CmdError(Exception):
72 """Indicates an error running an external command"""
73- pass
74+ def __init__(self, code):
75+ Exception.__init__(self, code)
76+ self.exit_status = code
77
78 class RestartTest(unittest.TestCase):
79 """
80@@ -49,6 +53,14 @@
81 """
82 def setUp(self):
83 assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1")
84+ assert not os.system("rm -rf testfiles/output "
85+ "testfiles/restore_out testfiles/cache")
86+ assert not os.system("mkdir testfiles/output testfiles/cache")
87+ backend = duplicity.backend.get_backend(backend_url)
88+ bl = backend.list()
89+ if bl:
90+ backend.delete(backend.list())
91+ backend.close()
92
93 def tearDown(self):
94 assert not os.system("rm -rf testfiles tempdir temp2.tar")
95@@ -72,7 +84,7 @@
96 # print "CMD: %s" % cmdline
97 return_val = os.system(cmdline)
98 if return_val:
99- raise CmdError(return_val)
100+ raise CmdError(os.WEXITSTATUS(return_val))
101
102 def backup(self, type, input_dir, options = [], current_time = None):
103 """Run duplicity backup to default directory"""
104@@ -103,25 +115,11 @@
105 options.extend(['--restore-time', str(time)])
106 self.run_duplicity(args, options, current_time)
107
108- def deltmp(self):
109- """
110- Delete temporary directories
111- """
112- assert not os.system("rm -rf testfiles/output "
113- "testfiles/restore_out testfiles/cache")
114- assert not os.system("mkdir testfiles/output testfiles/cache")
115- backend = duplicity.backend.get_backend(backend_url)
116- bl = backend.list()
117- if bl:
118- backend.delete(backend.list())
119- backend.close()
120-
121 def runtest(self, dirlist, backup_options = [], restore_options = []):
122 """
123 Run backup/restore test on directories in dirlist
124 """
125 assert len(dirlist) >= 1
126- self.deltmp()
127
128 # Back up directories to local backend
129 current_time = 100000
130@@ -142,6 +140,12 @@
131 self.verify(dirname,
132 time = current_time, options = restore_options)
133
134+ def make_largefiles(self):
135+ # create 3 2M files
136+ assert not os.system("mkdir testfiles/largefiles")
137+ for n in (1,2,3):
138+ assert not os.system("dd if=/dev/urandom of=testfiles/largefiles/file%d bs=1024 count=2048 > /dev/null 2>&1" % n)
139+
140 def check_same(self, filename1, filename2):
141 """
142 Verify two filenames are the same
143@@ -153,14 +157,14 @@
144 """
145 Test basic Checkpoint/Restart
146 """
147- excludes = ["--exclude **/output",
148- "--exclude **/cache",]
149- self.deltmp()
150+ excludes = ["--exclude '**/output'",
151+ "--exclude '**/cache'",]
152 # we know we're going to fail this one, its forced
153 try:
154 self.backup("full", "testfiles", options = ["--vol 1", "--fail 1"] + excludes)
155- except CmdError:
156- pass
157+ self.fail()
158+ except CmdError, e:
159+ self.assertEqual(30, e.exit_status)
160 # this one should pass OK
161 self.backup("full", "testfiles", options = excludes)
162 self.verify("testfiles", options = excludes)
163@@ -169,39 +173,42 @@
164 """
165 Test multiple Checkpoint/Restart
166 """
167- excludes = ["--exclude **/output",
168- "--exclude **/cache",]
169- self.deltmp()
170+ excludes = ["--exclude '**/output'",
171+ "--exclude '**/cache'",]
172+ self.make_largefiles()
173 # we know we're going to fail these, they are forced
174 try:
175- self.backup("full", "testfiles", options = ["--vol 1", "--fail 1"] + excludes)
176- except CmdError:
177- pass
178- try:
179- self.backup("full", "testfiles", options = ["--vol 1", "--fail 2"] + excludes)
180- except CmdError:
181- pass
182- try:
183- self.backup("full", "testfiles", options = ["--vol 1", "--fail 3"] + excludes)
184- except CmdError:
185- pass
186+ self.backup("full", "testfiles/largefiles", options = ["--vol 1", "--fail 1"] + excludes)
187+ self.fail()
188+ except CmdError, e:
189+ self.assertEqual(30, e.exit_status)
190+ try:
191+ self.backup("full", "testfiles/largefiles", options = ["--vol 1", "--fail 2"] + excludes)
192+ self.fail()
193+ except CmdError, e:
194+ self.assertEqual(30, e.exit_status)
195+ try:
196+ self.backup("full", "testfiles/largefiles", options = ["--vol 1", "--fail 3"] + excludes)
197+ self.fail()
198+ except CmdError, e:
199+ self.assertEqual(30, e.exit_status)
200 # this one should pass OK
201- self.backup("full", "testfiles", options = excludes)
202- self.verify("testfiles", options = excludes)
203+ self.backup("full", "testfiles/largefiles", options = excludes)
204+ self.verify("testfiles/largefiles", options = excludes)
205
206 def test_first_volume_failure(self):
207 """
208 Test restart when no volumes are available on the remote.
209 Caused when duplicity fails before the first transfer.
210 """
211- excludes = ["--exclude **/output",
212- "--exclude **/cache",]
213- self.deltmp()
214+ excludes = ["--exclude '**/output'",
215+ "--exclude '**/cache'",]
216 # we know we're going to fail these, they are forced
217 try:
218 self.backup("full", "testfiles", options = ["--vol 1", "--fail 1"] + excludes)
219- except CmdError:
220- pass
221+ self.fail()
222+ except CmdError, e:
223+ self.assertEqual(30, e.exit_status)
224 assert not os.system("rm testfiles/output/duplicity-full*difftar*")
225 # this one should pass OK
226 self.backup("full", "testfiles", options = excludes)
227@@ -213,12 +220,12 @@
228 than the local manifest has on record. Caused when duplicity
229 fails the last queued transfer(s).
230 """
231- self.deltmp()
232 # we know we're going to fail these, they are forced
233 try:
234 self.backup("full", "/bin", options = ["--vol 1", "--fail 3"])
235- except CmdError:
236- pass
237+ self.fail()
238+ except CmdError, e:
239+ self.assertEqual(30, e.exit_status)
240 assert not os.system("rm testfiles/output/duplicity-full*vol[23].difftar*")
241 # this one should pass OK
242 self.backup("full", "/bin", options = ["--vol 1"])
243@@ -230,16 +237,13 @@
244 Caused when the user deletes a file after a failure. This test puts
245 the file in the middle of the backup, with files following.
246 """
247- self.deltmp()
248- # create 3 2M files
249- assert not os.system("mkdir testfiles/largefiles")
250- for n in (1,2,3):
251- assert not os.system("dd if=/dev/urandom of=testfiles/largefiles/file%d bs=1024 count=2048 > /dev/null 2>&1" % n)
252+ self.make_largefiles()
253 # we know we're going to fail, it's forced
254 try:
255 self.backup("full", "testfiles/largefiles", options = ["--vol 1", "--fail 3"])
256- except CmdError:
257- pass
258+ self.fail()
259+ except CmdError, e:
260+ self.assertEqual(30, e.exit_status)
261 assert not os.system("rm testfiles/largefiles/file2")
262 # this one should pass OK
263 self.backup("full", "testfiles/largefiles", options = ["--vol 1"])
264@@ -253,16 +257,13 @@
265 Caused when the user deletes a file after a failure. This test puts
266 the file at the end of the backup, with no files following.
267 """
268- self.deltmp()
269- # create 3 2M files
270- assert not os.system("mkdir testfiles/largefiles")
271- for n in (1,2,3):
272- assert not os.system("dd if=/dev/urandom of=testfiles/largefiles/file%d bs=1024 count=2048 > /dev/null 2>&1" % n)
273+ self.make_largefiles()
274 # we know we're going to fail, it's forced
275 try:
276 self.backup("full", "testfiles/largefiles", options = ["--vol 1", "--fail 6"])
277- except CmdError:
278- pass
279+ self.fail()
280+ except CmdError, e:
281+ self.assertEqual(30, e.exit_status)
282 assert not os.system("rm testfiles/largefiles/file3")
283 # this one should pass OK
284 self.backup("full", "testfiles/largefiles", options = ["--vol 1"])
285@@ -276,19 +277,73 @@
286 """
287 # Make first normal full backup
288 self.backup("full", "testfiles/dir1")
289- # create 3 2M files
290- assert not os.system("mkdir testfiles/largefiles")
291- for n in (1,2,3):
292- assert not os.system("dd if=/dev/urandom of=testfiles/largefiles/file%d bs=1024 count=2048 > /dev/null 2>&1" % n)
293+ self.make_largefiles()
294 # Force a failure partway through
295 try:
296 self.backup("inc", "testfiles/largefiles", options = ["--vols 1", "--fail 2"])
297- assert False # shouldn't get this far
298+ self.fail()
299 except CmdError, e:
300- pass
301+ self.assertEqual(30, e.exit_status)
302 # Now finish that incremental
303 self.backup("inc", "testfiles/largefiles")
304 self.verify("testfiles/largefiles")
305
306+ def test_no_write_double_snapshot(self):
307+ """
308+ Test that restarting a full backup does not write duplicate entries
309+ into the sigtar, causing problems reading it back in older
310+ versions.
311+ https://launchpad.net/bugs/929067
312+ """
313+ self.make_largefiles()
314+ # Start backup
315+ try:
316+ self.backup("full", "testfiles/largefiles", options = ["--fail 2", "--vols 1", "--no-encryption"])
317+ self.fail()
318+ except CmdError, e:
319+ self.assertEqual(30, e.exit_status)
320+ # Finish it
321+ self.backup("full", "testfiles/largefiles", options = ["--no-encryption"])
322+ # Now check sigtar
323+ sigtars = glob.glob("testfiles/output/duplicity-full*.sigtar.gz")
324+ self.assertEqual(1, len(sigtars))
325+ sigtar = sigtars[0]
326+ output = subprocess.check_output(["tar", "t", "--file=%s" % sigtar])
327+ self.assertEqual(1, output.split("\n").count("snapshot/"))
328+
329+ def xtest_ignore_double_snapshot(self):
330+ """
331+ Test that we gracefully ignore double snapshot entries in a signature
332+ file. This winds its way through duplicity as a deleted base dir,
333+ which doesn't make sense and should be ignored. An older version of
334+ duplicity accidentally created such files as a result of a restart.
335+ https://launchpad.net/bugs/929067
336+ """
337+ # Intial normal backup
338+ self.backup("full", "testfiles/blocktartest", options = ["--no-encryption"])
339+ # Create an exact clone of the snapshot folder in the sigtar already.
340+ # Permissions and mtime must match.
341+ os.mkdir("testfiles/snapshot", 0755)
342+ os.utime("testfiles/snapshot", (1030384548, 1030384548))
343+ # Adjust the sigtar.gz file to have a bogus second snapshot/ entry
344+ # at the beginning.
345+ sigtars = glob.glob("testfiles/output/duplicity-full*.sigtar.gz")
346+ self.assertEqual(1, len(sigtars))
347+ sigtar = sigtars[0]
348+ self.assertEqual(0, os.system("tar c --file=testfiles/snapshot.sigtar -C testfiles snapshot"))
349+ self.assertEqual(0, os.system("gunzip -c %s > testfiles/full.sigtar" % sigtar))
350+ self.assertEqual(0, os.system("tar A --file=testfiles/snapshot.sigtar testfiles/full.sigtar"))
351+ self.assertEqual(0, os.system("gzip testfiles/snapshot.sigtar"))
352+ os.remove(sigtar)
353+ os.rename("testfiles/snapshot.sigtar.gz", sigtar)
354+ # Clear cache so our adjusted sigtar will be sync'd back into the cache
355+ self.assertEqual(0, os.system("rm -r testfiles/cache"))
356+ # Try a follow on incremental (which in buggy versions, would create
357+ # a deleted entry for the base dir)
358+ self.backup("inc", "testfiles/blocktartest")
359+ self.assertEqual(1, len(glob.glob("testfiles/output/duplicity-new*.sigtar.gz")))
360+ # Confirm we can restore it (which in buggy versions, would fail)
361+ self.restore()
362+
363 if __name__ == "__main__":
364 unittest.main()

Subscribers

People subscribed via source and target branches

to all changes: