Repository: fail2ban/fail2ban

Issue:
BF: guarantee order of dbfile to be before dbpurgeage (Closes #1048)
I think this should fix #1048 but need to run atm so can't check "live". may be later

dbpurgeage won't be honored when be set in fail2ban.local
Setting dbpurgeage = 691200 in fail2ban.local has no effect.
fail2ban-client get dbpurgeage still respond with the Default value of 86400seconds



Discussion:

I bet you forgot to define it within a [Definition] or [DEFAULT] section. it must work. reopen if my guiess is incorrect

It may be honored, but `fail2ban-client get dbpurgeage` does not display what is defined and there seems to be some odd behavior with it. I haven't had time to look into it any further:

```
# cat /etc/fail2ban/fail2ban.local
[Definition]

dbpurgeage = 691200
```

```
# fail2ban-client get dbpurgeage
Current database purge age is:
`- 86400seconds
# fail2ban-client -d | grep dbpurgeage
['set', 'dbpurgeage', 691200]
```

`fail2ban-client set dbpurgeage` seems to work:

```
# fail2ban-client set dbpurgeage 123456
Current database purge age is:
`- 123456seconds
# fail2ban-client get dbpurgeage
Current database purge age is:
`- 123456seconds
```

but then....

```
# fail2ban-client -d | grep dbpurge
['set', 'dbpurgeage', 691200]
```

I concur with Lee's findings.  In addition every option I'm putting in fail2band.local won't be reflected either.


Resolving patch:
diff --git a/fail2ban/client/fail2banreader.py b/fail2ban/client/fail2banreader.py
--- a/fail2ban/client/fail2banreader.py
+++ b/fail2ban/client/fail2banreader.py
@@ -53,18 +53,14 @@ def getOptions(self):
 		self.__opts = ConfigReader.getOptions(self, "Definition", opts)
 	
 	def convert(self):
+		# Ensure logtarget/level set first so any db errors are captured
+		# Also dbfile should be set before all other database options.
+		# So adding order indices into items, to be stripped after sorting, upon return
+		order = {"syslogsocket":0, "loglevel":1, "logtarget":2,
+			"dbfile":50, "dbpurgeage":51}
 		stream = list()
 		for opt in self.__opts:
-			if opt == "loglevel":
-				stream.append(["set", "loglevel", self.__opts[opt]])
-			elif opt == "logtarget":
-				stream.append(["set", "logtarget", self.__opts[opt]])
-			elif opt == "syslogsocket":
-				stream.append(["set", "syslogsocket", self.__opts[opt]])
-			elif opt == "dbfile":
-				stream.append(["set", "dbfile", self.__opts[opt]])
-			elif opt == "dbpurgeage":
-				stream.append(["set", "dbpurgeage", self.__opts[opt]])
-		# Ensure logtarget/level set first so any db errors are captured
-		return sorted(stream, reverse=True)
+			if opt in order:
+				stream.append((order[opt], ["set", opt, self.__opts[opt]]))
+		return [opt[1] for opt in sorted(stream)]
 	
diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py
--- a/fail2ban/server/server.py
+++ b/fail2ban/server/server.py
@@ -67,10 +67,10 @@ def __init__(self, daemon = False):
 			'FreeBSD': '/var/run/log',
 			'Linux': '/dev/log',
 		}
+		self.setSyslogSocket("auto")
 		# Set logging level
 		self.setLogLevel("INFO")
 		self.setLogTarget("STDOUT")
-		self.setSyslogSocket("auto")
 
 	def __sigTERMhandler(self, signum, frame):
 		logSys.debug("Caught signal %d. Exiting" % signum)
diff --git a/fail2ban/server/transmitter.py b/fail2ban/server/transmitter.py
--- a/fail2ban/server/transmitter.py
+++ b/fail2ban/server/transmitter.py
@@ -139,6 +139,7 @@ def __commandSet(self, command):
 		elif name == "dbpurgeage":
 			db = self.__server.getDatabase()
 			if db is None:
+				logSys.warning("dbpurgeage setting was not in effect since no db yet")
 				return None
 			else:
 				db.purgeage = command[1]


Test patch:
diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py
--- a/fail2ban/tests/clientreadertestcase.py
+++ b/fail2ban/tests/clientreadertestcase.py
@@ -622,6 +622,22 @@ def testStockConfigurator(self):
 			configurator.getOptions()
 			configurator.convertToProtocol()
 			commands = configurator.getConfigStream()
+
+			# verify that dbfile comes before dbpurgeage
+			def find_set(option):
+				for i, e in enumerate(commands):
+					if e[0] == 'set' and e[1] == option:
+						return i
+				raise ValueError("Did not find command 'set %s' among commands %s"
+								 % (option, commands))
+
+			# Set up of logging should come first
+			self.assertEqual(find_set('syslogsocket'), 0)
+			self.assertEqual(find_set('loglevel'), 1)
+			self.assertEqual(find_set('logtarget'), 2)
+			# then dbfile should be before dbpurgeage
+			self.assertTrue(find_set('dbpurgeage') > find_set('dbfile'))
+
 			# and there is logging information left to be passed into the
 			# server
 			self.assertEqual(sorted(commands),