[Checkins] SVN: ZODB/branches/tseaver-lp143158-feature/ Merge '--kill-old-on-full' patch from LP #143158, with added tests.

Tres Seaver tseaver at palladion.com
Sun Dec 20 10:23:11 EST 2009


Log message for revision 106808:
  Merge '--kill-old-on-full' patch from LP #143158, with added tests.
  

Changed:
  A   ZODB/branches/tseaver-lp143158-feature/
  U   ZODB/branches/tseaver-lp143158-feature/src/CHANGES.txt
  U   ZODB/branches/tseaver-lp143158-feature/src/ZODB/scripts/repozo.py
  D   ZODB/branches/tseaver-lp143158-feature/src/ZODB/scripts/tests/test_repozo.py
  A   ZODB/branches/tseaver-lp143158-feature/src/ZODB/scripts/tests/test_repozo.py

-=-
Modified: ZODB/branches/tseaver-lp143158-feature/src/CHANGES.txt
===================================================================
--- ZODB/trunk/src/CHANGES.txt	2009-12-20 05:02:08 UTC (rev 106805)
+++ ZODB/branches/tseaver-lp143158-feature/src/CHANGES.txt	2009-12-20 15:23:11 UTC (rev 106808)
@@ -5,6 +5,13 @@
 3.10.0a1 (2009-12-??)
 =====================
 
