Repository: fail2ban/fail2ban

Issue:
Error binding parameter 0
Month or two back these errors started randomly appearing in my logs and I still can't find a working solution. I tried to delete fail2ban sqlite database, reinstall fail2ban, but without success. Is there anyhing I overlooked or messed up?

System: Fedora 20
Fail2ban: v0.9.1.dev

Error examples:

`
2014-11-04 14:49:43,160 fail2ban.actions        [7870]: ERROR   Failed to execute ban jail 'exim' action 'firewallcmd-ipset' info 'CallingMap({'ipjailmatches': <function <lambda> at 0x17dac80>, 'matches': u'2014-11-03 05:19:05 dovecot_login authenticator failed for ([192.168.2.33]) [88.249.121.158]: 535 Incorrect authentication data (set_id=tim)\n2014-11-03 05:19:11 dovecot_login authenticator failed for ([192.168.2.33]) [88.249.121.158]: 535 Incorrect authentication data (set_id=tim)\n2014-11-03 05:19:21 dovecot_login authenticator failed for ([192.168.2.33]) [88.249.121.158]: 535 Incorrect authentication data (set_id=tim)\n2014-11-03 05:19:38 dovecot_login authenticator failed for ([192.168.2.33]) [88.249.121.158]: 535
 Incorrect authentication data (set_id=tim)\n2014-11-03 05:19:55 dovecot_login authenticator failed for ([192.168.2.33]) [88.249.121.158]: 535 Incorrect authentication data (set_id=tim)\n2014-11-03 08:34:05 dovecot_login authenticator failed for ([192.168.2.33]) [88.249.121.158]: 535 Incorre
ct authentication data (set_id=user2)\n2014-11-03 08:34:11 dovecot_login authenticator failed for ([192.168.2.33]) [88.249.121.158]: 535 Incorrect authentication data (set_id=user2)\n2014-11-03 08:34:21 dovecot_login authenticator failed for ([192.168.2.33]) [88.249.121.158]: 535 Incorrect authentication data (set_id=user2)\n2014-11-03 08:34:38 dovecot_login authenticator failed for ([192.168.2.33]) [88.249.121.158]: 535 Incorrect authentication data (set_id=user2)\n2014-11-03 08:34:55 dovecot_login authenticator failed for ([192.168.2.33]) [88.249.121.158]: 535 Incorrect authe
ntication data (set_id=user2)', 'ip': '88.249.121.158', 'ipmatches': <function <lambda> at 0x17dacf8>, 'ipfailures': <function <lambda> at 0x17dad70>, 'time': 1415108983.156087, 'failures': 10, 'ipjailfailures': <function <lambda> at 0x17dade8>})': Error binding parameter 0 - probably unsupported type.
`

`
2014-11-02 15:21:51,090 fail2ban.actions        [26325]: ERROR   Failed to execute ban jail 'recidive' action 'firewallcmd-ipset' info 'CallingMap({'ipjailmatches': <function <lambda> at 0x1bb1de8>, 'matches': u'2014-11-01 22:37:50,438 fail2ban.actions        [26325]: NOTICE  [exim] Ban 89.1
37.17.19\n2014-11-02 04:54:08,652 fail2ban.actions        [26325]: NOTICE  [exim] Ban 89.137.17.19\n2014-11-02 15:21:50,626 fail2ban.actions        [26325]: NOTICE  [exim] Ban 89.137.17.19', 'ip': '89.137.17.19', 'ipmatches': <function <lambda> at 0x1bb1d70>, 'ipfailures': <function <lambda> at 0x1bb1c80>, 'time': 1414938111.078974, 'failures': 3, 'ipjailfailures': <function <lambda> at 0x1bb1cf8>})': Error binding parameter 0 - probably unsupported type.
`



Discussion:

Resolving patch:
diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py
--- a/fail2ban/server/actions.py
+++ b/fail2ban/server/actions.py
@@ -243,6 +243,45 @@ def run(self):
 		logSys.debug(self._jail.name + ": action terminated")
 		return True
 
