diff --git a/.pylintrc b/.pylintrc index 9e4eb71..af49c4f 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,5 +1,5 @@ # pylint config [MESSAGES CONTROL] -disable=line-too-long, redefined-outer-name, too-many-arguments, too-many-instance-attributes, fixme, invalid-name, superfluous-parens, missing-function-docstring, missing-module-docstring, multiple-imports, no-else-return, too-many-return-statements +disable=line-too-long, redefined-outer-name, too-many-arguments, too-many-instance-attributes, fixme, invalid-name, superfluous-parens, missing-function-docstring, missing-module-docstring, multiple-imports, no-else-return, too-many-return-statements, too-many-branches, too-many-statements [MASTER] ignore-patterns=^test.* diff --git a/check_http_json.py b/check_http_json.py index 3b8d6d9..4781abd 100755 --- a/check_http_json.py +++ b/check_http_json.py @@ -225,9 +225,9 @@ def __init__(self, json_data, rules_args): if self.rules.value_separator: value_separator = self.rules.value_separator self.helper = JsonHelper(self.data, separator, value_separator) - debugPrint(rules_args.debug, "rules:%s" % rules_args) - debugPrint(rules_args.debug, "separator:%s" % separator) - debugPrint(rules_args.debug, "value_separator:%s" % value_separator) + debugPrint(rules_args.debug, "rules: %s" % rules_args) + debugPrint(rules_args.debug, "separator: %s" % separator) + debugPrint(rules_args.debug, "value_separator: %s" % value_separator) self.metric_list = self.expandKeys(self.rules.metric_list) self.key_threshold_warning = self.expandKeys( self.rules.key_threshold_warning) @@ -346,6 +346,8 @@ def checkWarning(self): def checkCritical(self): failure = '' + if not self.data: + failure = " Empty JSON data." if self.key_threshold_critical is not None: failure += self.checkThresholds(self.key_threshold_critical) if self.key_value_list_critical is not None: @@ -424,7 +426,7 @@ def parseArgs(args): parser.add_argument('-s', '--ssl', action='store_true', help='use TLS to connect to remote host') parser.add_argument('-H', '--host', dest='host', - required=not ('-V' in sys.argv or '--version' in sys.argv), + required=not ('-V' in args or '--version' in args), help='remote host to query') parser.add_argument('-k', '--insecure', action='store_true', help='do not check server SSL certificate') @@ -524,10 +526,12 @@ def debugPrint(debug_flag, message, pretty_flag=False): print(message) -# Program entry point -if __name__ == "__main__": +def main(cliargs): + """ + Main entrypoint for CLI + """ - args = parseArgs(sys.argv[1:]) + args = parseArgs(cliargs) nagios = NagiosHelper() context = None @@ -607,7 +611,11 @@ def debugPrint(debug_flag, message, pretty_flag=False): json_data = response.read() except HTTPError as e: - nagios.append_unknown(" HTTPError[%s], url:%s" % (str(e.code), url)) + # Try to recover from HTTP Error, if there is JSON in the response + if "json" in e.info().get_content_subtype(): + json_data = e.read() + else: + nagios.append_unknown(" HTTPError[%s], url:%s" % (str(e.code), url)) except URLError as e: nagios.append_critical(" URLError[%s], url:%s" % (str(e.reason), url)) @@ -630,4 +638,9 @@ def debugPrint(debug_flag, message, pretty_flag=False): print(nagios.getMessage()) sys.exit(nagios.getCode()) + +if __name__ == "__main__": + # Program entry point + main(sys.argv[1:]) + #EOF diff --git a/makefile b/makefile new file mode 100644 index 0000000..3f3e98b --- /dev/null +++ b/makefile @@ -0,0 +1,9 @@ +.PHONY: lint test coverage + +lint: + python3 -m pylint check_http_json.py +test: + python3 -m unittest discover +coverage: + python3 -m coverage run -m unittest discover + python3 -m coverage report -m --include check_http_json.py diff --git a/test/requirements.txt b/test/requirements.txt new file mode 100644 index 0000000..6467d27 --- /dev/null +++ b/test/requirements.txt @@ -0,0 +1,2 @@ +coverage==5.0.3 +pylint==2.4.4 diff --git a/test/test_check_http_json.py b/test/test_check_http_json.py index 489213a..659e77c 100644 --- a/test/test_check_http_json.py +++ b/test/test_check_http_json.py @@ -256,7 +256,7 @@ def test_array_with_missing_element(self): # This should not throw a KeyError data = '{}' - self.check_data(rules.dash_q(['(0).Node,foobar', '(1).Node,missing']), data, WARNING_CODE) + self.check_data(rules.dash_q(['(0).Node,foobar', '(1).Node,missing']), data, CRITICAL_CODE) def test_subelem(self): @@ -274,3 +274,23 @@ def test_subarrayelem_missing_elem(self): self.check_data(rules.dash_E(['(*).capacity.value.too_deep']), data, CRITICAL_CODE) # Should not throw keyerror self.check_data(rules.dash_E(['foo']), data, CRITICAL_CODE) + + + def test_empty_key_value_array(self): + """ + https://github.com/drewkerrigan/nagios-http-json/issues/61 + """ + + rules = RulesHelper() + + # This should simply work + data = '[{"update_status": "finished"},{"update_status": "finished"}]' + self.check_data(rules.dash_q(['(*).update_status,finished']), data, OK_CODE) + + # This should warn us + data = '[{"update_status": "finished"},{"update_status": "failure"}]' + self.check_data(rules.dash_q(['(*).update_status,finished']), data, WARNING_CODE) + + # This should throw an error + data = '[]' + self.check_data(rules.dash_q(['(*).update_status,warn_me']), data, CRITICAL_CODE) diff --git a/test/test_cli.py b/test/test_cli.py new file mode 100644 index 0000000..8766ef5 --- /dev/null +++ b/test/test_cli.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + + +import unittest +import unittest.mock as mock +import sys +import os + +sys.path.append('..') + +from check_http_json import debugPrint + + +class CLITest(unittest.TestCase): + """ + Tests for CLI + """ + + def setUp(self): + """ + Defining the exitcodes + """ + + self.exit_0 = 0 << 8 + self.exit_1 = 1 << 8 + self.exit_2 = 2 << 8 + self.exit_3 = 3 << 8 + + def test_debugprint(self): + with mock.patch('builtins.print') as mock_print: + debugPrint(True, 'debug') + mock_print.assert_called_once_with('debug') + + def test_debugprint_pprint(self): + with mock.patch('check_http_json.pprint') as mock_pprint: + debugPrint(True, 'debug', True) + mock_pprint.assert_called_once_with('debug') + + def test_cli_without_params(self): + + command = '/usr/bin/env python3 check_http_json.py > /dev/null 2>&1' + status = os.system(command) + + self.assertEqual(status, self.exit_2) diff --git a/test/test_main.py b/test/test_main.py index 873c62d..a531af7 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -8,37 +8,90 @@ sys.path.append('..') -from check_http_json import debugPrint +from check_http_json import main + + +class MockResponse(): + def __init__(self, status_code=200, content='{"foo": "bar"}'): + self.status_code = status_code + self.content = content + + def read(self): + return self.content class MainTest(unittest.TestCase): """ - Tests for main + Tests for Main """ - def setUp(self): - """ - Defining the exitcodes - """ + @mock.patch('builtins.print') + def test_main_version(self, mock_print): + args = ['--version'] + + with self.assertRaises(SystemExit) as test: + main(args) + + mock_print.assert_called_once() + self.assertEqual(test.exception.code, 0) + + @mock.patch('builtins.print') + @mock.patch('urllib.request.urlopen') + def test_main_with_ssl(self, mock_request, mock_print): + args = '-H localhost --ssl'.split(' ') + + mock_request.return_value = MockResponse() + + with self.assertRaises(SystemExit) as test: + main(args) + + self.assertEqual(test.exception.code, 0) + + + @mock.patch('builtins.print') + @mock.patch('urllib.request.urlopen') + def test_main_with_parse_error(self, mock_request, mock_print): + args = '-H localhost'.split(' ') + + mock_request.return_value = MockResponse(content='not JSON') + + with self.assertRaises(SystemExit) as test: + main(args) + + self.assertTrue('Parser error' in str(mock_print.call_args)) + self.assertEqual(test.exception.code, 3) + + @mock.patch('builtins.print') + def test_main_with_url_error(self, mock_print): + args = '-H localhost'.split(' ') + + with self.assertRaises(SystemExit) as test: + main(args) + + self.assertTrue('URLError' in str(mock_print.call_args)) + self.assertEqual(test.exception.code, 3) + + @mock.patch('builtins.print') + @mock.patch('urllib.request.urlopen') + def test_main_with_http_error_no_json(self, mock_request, mock_print): + args = '-H localhost'.split(' ') + + mock_request.return_value = MockResponse(content='not JSON', status_code=503) - self.exit_0 = 0 << 8 - self.exit_1 = 1 << 8 - self.exit_2 = 2 << 8 - self.exit_3 = 3 << 8 + with self.assertRaises(SystemExit) as test: + main(args) - def test_debugprint(self): - with mock.patch('builtins.print') as mock_print: - debugPrint(True, 'debug') - mock_print.assert_called_once_with('debug') + self.assertTrue('Parser error' in str(mock_print.call_args)) + self.assertEqual(test.exception.code, 3) - def test_debugprint_pprint(self): - with mock.patch('check_http_json.pprint') as mock_pprint: - debugPrint(True, 'debug', True) - mock_pprint.assert_called_once_with('debug') + @mock.patch('builtins.print') + @mock.patch('urllib.request.urlopen') + def test_main_with_http_error_valid_json(self, mock_request, mock_print): + args = '-H localhost'.split(' ') - def test_cli_without_params(self): + mock_request.return_value = MockResponse(status_code=503) - command = '/usr/bin/env python3 check_http_json.py > /dev/null 2>&1' - status = os.system(command) + with self.assertRaises(SystemExit) as test: + main(args) - self.assertEqual(status, self.exit_2) + self.assertEqual(test.exception.code, 0)