Repository: fail2ban/fail2ban

Issue:
fail2ban-regex error: TypeError: unorderable types: NoneType() <= int()
### Environment:
- Fail2Ban v0.10.0a2
- OS, MacOS 10.12 Sierra
- Python 3.5
- Fail2Ban installed via github clone > python3 setup.py install
- [x] No foreign patches
- Some customizations in jail.local

Firstly thanks for the work on fail2ban.
fail2ban-client and fail2ban-server seem to be working OK but I get the following error with any fail2ban-regex test, eg

fail2ban-regex /var/log/mail.log /etc/fail2ban/filter.d/postfix.conf
# Running tests

Traceback (most recent call last):
  File "/usr/local/bin/fail2ban-regex", line 34, in <module>
    exec_command_line()
  File "/opt/local/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/site-packages/fail2ban/client/fail2banregex.py", line 591, in exec_command_line
    fmt = '%(levelname)-1.1s: %(message)s' if opts.verbose <= 1 else '%(message)s'
TypeError: unorderable types: NoneType() <= int()



Discussion:

Resolving patch:
diff --git a/fail2ban/client/fail2banregex.py b/fail2ban/client/fail2banregex.py
--- a/fail2ban/client/fail2banregex.py
+++ b/fail2ban/client/fail2banregex.py
@@ -40,7 +40,7 @@
 
 from ConfigParser import NoOptionError, NoSectionError, MissingSectionHeaderError
 
-try:
+try: # pragma: no cover
 	from systemd import journal
 	from ..server.filtersystemd import FilterSystemd
 except ImportError:
@@ -80,7 +80,7 @@ def pprint_list(l, header=None):
 		s = ''
 	output( s + "|  " + "\n|  ".join(l) + '\n`-' )
 
-def journal_lines_gen(myjournal):
+def journal_lines_gen(myjournal): # pragma: no cover
 	while True:
 		try:
 			entry = myjournal.get_next()
@@ -136,13 +136,12 @@ def get_opt_parser():
 			   "\"systemd-journal\" only"),
 		Option('-l', "--log-level",
 			   dest="log_level",
-			   default=None,
+			   default='critical',
 			   help="Log level for the Fail2Ban logger to use"),
 		Option('-v', '--verbose', action="count", dest="verbose",
-			   default=None,
+			   default=0,
 			   help="Increase verbosity"),
 		Option("--verbosity", action="store", dest="verbose", type=int,
-			   default=None,
 			   help="Set numerical level of verbosity (0..4)"),
 		Option("-D", "--debuggex", action='store_true',
 			   help="Produce debuggex.com urls for debugging there"),
@@ -343,7 +342,7 @@ def testIgnoreRegex(self, line):
 				found = True
 				regex = self._ignoreregex[ret].inc()
 		except RegexException as e:
-			output( e )
+			output( 'ERROR: %s' % e )
 			return False
 		return found
 
@@ -360,10 +359,7 @@ def testRegex(self, line, date=None):
 				regex.inc()
 				regex.appendIP(match)
 		except RegexException as e:
-			output( e )
-			return False
-		except IndexError:
-			output( "Sorry, but no <HOST> found in regex" )
+			output( 'ERROR: %s' % e )
 			return False
 		for bufLine in orgLineBuffer[int(fullBuffer):]:
 			if bufLine not in self._filter._Filter__lineBuffer:
@@ -509,10 +505,13 @@ def start(self, opts, args):
 
 		cmd_log, cmd_regex = args[:2]
 
-		if not self.readRegex(cmd_regex, 'fail'):
-			return False
-
-		if len(args) == 3 and not self.readRegex(args[2], 'ignore'):
+		try:
+			if not self.readRegex(cmd_regex, 'fail'):
+				return False
+			if len(args) == 3 and not self.readRegex(args[2], 'ignore'):
+				return False
+		except RegexException as e:
+			output( 'ERROR: %s' % e )
 			return False
 
 		if os.path.isfile(cmd_log):
@@ -556,43 +555,36 @@ def start(self, opts, args):
 		return True
 
 
-def exec_command_line():
+def exec_command_line(*args):
 	parser = get_opt_parser()
-	(opts, args) = parser.parse_args()
-	if opts.print_no_missed and opts.print_all_missed:
-		sys.stderr.write("ERROR: --print-no-missed and --print-all-missed are mutually exclusive.\n\n")
-		parser.print_help()
-		sys.exit(-1)
-	if opts.print_no_ignored and opts.print_all_ignored:
-		sys.stderr.write("ERROR: --print-no-ignored and --print-all-ignored are mutually exclusive.\n\n")
-		parser.print_help()
-		sys.exit(-1)
+	(opts, args) = parser.parse_args(*args)
+	errors = []
+	if opts.print_no_missed and opts.print_all_missed: # pragma: no cover
+		errors.append("ERROR: --print-no-missed and --print-all-missed are mutually exclusive.")
+	if opts.print_no_ignored and opts.print_all_ignored: # pragma: no cover
+		errors.append("ERROR: --print-no-ignored and --print-all-ignored are mutually exclusive.")
 
 	# We need 2 or 3 parameters
 	if not len(args) in (2, 3):