+New Features
+------------
+
+- Added a '--kill-old-on-full' argument to the backup options:  if passed,
+  remove any older full or incremental backup files from the repository after
+  doing a full backup. (https://bugs.launchpad.net/zope2/+bug/143158)
+
 Bugs Fixed
 ----------
 

Modified: ZODB/branches/tseaver-lp143158-feature/src/ZODB/scripts/repozo.py
===================================================================
--- ZODB/trunk/src/ZODB/scripts/repozo.py	2009-12-20 05:02:08 UTC (rev 106805)
+++ ZODB/branches/tseaver-lp143158-feature/src/ZODB/scripts/repozo.py	2009-12-20 15:23:11 UTC (rev 106808)
@@ -50,6 +50,11 @@
         Compress with gzip the backup files.  Uses the default zlib
         compression level.  By default, gzip compression is not used.
 
+    -k / --kill-old-on-full
+        If a full backup is created, remove any prior full or incremental
+        backup files (and associated metadata files) from the repository
+        directory.
+
 Options for -R/--recover:
     -D str
     --date=str
@@ -109,10 +114,20 @@
 def parseargs(argv):
     global VERBOSE
     try:
-        opts, args = getopt.getopt(argv, 'BRvhf:r:FD:o:Qz',
-                                   ['backup', 'recover', 'verbose', 'help',
-                                    'file=', 'repository=', 'full', 'date=',
-                                    'output=', 'quick', 'gzip'])
+        opts, args = getopt.getopt(argv, 'BRvhr:f:FQzkD:o:',
+                                   ['backup',
+                                    'recover',
+                                    'verbose',
+                                    'help',
+                                    'repository=',
+                                    'file=',
+                                    'full',
+                                    'quick',
+                                    'gzip',
+                                    'kill-old-on-full',
+                                    'date=',
+                                    'output=',
+                                   ])
     except getopt.error, msg:
         usage(1, msg)
 
@@ -125,6 +140,7 @@
         output = None       # where to write recovered data; None = stdout
         quick = False       # -Q flag state
         gzip = False        # -z flag state
+        killold = False     # -k flag state
 
     options = Options()
 
@@ -155,6 +171,8 @@
             options.output = arg
         elif opt in ('-z', '--gzip'):
             options.gzip = True
+        elif opt in ('-k', '--kill-old-on-full'):
+            options.killold = True
         else:
             assert False, (opt, arg)
 
@@ -179,6 +197,9 @@
         if options.file is not None:
             log('--file option is ignored in recover mode')
             options.file = None
+        if options.killold is not None:
+            log('--kill-old-on-full option is ignored in recover mode')
+            options.killold = None
     return options
 
 
@@ -351,7 +372,40 @@
 
     return fn, startpos, endpos, sum
 
+def delete_old_backups(options):
+    # Delete all full backup files except for the most recent full backup file
+    all = filter(is_data_file, os.listdir(options.repository))
+    all.sort()
 
+    deletable = []
+    full = []
+    for fname in all:
+        root, ext = os.path.splitext(fname)
+        if ext in ('.fs', 'fsz'):
+            full.append(fname)
+        if ext in ('.fs', '.fsz', '.deltafs', '.deltafsz'):
+            deletable.append(fname)
+
+    # keep most recent full
+    if not full:
+        return
+    
+    recentfull = full.pop(-1)
+    deletable.remove(recentfull)
+    root, ext = os.path.splitext(recentfull)
+    dat = root + '.dat'
+    if dat in deletable:
+        deletable.remove(dat)
+    
+    for fname in deletable:
+        log('removing old backup file %s (and .dat)', fname)
+        root, ext = os.path.splitext(fname)
+        try:
+            os.unlink(os.path.join(options.repository, root + '.dat'))
+        except OSError:
+            pass
+        os.unlink(os.path.join(options.repository, fname))
+
 def do_full_backup(options):
     # Find the file position of the last completed transaction.
     fs = FileStorage(options.file, read_only=True)
@@ -376,6 +430,8 @@
     fp.flush()
     os.fsync(fp.fileno())
     fp.close()
+    if options.killold:
+        delete_old_backups(options)
 
 
 def do_incremental_backup(options, reposz, repofiles):

Deleted: ZODB/branches/tseaver-lp143158-feature/src/ZODB/scripts/tests/test_repozo.py
===================================================================
--- ZODB/trunk/src/ZODB/scripts/tests/test_repozo.py	2009-12-20 05:02:08 UTC (rev 106805)
+++ ZODB/branches/tseaver-lp143158-feature/src/ZODB/scripts/tests/test_repozo.py	2009-12-20 15:23:11 UTC (rev 106808)
@@ -1,181 +0,0 @@
-#!/usr/bin/env python
-##############################################################################
-#
-# Copyright (c) 2004-2009 Zope Corporation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-
-import unittest
-import os
-import ZODB.tests.util
-
-_NOISY = os.environ.get('NOISY_REPOZO_TEST_OUTPUT')
-
-class OurDB:
-
-    def __init__(self, dir):
-        from BTrees.OOBTree import OOBTree
-        import transaction
-        self.dir = dir
-        self.getdb()
-        conn = self.db.open()
-        conn.root()['tree'] = OOBTree()
-        transaction.commit()
-        self.close()
-
-    def getdb(self):
-        from ZODB import DB
-        from ZODB.FileStorage import FileStorage
-        storage_filename = os.path.join(self.dir, 'Data.fs')
-        storage = FileStorage(storage_filename)
-        self.db = DB(storage)
-
-    def gettree(self):
-        self.getdb()
-        conn = self.db.open()
-        return conn.root()['tree']
-
-    def pack(self):
-        self.getdb()
-        self.db.pack()
-
-    def close(self):
-        if self.db is not None:
-            self.db.close()
-            self.db = None
-
-    def mutate(self):
-        # Make random mutations to the btree in the database.
-        import random
-        import transaction
-        tree = self.gettree()
-        for dummy in range(100):
-            if random.random() < 0.6:
-                tree[random.randrange(100000)] = random.randrange(100000)
-            else:
-                keys = tree.keys()
-                if keys:
-                    del tree[keys[0]]
-        transaction.commit()
-        self.close()
-
-
-class RepozoTests(unittest.TestCase):
-
-    layer = ZODB.tests.util.MininalTestLayer('repozo')
-
-    def setUp(self):
-        # compute directory names
-        import tempfile
-        self.basedir = tempfile.mkdtemp()
-        self.backupdir = os.path.join(self.basedir, 'backup')
-        self.datadir = os.path.join(self.basedir, 'data')
-        self.restoredir = os.path.join(self.basedir, 'restore')
-        self.copydir = os.path.join(self.basedir, 'copy')
-        self.currdir = os.getcwd()
-        # create empty directories
-        os.mkdir(self.backupdir)
-        os.mkdir(self.datadir)
-        os.mkdir(self.restoredir)
-        os.mkdir(self.copydir)
-        os.chdir(self.datadir)
-        self.db = OurDB(self.datadir)
-
-    def tearDown(self):
-        os.chdir(self.currdir)
-        import shutil
-        shutil.rmtree(self.basedir)
-
-    def _callRepozoMain(self, argv):
-        from ZODB.scripts.repozo import main
-        main(argv)
-
-    def testRepozo(self):
-        self.saved_snapshots = []  # list of (name, time) pairs for copies.
-
-        for i in range(100):
-            self.mutate_pack_backup(i)
-
-        # Verify snapshots can be reproduced exactly.
-        for copyname, copytime in self.saved_snapshots:
-            if _NOISY:
-                print "Checking that", copyname,
-                print "at", copytime, "is reproducible."
-            self.assertRestored(copyname, copytime)
-
-    def mutate_pack_backup(self, i):
-        import random
-        from shutil import copyfile
-        from time import gmtime
-        from time import sleep
-        self.db.mutate()
-
-        # Pack about each tenth time.
-        if random.random() < 0.1:
-            if _NOISY:
-                print "packing"
-            self.db.pack()
-            self.db.close()
-
-        # Make an incremental backup, half the time with gzip (-z).
-        argv = ['-BQr', self.backupdir, '-f', 'Data.fs']
-        if _NOISY:
-            argv.insert(0, '-v')
-        if random.random() < 0.5:
-            argv.insert(0, '-z')
-        self._callRepozoMain(argv)
-
-        # Save snapshots to assert that dated restores are possible
-        if i % 9 == 0:
-            srcname = os.path.join(self.datadir, 'Data.fs')
-            copytime = '%04d-%02d-%02d-%02d-%02d-%02d' % (gmtime()[:6])
-            copyname = os.path.join(self.copydir, "Data%d.fs" % i)
-            copyfile(srcname, copyname)
-            self.saved_snapshots.append((copyname, copytime))
-
-        # Make sure the clock moves at least a second.
-        sleep(1.01)
-
-        # Verify current Data.fs can be reproduced exactly.
-        self.assertRestored()
-
-    def assertRestored(self, correctpath='Data.fs', when=None):
-    # Do recovery to time 'when', and check that it's identical to correctpath.
-        # restore to Restored.fs
-        restoredfile = os.path.join(self.restoredir, 'Restored.fs')
-        argv = ['-Rr', self.backupdir, '-o', restoredfile]
-        if _NOISY:
-            argv.insert(0, '-v')
-        if when is not None:
-            argv.append('-D')
-            argv.append(when)
-        self._callRepozoMain(argv)
-
-        # check restored file content is equal to file that was backed up
-        f = file(correctpath, 'rb')
-        g = file(restoredfile, 'rb')
-        fguts = f.read()
-        gguts = g.read()
-        f.close()
-        g.close()
-        msg = ("guts don't match\ncorrectpath=%r when=%r\n cmd=%r" %
-            (correctpath, when, ' '.join(argv)))
-        self.assertEquals(fguts, gguts, msg)
-
-
-def test_suite():
-    suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(RepozoTests))
-    return suite
-
-
-if __name__ == '__main__':
-    unittest.main(defaultTest='test_suite')

