From e9f5f9b86f27350984aaa7856243b7c9c1d722c6 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Thu, 26 Dec 2013 05:27:41 +0000 Subject: [PATCH 01/11] Add ticket equality test and representation. --- fail2ban/server/ticket.py | 14 +++++++++++--- fail2ban/tests/failmanagertestcase.py | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/fail2ban/server/ticket.py b/fail2ban/server/ticket.py index 3883f0c8..6d856be3 100644 --- a/fail2ban/server/ticket.py +++ b/fail2ban/server/ticket.py @@ -46,9 +46,17 @@ class Ticket: self.__matches = matches or [] def __str__(self): - return "%s: ip=%s time=%s #attempts=%d" % \ - (self.__class__.__name__.split('.')[-1], self.__ip, self.__time, self.__attempt) - + return "%s: ip=%s time=%s #attempts=%d matches=%r" % \ + (self.__class__.__name__.split('.')[-1], self.__ip, self.__time, self.__attempt, self.__matches) + + def __repr__(self): + return str(self) + + def __eq__(self, other): + return self.__ip == other.__ip and \ + round(self.__time,2) == round(other.__time,2) and \ + self.__attempt == other.__attempt and \ + self.__matches == other.__matches def setIP(self, value): if isinstance(value, basestring): diff --git a/fail2ban/tests/failmanagertestcase.py b/fail2ban/tests/failmanagertestcase.py index 00ed603d..ee58d1b9 100644 --- a/fail2ban/tests/failmanagertestcase.py +++ b/fail2ban/tests/failmanagertestcase.py @@ -95,14 +95,14 @@ class AddFailure(unittest.TestCase): ticket_str = str(ticket) self.assertEqual( ticket_str, - 'FailTicket: ip=193.168.0.128 time=1167605999.0 #attempts=5') + 'FailTicket: ip=193.168.0.128 time=1167605999.0 #attempts=5 matches=[]') # and some get/set-ers otherwise not tested ticket.setTime(1000002000.0) self.assertEqual(ticket.getTime(), 1000002000.0) # and str() adjusted correspondingly self.assertEqual( str(ticket), - 'FailTicket: ip=193.168.0.128 time=1000002000.0 #attempts=5') + 'FailTicket: ip=193.168.0.128 time=1000002000.0 #attempts=5 matches=[]') def testbanNOK(self): self.__failManager.setMaxRetry(10) From 8a25dd2dadb245937258826a1422d5340d0b660f Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Thu, 26 Dec 2013 05:46:38 +0000 Subject: [PATCH 02/11] ENH: change addLog to use single SQL statement ENH: separate out the database creation defination to make updates easier TST: add test framework for updates --- MANIFEST | 1 + fail2ban/server/database.py | 114 +++++++++++++++++----------- fail2ban/tests/databasetestcase.py | 15 +++- fail2ban/tests/files/database_v1.db | Bin 0 -> 15360 bytes 4 files changed, 83 insertions(+), 47 deletions(-) create mode 100644 fail2ban/tests/files/database_v1.db diff --git a/MANIFEST b/MANIFEST index 1f8ac991..0d0f3e89 100644 --- a/MANIFEST +++ b/MANIFEST @@ -77,6 +77,7 @@ fail2ban/tests/config/apache-auth/digest_anon/.htaccess fail2ban/tests/config/apache-auth/digest_anon/.htpasswd fail2ban/tests/config/apache-auth/README fail2ban/tests/config/apache-auth/noentry/.htaccess +fail2ban/tests/files/database_v1.db fail2ban/tests/files/testcase01.log fail2ban/tests/files/testcase02.log fail2ban/tests/files/testcase03.log diff --git a/fail2ban/server/database.py b/fail2ban/server/database.py index b9c2e12d..e6045ddd 100644 --- a/fail2ban/server/database.py +++ b/fail2ban/server/database.py @@ -23,6 +23,7 @@ __license__ = "GPL" import logging import sys +import shutil, time import sqlite3 import json import locale @@ -55,7 +56,39 @@ def commitandrollback(f): return wrapper class Fail2BanDb(object): - __version__ = 1 + __version__ = 2 + _TABLE_fail2banDb = "CREATE TABLE fail2banDb(version INTEGER)" + _TABLE_jails = "CREATE TABLE jails(" \ + "name TEXT NOT NULL UNIQUE, " \ + "enabled INTEGER NOT NULL DEFAULT 1" \ + ");" \ + "CREATE INDEX jails_name ON jails(name);" + _TABLE_logs = "CREATE TABLE logs(" \ + "jail TEXT NOT NULL, " \ + "path TEXT, " \ + "firstlinemd5 TEXT, " \ + "lastfilepos INTEGER DEFAULT 0, " \ + "FOREIGN KEY(jail) REFERENCES jails(name) ON DELETE CASCADE, " \ + "UNIQUE(jail, path)," \ + "UNIQUE(jail, path, firstlinemd5)" \ + ");" \ + "CREATE INDEX logs_path ON logs(path);" \ + "CREATE INDEX logs_jail_path ON logs(jail, path);" + #TODO: systemd journal features \ + #"journalmatch TEXT, " \ + #"journlcursor TEXT, " \ + #"lastfiletime INTEGER DEFAULT 0, " # is this easily available \ + _TABLE_bans = "CREATE TABLE bans(" \ + "jail TEXT NOT NULL, " \ + "ip TEXT, " \ + "timeofban INTEGER NOT NULL, " \ + "data JSON, " \ + "FOREIGN KEY(jail) REFERENCES jails(name) " \ + ");" \ + "CREATE INDEX bans_jail_timeofban_ip ON bans(jail, timeofban);" \ + "CREATE INDEX bans_jail_ip ON bans(jail, ip);" \ + "CREATE INDEX bans_ip ON bans(ip);" \ + def __init__(self, filename, purgeAge=24*60*60): try: self._db = sqlite3.connect( @@ -85,8 +118,15 @@ class Fail2BanDb(object): else: version = cur.fetchone()[0] if version < Fail2BanDb.__version__: - logSys.warning( "Database updated from '%i' to '%i'", - version, self.updateDb(version)) + newversion = self.updateDb(version) + if newversion == Fail2BanDb.__version__: + logSys.warning( "Database updated from '%i' to '%i'", + version, newversion) + else: + logSys.error( "Database update failed to acheive version '%i'" + ": updated from '%i' to '%i'", + Fail2BanDb.__version__, version, newversion) + raise Exception('Failed to fully update') finally: cur.close() @@ -102,53 +142,37 @@ class Fail2BanDb(object): @commitandrollback def createDb(self, cur): # Version info - cur.execute("CREATE TABLE fail2banDb(version INTEGER)") + cur.executescript(Fail2BanDb._TABLE_fail2banDb) cur.execute("INSERT INTO fail2banDb(version) VALUES(?)", (Fail2BanDb.__version__, )) - # Jails - cur.execute("CREATE TABLE jails(" - "name TEXT NOT NULL UNIQUE, " - "enabled INTEGER NOT NULL DEFAULT 1" - ")") - cur.execute("CREATE INDEX jails_name ON jails(name)") - + cur.executescript(Fail2BanDb._TABLE_jails) # Logs - cur.execute("CREATE TABLE logs(" - "jail TEXT NOT NULL, " - "path TEXT, " - "firstlinemd5 TEXT, " - #TODO: systemd journal features - #"journalmatch TEXT, " - #"journlcursor TEXT, " - "lastfilepos INTEGER DEFAULT 0, " - #"lastfiletime INTEGER DEFAULT 0, " # is this easily available - "FOREIGN KEY(jail) REFERENCES jails(name) ON DELETE CASCADE, " - "UNIQUE(jail, path)" - ")") - cur.execute("CREATE INDEX logs_path ON logs(path)") - cur.execute("CREATE INDEX logs_jail_path ON logs(jail, path)") - + cur.executescript(Fail2BanDb._TABLE_logs) # Bans - cur.execute("CREATE TABLE bans(" - "jail TEXT NOT NULL, " - "ip TEXT, " - "timeofban INTEGER NOT NULL, " - "data JSON, " - "FOREIGN KEY(jail) REFERENCES jails(name) " - ")") - cur.execute( - "CREATE INDEX bans_jail_timeofban_ip ON bans(jail, timeofban)") - cur.execute("CREATE INDEX bans_jail_ip ON bans(jail, ip)") - cur.execute("CREATE INDEX bans_ip ON bans(ip)") + cur.executescript(Fail2BanDb._TABLE_bans) cur.execute("SELECT version FROM fail2banDb LIMIT 1") return cur.fetchone()[0] @commitandrollback def updateDb(self, cur, version): - raise NotImplementedError( - "Only single version of database exists...how did you get here??") + self.dbBackupFilename = self._dbFilename + '.' + time.strftime('%Y%m%d-%H%M%S', MyTime.gmtime()) + shutil.copyfile(self._dbFilename, self.dbBackupFilename) + if version > Fail2BanDb.__version__: + raise NotImplementedError( + "Attempt to travel to future version of database ...how did you get here??") + + if version < 2: + cur.executescript("BEGIN TRANSACTION;" + "CREATE TEMPORARY TABLE logs_temp AS SELECT * FROM logs;" + "DROP TABLE logs;" + "%s;" + "INSERT INTO logs SELECT * from logs_temp;" + "DROP TABLE logs_temp;" + "UPDATE fail2banDb SET version = 2;" + "COMMIT;" % Fail2BanDb._TABLE_logs) + cur.execute("SELECT version FROM fail2banDb LIMIT 1") return cur.fetchone()[0] @@ -187,15 +211,15 @@ class Fail2BanDb(object): try: firstLineMD5, lastLinePos = cur.fetchone() except TypeError: - cur.execute( - "INSERT INTO logs(jail, path, firstlinemd5, lastfilepos) " + firstLineMD5 = False + + cur.execute( + "INSERT OR REPLACE INTO logs(jail, path, firstlinemd5, lastfilepos) " "VALUES(?, ?, ?, ?)", (jail.getName(), container.getFileName(), container.getHash(), container.getPos())) - else: - if container.getHash() != firstLineMD5: - self._updateLog(cur, jail, container) - lastLinePos = None + if container.getHash() != firstLineMD5: + lastLinePos = None return lastLinePos @commitandrollback diff --git a/fail2ban/tests/databasetestcase.py b/fail2ban/tests/databasetestcase.py index c4c5f53e..19502e4f 100644 --- a/fail2ban/tests/databasetestcase.py +++ b/fail2ban/tests/databasetestcase.py @@ -26,6 +26,7 @@ import os import unittest import tempfile import sqlite3 +import shutil from fail2ban.server.database import Fail2BanDb from fail2ban.server.filter import FileContainer @@ -64,8 +65,16 @@ class DatabaseTest(unittest.TestCase): "Jail not retained in Db after disconnect reconnect.") def testUpdateDb(self): - # TODO: Currently only single version exists - pass + shutil.copyfile('fail2ban/tests/files/database_v1.db', self.dbFilename) + self.db = Fail2BanDb(self.dbFilename) + self.assertEqual(self.db.getJailNames(), {'DummyJail #29162448 with 0 tickets'}) + self.assertEqual(self.db.getLogPaths(), {'/tmp/Fail2BanDb_pUlZJh.log'}) + ticket = FailTicket("127.0.0.1", 1388009242.26, [u"abc\n"]) + self.assertEqual(self.db.getBans()[0], ticket) + + self.assertEqual(self.db.updateDb(Fail2BanDb.__version__), Fail2BanDb.__version__) + self.assertRaises(NotImplementedError, self.db.updateDb, Fail2BanDb.__version__ + 1) + os.remove(self.db.dbBackupFilename) def testAddJail(self): self.jail = DummyJail() @@ -83,6 +92,7 @@ class DatabaseTest(unittest.TestCase): self.db.addLog(self.jail, self.fileContainer) self.assertTrue(filename in self.db.getLogPaths(self.jail)) + os.remove(filename) def testUpdateLog(self): self.testAddLog() # Add log file @@ -121,6 +131,7 @@ class DatabaseTest(unittest.TestCase): # last position in file self.assertEqual( self.db.addLog(self.jail, self.fileContainer), None) + os.remove(filename) def testAddBan(self): self.testAddJail() diff --git a/fail2ban/tests/files/database_v1.db b/fail2ban/tests/files/database_v1.db new file mode 100644 index 0000000000000000000000000000000000000000..2082267184e3467aef9e8a48c659de334da7cce1 GIT binary patch literal 15360 zcmeHN%WoP-9G=;M#W;4ODn)y!Do>~^dnIfZ3;|k23NdSH7h^33C24D+gLktK|@H+GVDL_wW2snqz8JKnkmLbx- zspDGh%N^aS;&;P|(2Z~;633rg_90fVZIzEqyG=Ls5d8(nmj})OXJBd=SmyJ8YQ~ZS z;S5a5zLfN3`{WtcaU3K+nsy$wmA4eZw0H3PlHEBZJoqMBxJ