+	def __getBansMerged(self, mi, overalljails=False):
+		"""Gets bans merged once, a helper for lambda(s), prevents stop of executing action by any exception inside.
+
+		This function never returns None for ainfo lambdas - always a ticket (merged or single one)
+		and prevents any errors through merging (to guarantee ban actions will be executed).
+		[TODO] move merging to observer - here we could wait for merge and read already merged info from a database
+
+		Parameters
+		----------
+		mi : dict
+			merge info, initial for lambda should contains {ip, ticket}
+		overalljails : bool
+			switch to get a merged bans :
+			False - (default) bans merged for current jail only
+			True - bans merged for all jails of current ip address
+
+		Returns
+		-------
+		BanTicket 
+			merged or self ticket only
+		"""
+		idx = 'all' if overalljails else 'jail'
+		if idx in mi:
+			return mi[idx] if mi[idx] is not None else mi['ticket']
+		try:
+			jail=self._jail
+			ip=mi['ip']
+			mi[idx] = None
+			if overalljails:
+				mi[idx] = jail.database.getBansMerged(ip=ip)
+			else:
+				mi[idx] = jail.database.getBansMerged(ip=ip, jail=jail)
+		except Exception as e:
+			logSys.error(
+				"Failed to get %s bans merged, jail '%s': %s",
+				idx, jail.name, e,
+				exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
+		return mi[idx] if mi[idx] is not None else mi['ticket']
+
 	def __checkBan(self):
 		"""Check for IP address to ban.
 
@@ -264,14 +303,12 @@ def __checkBan(self):
 			aInfo["time"] = bTicket.getTime()
 			aInfo["matches"] = "\n".join(bTicket.getMatches())
 			if self._jail.database is not None:
-				aInfo["ipmatches"] = lambda jail=self._jail: "\n".join(
-					jail.database.getBansMerged(ip=ip).getMatches())
-				aInfo["ipjailmatches"] = lambda jail=self._jail: "\n".join(
-					jail.database.getBansMerged(ip=ip, jail=jail).getMatches())
-				aInfo["ipfailures"] = lambda jail=self._jail: \
-					jail.database.getBansMerged(ip=ip).getAttempt()
-				aInfo["ipjailfailures"] = lambda jail=self._jail: \
-					jail.database.getBansMerged(ip=ip, jail=jail).getAttempt()
+				mi4ip = lambda overalljails=False, self=self, \
+					mi={'ip':ip, 'ticket':bTicket}: self.__getBansMerged(mi, overalljails)
+				aInfo["ipmatches"]      = lambda: "\n".join(mi4ip(True).getMatches())
+				aInfo["ipjailmatches"]  = lambda: "\n".join(mi4ip().getMatches())
+				aInfo["ipfailures"]     = lambda: mi4ip(True).getAttempt()
+				aInfo["ipjailfailures"] = lambda: mi4ip().getAttempt()
 			if self.__banManager.addBanTicket(bTicket):
 				logSys.notice("[%s] Ban %s" % (self._jail.name, aInfo["ip"]))
 				for name, action in self._actions.iteritems():
diff --git a/fail2ban/server/database.py b/fail2ban/server/database.py
--- a/fail2ban/server/database.py
+++ b/fail2ban/server/database.py
@@ -27,7 +27,7 @@
 import json
 import locale
 from functools import wraps
-from threading import Lock
+from threading import RLock
 
 from .mytime import MyTime
 from .ticket import FailTicket
@@ -123,7 +123,7 @@ class Fail2BanDb(object):
 
 	def __init__(self, filename, purgeAge=24*60*60):
 		try:
-			self._lock = Lock()
+			self._lock = RLock()
 			self._db = sqlite3.connect(
 				filename, check_same_thread=False,
 				detect_types=sqlite3.PARSE_DECLTYPES)
@@ -365,6 +365,10 @@ def addBan(self, cur, jail, ticket):
 			del self._bansMergedCache[(ticket.getIP(), jail)]
 		except KeyError:
 			pass
+		try:
+			del self._bansMergedCache[(ticket.getIP(), None)]
+		except KeyError:
+			pass
 		#TODO: Implement data parts once arbitrary match keys completed
 		cur.execute(
 			"INSERT INTO bans(jail, ip, timeofban, data) VALUES(?, ?, ?, ?)",
@@ -455,40 +459,41 @@ def getBansMerged(self, ip=None, jail=None, bantime=None):
 			in a list. When `ip` argument passed, a single `Ticket` is
 			returned.
 		"""
-		cacheKey = None
-		if bantime is None or bantime < 0:
-			cacheKey = (ip, jail)
-			if cacheKey in self._bansMergedCache:
-				return self._bansMergedCache[cacheKey]
-
-		tickets = []
-		ticket = None
-
-		results = list(self._getBans(ip=ip, jail=jail, bantime=bantime))
-		if results:
-			prev_banip = results[0][0]
-			matches = []
-			failures = 0
-			for banip, timeofban, data in results:
-				#TODO: Implement data parts once arbitrary match keys completed
-				if banip != prev_banip:
-					ticket = FailTicket(prev_banip, prev_timeofban, matches)
-					ticket.setAttempt(failures)
-					tickets.append(ticket)
-					# Reset variables
-					prev_banip = banip
-					matches = []
-					failures = 0
-				matches.extend(data['matches'])
-				failures += data['failures']
-				prev_timeofban = timeofban
-			ticket = FailTicket(banip, prev_timeofban, matches)
-			ticket.setAttempt(failures)
-			tickets.append(ticket)
-
-		if cacheKey:
-			self._bansMergedCache[cacheKey] = tickets if ip is None else ticket
-		return tickets if ip is None else ticket
+		with self._lock:
+			cacheKey = None
+			if bantime is None or bantime < 0:
+				cacheKey = (ip, jail)
+				if cacheKey in self._bansMergedCache:
+					return self._bansMergedCache[cacheKey]
+
+			tickets = []
+			ticket = None
+
+			results = list(self._getBans(ip=ip, jail=jail, bantime=bantime))
+			if results:
+				prev_banip = results[0][0]
+				matches = []
+				failures = 0
+				for banip, timeofban, data in results:
+					#TODO: Implement data parts once arbitrary match keys completed
+					if banip != prev_banip:
+						ticket = FailTicket(prev_banip, prev_timeofban, matches)
+						ticket.setAttempt(failures)
+						tickets.append(ticket)
+						# Reset variables
+						prev_banip = banip
+						matches = []
+						failures = 0
+					matches.extend(data['matches'])
+					failures += data['failures']
+					prev_timeofban = timeofban
+				ticket = FailTicket(banip, prev_timeofban, matches)
+				ticket.setAttempt(failures)
+				tickets.append(ticket)
+
+			if cacheKey:
+				self._bansMergedCache[cacheKey] = tickets if ip is None else ticket
+			return tickets if ip is None else ticket
 
 	@commitandrollback
 	def purge(self, cur):


Test patch:
diff --git a/fail2ban/tests/databasetestcase.py b/fail2ban/tests/databasetestcase.py
--- a/fail2ban/tests/databasetestcase.py
+++ b/fail2ban/tests/databasetestcase.py
@@ -32,18 +32,21 @@
 from ..server.filter import FileContainer
 from ..server.mytime import MyTime
 from ..server.ticket import FailTicket
+from ..server.actions import Actions
 from .dummyjail import DummyJail
 try:
 	from ..server.database import Fail2BanDb
 except ImportError:
 	Fail2BanDb = None
+from .utils import LogCaptureTestCase
 
 TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
 
-class DatabaseTest(unittest.TestCase):
+class DatabaseTest(LogCaptureTestCase):
 
 	def setUp(self):
 		"""Call before every test case."""
+		super(DatabaseTest, self).setUp()
 		if Fail2BanDb is None and sys.version_info >= (2,7): # pragma: no cover
 			raise unittest.SkipTest(
 				"Unable to import fail2ban database module as sqlite is not "
@@ -55,6 +58,7 @@ def setUp(self):
 
 	def tearDown(self):
 		"""Call after every test case."""
+		super(DatabaseTest, self).tearDown()
 		if Fail2BanDb is None: # pragma: no cover
 			return
 		# Cleanup
@@ -267,6 +271,22 @@ def testGetBansMerged(self):
 		tickets = self.db.getBansMerged(bantime=-1)
 		self.assertEqual(len(tickets), 2)
 
+	def testActionWithDB(self):
+		# test action together with database functionality
+		self.testAddJail() # Jail required
+		self.jail.database = self.db;
+		actions = Actions(self.jail)
+		actions.add(
+			"action_checkainfo",
+			os.path.join(TEST_FILES_DIR, "action.d/action_checkainfo.py"),
+			{})
+		ticket = FailTicket("1.2.3.4", MyTime.time(), ['test', 'test'])
+		ticket.setAttempt(5)
+		self.jail.putFailTicket(ticket)
+		actions._Actions__checkBan()
+		self.assertTrue(self._is_logged("ban ainfo %s, %s, %s, %s" % (True, True, True, True)))
+
+
 	def testPurge(self):
 		if Fail2BanDb is None: # pragma: no cover
 			return
diff --git a/fail2ban/tests/files/action.d/action_checkainfo.py b/fail2ban/tests/files/action.d/action_checkainfo.py
new file mode 100644
--- /dev/null
+++ b/fail2ban/tests/files/action.d/action_checkainfo.py
@@ -0,0 +1,14 @@
+
+from fail2ban.server.action import ActionBase
+
+class TestAction(ActionBase):
+
+    def ban(self, aInfo):
+        self._logSys.info("ban ainfo %s, %s, %s, %s",
+          aInfo["ipmatches"] != '', aInfo["ipjailmatches"] != '', aInfo["ipfailures"] > 0, aInfo["ipjailfailures"] > 0
+        )
+
+    def unban(self, aInfo):
+        pass
+
+Action = TestAction
diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py
--- a/fail2ban/tests/servertestcase.py
+++ b/fail2ban/tests/servertestcase.py
@@ -804,7 +804,7 @@ def testHost(self):
 
 class _BadThread(JailThread):
 	def run(self):
-		int("ignore this exception -- raised for testing")
+		raise RuntimeError('run bad thread exception')
 
 class LoggingTests(LogCaptureTestCase):
 
@@ -814,7 +814,15 @@ def testGetF2BLogger(self):
 		self.assertEqual(testLogSys.name, "fail2ban.name")
 
 	def testFail2BanExceptHook(self):
-		badThread = _BadThread()
-		badThread.start()
-		badThread.join()
-		self.assertTrue(self._is_logged("Unhandled exception"))
+		prev_exchook = sys.__excepthook__
+		x = []
+		sys.__excepthook__ = lambda *args: x.append(args)
+		try:
+			badThread = _BadThread()
+			badThread.start()
+			badThread.join()
+			self.assertTrue(self._is_logged("Unhandled exception"))
+		finally:
+			sys.__excepthook__ = prev_exchook
+		self.assertEqual(len(x), 1)
+		self.assertEqual(x[0][0], RuntimeError)