Copied: ZODB/branches/tseaver-lp143158-feature/src/ZODB/scripts/tests/test_repozo.py (from rev 106806, ZODB/trunk/src/ZODB/scripts/tests/test_repozo.py)
===================================================================
--- ZODB/branches/tseaver-lp143158-feature/src/ZODB/scripts/tests/test_repozo.py	                        (rev 0)
+++ ZODB/branches/tseaver-lp143158-feature/src/ZODB/scripts/tests/test_repozo.py	2009-12-20 15:23:11 UTC (rev 106808)
@@ -0,0 +1,242 @@
+##############################################################################
+#
+# Copyright (c) 2004-2009 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+import unittest
+import os
+import ZODB.tests.util  # layer used at class scope
+
+_NOISY = os.environ.get('NOISY_REPOZO_TEST_OUTPUT')
+
+class OurDB:
+
+    def __init__(self, dir):
+        from BTrees.OOBTree import OOBTree
+        import transaction
+        self.dir = dir
+        self.getdb()
+        conn = self.db.open()
+        conn.root()['tree'] = OOBTree()
+        transaction.commit()
+        self.close()
+
+    def getdb(self):
+        from ZODB import DB
+        from ZODB.FileStorage import FileStorage
+        storage_filename = os.path.join(self.dir, 'Data.fs')
+        storage = FileStorage(storage_filename)
+        self.db = DB(storage)
+
+    def gettree(self):
+        self.getdb()
+        conn = self.db.open()
+        return conn.root()['tree']
+
+    def pack(self):
+        self.getdb()
+        self.db.pack()
+
+    def close(self):
+        if self.db is not None:
+            self.db.close()
+            self.db = None
+
+    def mutate(self):
+        # Make random mutations to the btree in the database.
+        import random
+        import transaction
+        tree = self.gettree()
+        for dummy in range(100):
+            if random.random() < 0.6:
+                tree[random.randrange(100000)] = random.randrange(100000)
+            else:
+                keys = tree.keys()
+                if keys:
+                    del tree[keys[0]]
+        transaction.commit()
+        self.close()
+
+
+class RepozoTests(unittest.TestCase):
+
+    layer = ZODB.tests.util.MininalTestLayer('repozo')
+
+    def setUp(self):
+        # compute directory names
+        import tempfile
+        self.basedir = tempfile.mkdtemp()
+        self.backupdir = os.path.join(self.basedir, 'backup')
+        self.datadir = os.path.join(self.basedir, 'data')
+        self.restoredir = os.path.join(self.basedir, 'restore')
+        self.copydir = os.path.join(self.basedir, 'copy')
+        self.currdir = os.getcwd()
+        # create empty directories
+        os.mkdir(self.backupdir)
+        os.mkdir(self.datadir)
+        os.mkdir(self.restoredir)
+        os.mkdir(self.copydir)
+        os.chdir(self.datadir)
+        self.db = OurDB(self.datadir)
+
+    def tearDown(self):
+        os.chdir(self.currdir)
+        import shutil
+        shutil.rmtree(self.basedir)
+
+    def _callRepozoMain(self, argv):
+        from ZODB.scripts.repozo import main
+        main(argv)
+
+    def testRepozo(self):
+        self.saved_snapshots = []  # list of (name, time) pairs for copies.
+
+        for i in range(100):
+            self.mutate_pack_backup(i)
+
+        # Verify snapshots can be reproduced exactly.
+        for copyname, copytime in self.saved_snapshots:
+            if _NOISY:
+                print "Checking that", copyname,
+                print "at", copytime, "is reproducible."
+            self.assertRestored(copyname, copytime)
+
+    def mutate_pack_backup(self, i):
+        import random
+        from shutil import copyfile
+        from time import gmtime
+        from time import sleep
+        self.db.mutate()
+
+        # Pack about each tenth time.
+        if random.random() < 0.1:
+            if _NOISY:
+                print "packing"
+            self.db.pack()
+            self.db.close()
+
+        # Make an incremental backup, half the time with gzip (-z).
+        argv = ['-BQr', self.backupdir, '-f', 'Data.fs']
+        if _NOISY:
+            argv.insert(0, '-v')
+        if random.random() < 0.5:
+            argv.insert(0, '-z')
+        self._callRepozoMain(argv)
+
+        # Save snapshots to assert that dated restores are possible
+        if i % 9 == 0:
+            srcname = os.path.join(self.datadir, 'Data.fs')
+            copytime = '%04d-%02d-%02d-%02d-%02d-%02d' % (gmtime()[:6])
+            copyname = os.path.join(self.copydir, "Data%d.fs" % i)
+            copyfile(srcname, copyname)
+            self.saved_snapshots.append((copyname, copytime))
+
+        # Make sure the clock moves at least a second.
+        sleep(1.01)
+
+        # Verify current Data.fs can be reproduced exactly.
+        self.assertRestored()
+
+    def assertRestored(self, correctpath='Data.fs', when=None):
+    # Do recovery to time 'when', and check that it's identical to correctpath.
+        # restore to Restored.fs
+        restoredfile = os.path.join(self.restoredir, 'Restored.fs')
+        argv = ['-Rr', self.backupdir, '-o', restoredfile]
+        if _NOISY:
+            argv.insert(0, '-v')
+        if when is not None:
+            argv.append('-D')
+            argv.append(when)
+        self._callRepozoMain(argv)
+
+        # check restored file content is equal to file that was backed up
+        f = file(correctpath, 'rb')
+        g = file(restoredfile, 'rb')
+        fguts = f.read()
+        gguts = g.read()
+        f.close()
+        g.close()
+        msg = ("guts don't match\ncorrectpath=%r when=%r\n cmd=%r" %
+            (correctpath, when, ' '.join(argv)))
+        self.assertEquals(fguts, gguts, msg)
+
+class Test_delete_old_backups(unittest.TestCase):
+
+    _repository_directory = None
+
+    def tearDown(self):
+        if self._repository_directory is not None:
+            from shutil import rmtree
+            rmtree(self._repository_directory)
+
+    def _callFUT(self, options=None, filenames=()):
+        from ZODB.scripts.repozo import delete_old_backups
+        if options is None:
+            options = self._makeOptions(filenames)
+        delete_old_backups(options)
+
+    def _makeOptions(self, filenames=()):
+        import tempfile
+        dir = self._repository_directory = tempfile.mkdtemp()
+        for filename in filenames:
+            fqn = os.path.join(dir, filename)
+            f = open(fqn, 'wb')
+            f.write('testing delete_old_backups')
+            f.close()
+        class Options(object):
+            repository = dir
+        return Options()
+
+    def test_empty_dir_doesnt_raise(self):
+        self._callFUT()
+        self.assertEqual(len(os.listdir(self._repository_directory)), 0)
+
+    def test_no_repozo_files_doesnt_raise(self):
+        FILENAMES = ['bogus.txt', 'not_a_repozo_file']
+        self._callFUT(filenames=FILENAMES)
+        remaining = os.listdir(self._repository_directory)
+        self.assertEqual(len(remaining), len(FILENAMES))
+        for name in FILENAMES:
+            fqn = os.path.join(self._repository_directory, name)
+            self.failUnless(os.path.isfile(fqn))
+
+    def test_doesnt_remove_current_repozo_files(self):
+        FILENAMES = ['2009-12-20-10-08-03.fs', '2009-12-20-10-08-03.dat']
+        self._callFUT(filenames=FILENAMES)
+        remaining = os.listdir(self._repository_directory)
+        self.assertEqual(len(remaining), len(FILENAMES))
+        for name in FILENAMES:
+            fqn = os.path.join(self._repository_directory, name)
+            self.failUnless(os.path.isfile(fqn))
+
+    def test_removes_older_repozo_files(self):
+        OLDER_FULL = ['2009-12-20-00-01-03.fs', '2009-12-20-00-01-03.dat']
+        DELTAS = ['2009-12-21-00-00-01.deltafs', '2009-12-22-00-00-01.deltafs']
+        CURRENT_FULL = ['2009-12-23-00-00-01.fs', '2009-12-23-00-00-01.dat']
+        FILENAMES = OLDER_FULL + DELTAS + CURRENT_FULL
+        self._callFUT(filenames=FILENAMES)
+        remaining = os.listdir(self._repository_directory)
+        self.assertEqual(len(remaining), len(CURRENT_FULL))
+        for name in OLDER_FULL:
+            fqn = os.path.join(self._repository_directory, name)
+            self.failIf(os.path.isfile(fqn))
+        for name in DELTAS:
+            fqn = os.path.join(self._repository_directory, name)
+            self.failIf(os.path.isfile(fqn))
+        for name in CURRENT_FULL:
+            fqn = os.path.join(self._repository_directory, name)
+            self.failUnless(os.path.isfile(fqn))
+
+def test_suite():
+    return unittest.TestSuite([
+        #unittest.makeSuite(RepozoTests),
+        unittest.makeSuite(Test_delete_old_backups),
+    ])



More information about the checkins mailing list