-		sys.stderr.write("ERROR: provide both <LOG> and <REGEX>.\n\n")
+		errors.append("ERROR: provide both <LOG> and <REGEX>.")
+	if errors:
+		sys.stderr.write("\n".join(errors) + "\n\n")
 		parser.print_help()
-		return False
+		sys.exit(-1)
 
 	output( "" )
 	output( "Running tests" )
 	output( "=============" )
 	output( "" )
 
-	# TODO: taken from -testcases -- move common functionality somewhere
-	if opts.log_level is not None:
-		# so we had explicit settings
-		logSys.setLevel(str2LogLevel(opts.log_level))
-	else:
-		# suppress the logging but it would leave unittests' progress dots
-		# ticking, unless like with '-l critical' which would be silent
-		# unless error occurs
-		logSys.setLevel(logging.CRITICAL)
+	# Log level (default critical):
+	opts.log_level = str2LogLevel(opts.log_level)
+	logSys.setLevel(opts.log_level)
 
 	# Add the default logging handler
 	stdout = logging.StreamHandler(sys.stdout)
 
-	fmt = '%(levelname)-1.1s: %(message)s' if opts.verbose <= 1 else '%(message)s'
+	fmt = '%(levelname)-1.1s: %(message)s' if opts.verbose <= 1 else ' %(message)s'
 
 	if opts.log_traceback:
 		Formatter = FormatterWithTraceBack


Test patch:
diff --git a/fail2ban/tests/fail2banregextestcase.py b/fail2ban/tests/fail2banregextestcase.py
--- a/fail2ban/tests/fail2banregextestcase.py
+++ b/fail2ban/tests/fail2banregextestcase.py
@@ -24,9 +24,10 @@
 __license__ = "GPL"
 
 import os
+import sys
 
 from ..client import fail2banregex
-from ..client.fail2banregex import Fail2banRegex, get_opt_parser, output
+from ..client.fail2banregex import Fail2banRegex, get_opt_parser, exec_command_line, output
 from .utils import setUpMyTime, tearDownMyTime, LogCaptureTestCase, logSys
 from .utils import CONFIG_DIR
 
@@ -39,12 +40,38 @@ def _test_output(*args):
 
 TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
 
+DEV_NULL = None
 
 def _Fail2banRegex(*args):
 	parser = get_opt_parser()
 	(opts, args) = parser.parse_args(list(args))
 	return (opts, args, Fail2banRegex(opts))
 
+class ExitException(Exception):
+	def __init__(self, code):
+		self.code = code
+		self.msg = 'Exit with code: %s' % code
+
+def _test_exec_command_line(*args):
+	def _exit(code=0):
+		raise ExitException(code)
+	global DEV_NULL
+	_org = {'exit': sys.exit, 'stdout': sys.stdout, 'stderr': sys.stderr}
+	_exit_code = 0
+	sys.exit = _exit
+	if not DEV_NULL: DEV_NULL = open(os.devnull, "w")
+	sys.stderr = sys.stdout = DEV_NULL
+	try:
+		exec_command_line(list(args))
+	except ExitException as e:
+		_exit_code = e.code
+	finally:
+		sys.exit = _org['exit']
+		sys.stdout = _org['stdout']
+		sys.stderr = _org['stderr']
+	return _exit_code
+
+
 class Fail2banRegexTest(LogCaptureTestCase):
 
 	RE_00 = r"(?:(?:Authentication failure|Failed [-/\w+]+) for(?: [iI](?:llegal|nvalid) user)?|[Ii](?:llegal|nvalid) user|ROOT LOGIN REFUSED) .*(?: from|FROM) <HOST>"
@@ -69,14 +96,14 @@ def testWrongRE(self):
 		(opts, args, fail2banRegex) = _Fail2banRegex(
 			"test", r".** from <HOST>$"
 		)
-		self.assertRaises(Exception, lambda: fail2banRegex.start(opts, args))
+		self.assertFalse(fail2banRegex.start(opts, args))
 		self.assertLogged("Unable to compile regular expression")
 
 	def testWrongIngnoreRE(self):
 		(opts, args, fail2banRegex) = _Fail2banRegex(
 			"test", r".*? from <HOST>$", r".**"
 		)
-		self.assertRaises(Exception, lambda: fail2banRegex.start(opts, args))
+		self.assertFalse(fail2banRegex.start(opts, args))
 		self.assertLogged("Unable to compile regular expression")
 
 	def testDirectFound(self):
@@ -184,4 +211,21 @@ def testWronCharDebuggex(self):
 
 		self.assertLogged('https://')
 
+	def testExecCmdLine_Usage(self):
+		self.assertNotEqual(_test_exec_command_line(), 0)
 
+	def testExecCmdLine_Direct(self):
+		self.assertEqual(_test_exec_command_line(
+			'-l', 'info',
+			"Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 192.0.2.0",
+			r"Authentication failure for .*? from <HOST>$"
+		), 0)
+		self.assertLogged('Lines: 1 lines, 0 ignored, 1 matched, 0 missed')
+		
+	def testExecCmdLine_MissFailID(self):
+		self.assertNotEqual(_test_exec_command_line(
+			'-l', 'info',
+			"Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 192.0.2.0",
+			r"Authentication failure"
+		), 0)
+		self.assertLogged('No failure-id group in ')