Repository: fail2ban/fail2ban

Issue:
ENH+BF: Provide <fail2ban-agent-string> tag
Was crafted to resolve #1271 but actually not sure now if is worth this approach over "let's execute fail2ban-client version" and define that variable for string interpolations in e.g. actions/common.conf



Discussion:
@fail2ban/developers , @fail2ban/contributors -- any feedback?

I don't like it - too many changes (and conflicts :)) for one little thing (a new parameter used in action), that can be also easy added somewhere in configreader (ActionReader or JailReader)...
Will try to check it now...

I don't quite like it as well, but not for "too many changes" of which I see not that many ;-)  Will wait then for your version


Resolving patch:
diff --git a/config/action.d/badips.py b/config/action.d/badips.py
--- a/config/action.d/badips.py
+++ b/config/action.d/badips.py
@@ -21,7 +21,6 @@
 if sys.version_info < (2, 7):
 	raise ImportError("badips.py action requires Python >= 2.7")
 import json
-from functools import partial
 import threading
 import logging
 if sys.version_info >= (3, ):
@@ -33,7 +32,6 @@
 	from urllib import urlencode
 
 from fail2ban.server.actions import ActionBase
-from fail2ban.version import version as f2bVersion
 
 
 class BadIPsAction(ActionBase):
@@ -72,6 +70,9 @@ class BadIPsAction(ActionBase):
 	updateperiod : int, optional
 		Time in seconds between updating bad IPs blacklist.
 		Default 900 (15 minutes)
+	agent : str, optional
+		User agent transmitted to server.
+		Default `Fail2Ban/ver.`
 
 	Raises
 	------
@@ -80,13 +81,14 @@ class BadIPsAction(ActionBase):
 	"""
 
 	_badips = "http://www.badips.com"
-	_Request = partial(
-		Request, headers={'User-Agent': "Fail2Ban %s" % f2bVersion})
+	def _Request(self, url, **argv):
+		return Request(url, headers={'User-Agent': self.agent}, **argv)
 
 	def __init__(self, jail, name, category, score=3, age="24h", key=None,
-		banaction=None, bancategory=None, bankey=None, updateperiod=900):
+		banaction=None, bancategory=None, bankey=None, updateperiod=900, agent="Fail2Ban"):
 		super(BadIPsAction, self).__init__(jail, name)
 
+		self.agent = agent
 		self.category = category
 		self.score = score
 		self.age = age
diff --git a/fail2ban/client/jailreader.py b/fail2ban/client/jailreader.py
--- a/fail2ban/client/jailreader.py
+++ b/fail2ban/client/jailreader.py
@@ -32,6 +32,7 @@
 from .configreader import ConfigReaderUnshared, ConfigReader
 from .filterreader import FilterReader
 from .actionreader import ActionReader
+from ..version import version
 from ..helpers import getLogger
 from ..helpers import splitcommaspace
 
@@ -108,6 +109,10 @@ def getOptions(self):
 				["string", "filter", ""],
 				["string", "action", ""]]
 
+		# Before interpolation (substitution) add static options always available as default:
+		defsec = self._cfg.get_defaults()
+		defsec["fail2ban_version"] = version
+
 		# Read first options only needed for merge defaults ('known/...' from filter):
 		self.__opts = ConfigReader.getOptions(self, self.__name, opts1st)
 		if not self.__opts:


Test patch:
diff --git a/fail2ban/tests/clientreadertestcase.py b/fail2ban/tests/clientreadertestcase.py
--- a/fail2ban/tests/clientreadertestcase.py
+++ b/fail2ban/tests/clientreadertestcase.py
@@ -28,18 +28,20 @@
 import shutil
 import tempfile
 import unittest
-from ..client.configreader import ConfigReaderUnshared
+from ..client.configreader import ConfigReader, ConfigReaderUnshared
 from ..client import configparserinc
 from ..client.jailreader import JailReader
 from ..client.filterreader import FilterReader
 from ..client.jailsreader import JailsReader
 from ..client.actionreader import ActionReader
 from ..client.configurator import Configurator
+from ..version import version
 from .utils import LogCaptureTestCase
 
 TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
 
 from .utils import CONFIG_DIR
+CONFIG_DIR_TESTSHARE_CFG = {}
 
 STOCK = os.path.exists(os.path.join('config','fail2ban.conf'))
 
@@ -251,6 +253,34 @@ def testSplitOption(self):
 		result = JailReader.extractOptions(option)
 		self.assertEqual(expected, result)
 
+	def testVersionAgent(self):
+		jail = JailReader('blocklisttest', force_enable=True, basedir=CONFIG_DIR)
+		# emulate jail.read(), because such jail not exists:
+		ConfigReader.read(jail, "jail"); 
+		sections = jail._cfg.get_sections()
+		sections['blocklisttest'] = dict((('__name__', 'blocklisttest'), 
+			('filter', ''),	('failregex', '^test <HOST>$'),
+			('sender', 'f2b-test@example.com'), ('blocklist_de_apikey', 'test-key'), 
+			('action', 
+				'%(action_blocklist_de)s\n'
+				'%(action_badips_report)s\n'
+				'%(action_badips)s\n'
+				'mynetwatchman[port=1234,protocol=udp,agent="%(fail2ban_agent)s"]'
+			),
+		))
+		# get options:
+		self.assertTrue(jail.getOptions())
+		# convert and get stream
+		stream = jail.convert()
+		# get action and retrieve agent from it, compare with agent saved in version:
+		act = [o for o in stream if len(o) > 4 and (o[4] == 'agent' or o[4].endswith('badips.py'))]
+		useragent = 'Fail2Ban/%s' % version
+		self.assertEqual(len(act), 4)
+		self.assertEqual(act[0], ['set', 'blocklisttest', 'action', 'blocklist_de', 'agent', useragent])
+		self.assertEqual(act[1], ['set', 'blocklisttest', 'action', 'badips', 'agent', useragent])
+		self.assertEqual(eval(act[2][5]).get('agent', '<wrong>'), useragent)
+		self.assertEqual(act[3], ['set', 'blocklisttest', 'action', 'mynetwatchman', 'agent', useragent])
+
 	def testGlob(self):
 		d = tempfile.mkdtemp(prefix="f2b-temp")
 		# Generate few files