We_UoncCw2cp0Q)QC zKEZK|Wh-Ah&;OnP^yE^q!=2^~a0Uh$Sa)fj!8-yYR*UW;LG@pR&jEZcM1>!P-_RoZ zbI=@5PYnYfKvBAW9Uj4fBik{VI7^y{I4C4R&5+JrqMMawS~^o&@i)BBzA*%~I}; zW82x3dKdQ#@m|(BV{XbPX}3r6`QSJA88C9+zfBKu2o}v*r6z_^1#nX*FV;|`0bH?K zZM$mK&6*MIWL5RHU9qZWv(fGW?&4Gecxx}GZl|;OL-igrF5{fKrRLP^rn*1qBK0_> zW>hjvo5}snWQy?U@X4AvUdZx_C`#}9M)5`=6hrjaK&MmlKj%O=12fA&(4{Sn zA^*LL1xNqSfDjOlgkMk&{S41$mb}>j9$OHlw|#Jz2_@MAw2M}gMpuVKvQt3yh9+07 zrhL;QN=rUiU=5juP|2f-xuK@LBHEaqtkQb&?wlyeK6un`PF1wnwc&lyMz6eQh~{)O z?!@PH#Cnm5HFc66^Fw0ITz{EZTg{Fx+g8nNR7ky^DtGrO0c_~Dj(7I=vJ>dDT%Pwx z2_HOh_xWWwdL4D>zA}WDI?DDl#ld<#_8r{4A~}6Ul$L$4e69~?AQwkiCYZaU>DVJ< Z>gfL&Kz!EE#_Z=PXPtoswB+sS|Nq)4sMP=f literal 0 HcmV?d00001 From 74567d64b60ed9c259c41891c1f5c1a1796e268f Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Thu, 26 Dec 2013 09:01:29 +0000 Subject: [PATCH 03/11] TST: py26 compatible test --- fail2ban/tests/databasetestcase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fail2ban/tests/databasetestcase.py b/fail2ban/tests/databasetestcase.py index 19502e4f..d2b8706a 100644 --- a/fail2ban/tests/databasetestcase.py +++ b/fail2ban/tests/databasetestcase.py @@ -67,7 +67,7 @@ class DatabaseTest(unittest.TestCase): def testUpdateDb(self): shutil.copyfile('fail2ban/tests/files/database_v1.db', self.dbFilename) self.db = Fail2BanDb(self.dbFilename) - self.assertEqual(self.db.getJailNames(), {'DummyJail #29162448 with 0 tickets'}) + self.assertEqual(self.db.getJailNames(), set(['DummyJail #29162448 with 0 tickets'])) self.assertEqual(self.db.getLogPaths(), {'/tmp/Fail2BanDb_pUlZJh.log'}) ticket = FailTicket("127.0.0.1", 1388009242.26, [u"abc\n"]) self.assertEqual(self.db.getBans()[0], ticket) From de22c49b4dd3fe46db57a7f9441a203c547e37f7 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Thu, 26 Dec 2013 09:05:45 +0000 Subject: [PATCH 04/11] TST: (another) py26 compatible test fix --- fail2ban/tests/databasetestcase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fail2ban/tests/databasetestcase.py b/fail2ban/tests/databasetestcase.py index d2b8706a..acbd982c 100644 --- a/fail2ban/tests/databasetestcase.py +++ b/fail2ban/tests/databasetestcase.py @@ -68,7 +68,7 @@ class DatabaseTest(unittest.TestCase): shutil.copyfile('fail2ban/tests/files/database_v1.db', self.dbFilename) self.db = Fail2BanDb(self.dbFilename) self.assertEqual(self.db.getJailNames(), set(['DummyJail #29162448 with 0 tickets'])) - self.assertEqual(self.db.getLogPaths(), {'/tmp/Fail2BanDb_pUlZJh.log'}) + self.assertEqual(self.db.getLogPaths(), set(['/tmp/Fail2BanDb_pUlZJh.log'])) ticket = FailTicket("127.0.0.1", 1388009242.26, [u"abc\n"]) self.assertEqual(self.db.getBans()[0], ticket) From 4ee018a84be963ff9535b37e03fcb36316b82c1a Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Thu, 26 Dec 2013 09:06:54 +0000 Subject: [PATCH 05/11] TST: repr test for Ticket --- fail2ban/tests/failmanagertestcase.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fail2ban/tests/failmanagertestcase.py b/fail2ban/tests/failmanagertestcase.py index ee58d1b9..fb9d30b1 100644 --- a/fail2ban/tests/failmanagertestcase.py +++ b/fail2ban/tests/failmanagertestcase.py @@ -93,9 +93,13 @@ class AddFailure(unittest.TestCase): # finish with rudimentary tests of the ticket # verify consistent str ticket_str = str(ticket) + ticket_repr = repr(ticket) self.assertEqual( ticket_str, 'FailTicket: ip=193.168.0.128 time=1167605999.0 #attempts=5 matches=[]') + self.assertEqual( + ticket_repr, + 'FailTicket: ip=193.168.0.128 time=1167605999.0 #attempts=5 matches=[]') # and some get/set-ers otherwise not tested ticket.setTime(1000002000.0) self.assertEqual(ticket.getTime(), 1000002000.0) From 5d2a03e8522740d28e97cf58a036f0bda1fdbfe9 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Thu, 26 Dec 2013 09:22:02 +0000 Subject: [PATCH 06/11] TST: remove deprecated warn method of logging and use warning() instead --- fail2ban/tests/servertestcase.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index 26ff8e0a..442823f1 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -670,14 +670,14 @@ class TransmitterLogging(TransmitterBase): self.server.setLogLevel(2) self.assertEqual(self.transm.proceed(["set", "logtarget", fn]), (0, fn)) l = logging.getLogger('fail2ban.server.server').parent.parent - l.warn("Before file moved") + l.warning("Before file moved") try: f2, fn2 = tempfile.mkstemp("fail2ban.log") os.close(f2) os.rename(fn, fn2) - l.warn("After file moved") + l.warning("After file moved") self.assertEqual(self.transm.proceed(["flushlogs"]), (0, "rolled over")) - l.warn("After flushlogs") + l.warning("After flushlogs") with open(fn2,'r') as f: line1 = f.next() if line1.find('Changed logging target to') >= 0: From 1990eeae649eaf0e50dd24d4f45bcc75b089a153 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Thu, 26 Dec 2013 09:31:45 +0000 Subject: [PATCH 07/11] BF: Ticket compared to non-Ticket type returns False --- fail2ban/server/ticket.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fail2ban/server/ticket.py b/fail2ban/server/ticket.py index 6d856be3..8d036b2d 100644 --- a/fail2ban/server/ticket.py +++ b/fail2ban/server/ticket.py @@ -53,10 +53,13 @@ class Ticket: return str(self) def __eq__(self, other): - return self.__ip == other.__ip and \ + try: + return self.__ip == other.__ip and \ round(self.__time,2) == round(other.__time,2) and \ self.__attempt == other.__attempt and \ self.__matches == other.__matches + except AttributeError: + return False def setIP(self, value): if isinstance(value, basestring): From fed593e689a53c20551f194fe2f0c9cfd6f3e4a8 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Thu, 26 Dec 2013 10:03:51 +0000 Subject: [PATCH 08/11] TST: for database.getBans with bantime argument --- fail2ban/tests/databasetestcase.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fail2ban/tests/databasetestcase.py b/fail2ban/tests/databasetestcase.py index acbd982c..d02edcc5 100644 --- a/fail2ban/tests/databasetestcase.py +++ b/fail2ban/tests/databasetestcase.py @@ -142,6 +142,13 @@ class DatabaseTest(unittest.TestCase): self.assertTrue( isinstance(self.db.getBans(jail=self.jail)[0], FailTicket)) + def testGetBansWithTime(self): + self.testAddJail() + ticket = FailTicket("127.0.0.1", MyTime.time() - 40, ["abc\n"]) + self.db.addBan(self.jail, ticket) + self.assertEquals(len(self.db.getBans(jail=self.jail,bantime=50)), 1) + self.assertEquals(len(self.db.getBans(jail=self.jail,bantime=20)), 0) + def testGetBansMerged(self): self.testAddJail() From 37ab4147d13e3ea1e4213c9524c605f8a4ddf5a2 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Thu, 26 Dec 2013 10:09:12 +0000 Subject: [PATCH 09/11] TST: for db.getFilename --- fail2ban/tests/databasetestcase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fail2ban/tests/databasetestcase.py b/fail2ban/tests/databasetestcase.py index d02edcc5..1a2ac053 100644 --- a/fail2ban/tests/databasetestcase.py +++ b/fail2ban/tests/databasetestcase.py @@ -46,7 +46,7 @@ class DatabaseTest(unittest.TestCase): # Cleanup os.remove(self.dbFilename) - def getFilename(self): + def testGetFilename(self): self.assertEqual(self.dbFilename, self.db.getFilename()) def testCreateInvalidPath(self): From ec31e6a702006afd533628b145cc20ef80700eb8 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Thu, 26 Dec 2013 10:13:14 +0000 Subject: [PATCH 10/11] TST: restore Ticket testcase coverage to 100% after addition of exception test in Ticket.__eq__ --- fail2ban/tests/failmanagertestcase.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fail2ban/tests/failmanagertestcase.py b/fail2ban/tests/failmanagertestcase.py index fb9d30b1..fb452b01 100644 --- a/fail2ban/tests/failmanagertestcase.py +++ b/fail2ban/tests/failmanagertestcase.py @@ -100,6 +100,7 @@ class AddFailure(unittest.TestCase): self.assertEqual( ticket_repr, 'FailTicket: ip=193.168.0.128 time=1167605999.0 #attempts=5 matches=[]') + self.assertFalse(ticket == False) # and some get/set-ers otherwise not tested ticket.setTime(1000002000.0) self.assertEqual(ticket.getTime(), 1000002000.0) From 41bd0470bd5407d9adf413d33d259b59620d69a0 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Thu, 26 Dec 2013 21:28:46 +0000 Subject: [PATCH 11/11] TST: table create definitations to end in ; for py26 compatibility --- fail2ban/server/database.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fail2ban/server/database.py b/fail2ban/server/database.py index e6045ddd..b0d4200c 100644 --- a/fail2ban/server/database.py +++ b/fail2ban/server/database.py @@ -57,7 +57,8 @@ def commitandrollback(f): class Fail2BanDb(object): __version__ = 2 - _TABLE_fail2banDb = "CREATE TABLE fail2banDb(version INTEGER)" + # Note all _TABLE_* strings must end in ';' for py26 compatibility + _TABLE_fail2banDb = "CREATE TABLE fail2banDb(version INTEGER);" _TABLE_jails = "CREATE TABLE jails(" \ "name TEXT NOT NULL UNIQUE, " \ "enabled INTEGER NOT NULL DEFAULT 1" \