Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add flag to increase verbosity and flag to override unreachable state #86

Merged
merged 4 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var/
.installed.cfg
*.egg
.venv/
venv/

# PyInstaller
# Usually these files are written by a python script from a template
Expand Down
203 changes: 112 additions & 91 deletions check_http_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import argparse
import sys
import ssl
from pprint import pprint
from urllib.error import HTTPError
from urllib.error import URLError

Expand Down Expand Up @@ -69,20 +68,19 @@ def getCode(self):
code = UNKNOWN_CODE
return code

def append_warning(self, warning_message):
self.warning_message += warning_message

def append_critical(self, critical_message):
self.critical_message += critical_message

def append_unknown(self, unknown_message):
self.unknown_message += unknown_message
def append_message(self, code, msg):
if code > 2 or code < 0:
self.unknown_message += msg
if code == 1:
self.warning_message += msg
if code == 2:
self.critical_message += msg

def append_metrics(self, metrics):
(performance_data, warning_message, critical_message) = metrics
self.performance_data += performance_data
self.append_warning(warning_message)
self.append_critical(critical_message)
self.append_message(WARNING_CODE, warning_message)
self.append_message(CRITICAL_CODE, critical_message)


class JsonHelper:
Expand Down Expand Up @@ -423,6 +421,9 @@ def parseArgs(args):

parser.add_argument('-d', '--debug', action='store_true',
help='debug mode')
parser.add_argument('-v', '--verbose', action='count', default=0,
help='Verbose mode. Multiple -v options increase the verbosity')

parser.add_argument('-s', '--ssl', action='store_true',
help='use TLS to connect to remote host')
parser.add_argument('-H', '--host', dest='host',
Expand All @@ -444,6 +445,8 @@ def parseArgs(args):
parser.add_argument('-p', '--path', dest='path', help='Path')
parser.add_argument('-t', '--timeout', type=int,
help='Connection timeout (seconds)')
parser.add_argument('--unreachable-state', type=int, default=3,
help='Exit with specified code if URL unreachable. Examples: 1 for Warning, 2 for Critical, 3 for Unknown (default: 3)')
parser.add_argument('-B', '--basic-auth', dest='auth',
help='Basic auth string "username:password"')
parser.add_argument('-D', '--data', dest='data',
Expand Down Expand Up @@ -516,16 +519,92 @@ def parseArgs(args):
return parser.parse_args(args)


def debugPrint(debug_flag, message, pretty_flag=False):
def debugPrint(debug_flag, message):
"""
Print debug messages if -d (debug_flat ) is set.
Print debug messages if -d is set.
"""
if not debug_flag:
return

if debug_flag:
if pretty_flag:
pprint(message)
else:
print(message)
print(message)

def verbosePrint(verbose_flag, when, message):
"""
Print verbose messages if -v is set.
Since -v can be used multiple times, the when parameter sets the required amount before printing
"""
if not verbose_flag:
return
if verbose_flag >= when:
print(message)

def prepare_context(args):
"""
Prepare TLS Context
"""
nagios = NagiosHelper()

context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.options |= ssl.OP_NO_SSLv2
context.options |= ssl.OP_NO_SSLv3

if args.insecure:
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
else:
context.verify_mode = ssl.CERT_OPTIONAL
context.load_default_certs()
if args.cacert:
try:
context.load_verify_locations(args.cacert)
except ssl.SSLError:
nagios.append_message(UNKNOWN_CODE, 'Error loading SSL CA cert "%s"!' % args.cacert)
if args.cert:
try:
context.load_cert_chain(args.cert, keyfile=args.key)
except ssl.SSLError:
if args.key:
nagios.append_message(UNKNOWN_CODE, 'Error loading SSL cert. Make sure key "%s" belongs to cert "%s"!' % (args.key, args.cert))
else:
nagios.append_message(UNKNOWN_CODE, 'Error loading SSL cert. Make sure "%s" contains the key as well!' % (args.cert))

if nagios.getCode() != OK_CODE:
print(nagios.getMessage())
sys.exit(nagios.getCode())

return context


def make_request(args, url, context):
"""
Performs the actual request to the given URL
"""
req = urllib.request.Request(url, method=args.method)
req.add_header("User-Agent", "check_http_json")
if args.auth:
authbytes = str(args.auth).encode()
base64str = base64.encodebytes(authbytes).decode().replace('\n', '')
req.add_header('Authorization', 'Basic %s' % base64str)
if args.headers:
headers = json.loads(args.headers)
debugPrint(args.debug, "Headers:\n %s" % headers)
for header in headers:
req.add_header(header, headers[header])
if args.timeout and args.data:
databytes = str(args.data).encode()
response = urllib.request.urlopen(req, timeout=args.timeout,
data=databytes, context=context)
elif args.timeout:
response = urllib.request.urlopen(req, timeout=args.timeout,
context=context)
elif args.data:
databytes = str(args.data).encode()
response = urllib.request.urlopen(req, data=databytes, context=context)
else:
# pylint: disable=consider-using-with
response = urllib.request.urlopen(req, context=context)

return response.read()


def main(cliargs):
Expand All @@ -543,103 +622,45 @@ def main(cliargs):

if args.ssl:
url = "https://%s" % args.host

context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.options |= ssl.OP_NO_SSLv2
context.options |= ssl.OP_NO_SSLv3

if args.insecure:
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
else:
context.verify_mode = ssl.CERT_OPTIONAL
context.load_default_certs()
if args.cacert:
try:
context.load_verify_locations(args.cacert)
except ssl.SSLError:
nagios.append_unknown(
'Error loading SSL CA cert "%s"!'
% args.cacert)

if args.cert:
try:
context.load_cert_chain(args.cert, keyfile=args.key)
except ssl.SSLError:
if args.key:
nagios.append_unknown(
'Error loading SSL cert. Make sure key "%s" belongs to cert "%s"!'
% (args.key, args.cert))
else:
nagios.append_unknown(
'Error loading SSL cert. Make sure "%s" contains the key as well!'
% (args.cert))

if nagios.getCode() != OK_CODE:
print(nagios.getMessage())
sys.exit(nagios.getCode())

context = prepare_context(args)
else:
url = "http://%s" % args.host
if args.port:
url += ":%s" % args.port
if args.path:
url += "/%s" % args.path

debugPrint(args.debug, "url:%s" % url)
debugPrint(args.debug, "url: %s" % url)
json_data = ''

try:
req = urllib.request.Request(url, method=args.method)
req.add_header("User-Agent", "check_http_json")
if args.auth:
authbytes = str(args.auth).encode()
base64str = base64.encodebytes(authbytes).decode().replace('\n', '')
req.add_header('Authorization', 'Basic %s' % base64str)
if args.headers:
headers = json.loads(args.headers)
debugPrint(args.debug, "Headers:\n %s" % headers)
for header in headers:
req.add_header(header, headers[header])
if args.timeout and args.data:
databytes = str(args.data).encode()
response = urllib.request.urlopen(req, timeout=args.timeout,
data=databytes, context=context)
elif args.timeout:
response = urllib.request.urlopen(req, timeout=args.timeout,
context=context)
elif args.data:
databytes = str(args.data).encode()
response = urllib.request.urlopen(req, data=databytes, context=context)
else:
# pylint: disable=consider-using-with
response = urllib.request.urlopen(req, context=context)

json_data = response.read()

json_data = make_request(args, url, context)
except HTTPError as e:
# 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))
nagios.append_message(UNKNOWN_CODE, " Could not find JSON in HTTP body. HTTPError[%s], url:%s" % (str(e.code), url))
except URLError as e:
nagios.append_critical(" URLError[%s], url:%s" % (str(e.reason), url))
# Some users might prefer another exit code if the URL wasn't reached
exit_code = args.unreachable_state
nagios.append_message(exit_code, " URLError[%s], url:%s" % (str(e.reason), url))
# Since we don't got any data, we can simply exit
print(nagios.getMessage())
sys.exit(nagios.getCode())

try:
data = json.loads(json_data)
except ValueError as e:
nagios.append_unknown(" Parser error: %s" % str(e))

nagios.append_message(UNKNOWN_CODE, " JSON Parser error: %s" % str(e))
else:
debugPrint(args.debug, 'json:')
debugPrint(args.debug, data, True)
verbosePrint(args.verbose, 1, json.dumps(data, indent=2))
# Apply rules to returned JSON data
processor = JsonRuleProcessor(data, args)
nagios.append_warning(processor.checkWarning())
nagios.append_critical(processor.checkCritical())
nagios.append_message(WARNING_CODE, processor.checkWarning())
nagios.append_message(CRITICAL_CODE, processor.checkCritical())
nagios.append_metrics(processor.checkMetrics())
nagios.append_unknown(processor.checkUnknown())
nagios.append_message(UNKNOWN_CODE, processor.checkUnknown())

# Print Nagios specific string and exit appropriately
print(nagios.getMessage())
Expand Down
4 changes: 2 additions & 2 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
coverage==6.4.4
pylint==2.15.2
coverage==6.5.0
pylint==2.17.7
6 changes: 3 additions & 3 deletions test/test_check_http_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,10 @@ def check_data(self, args, jsondata, code):
data = json.loads(jsondata)
nagios = NagiosHelper()
processor = JsonRuleProcessor(data, args)
nagios.append_warning(processor.checkWarning())
nagios.append_critical(processor.checkCritical())
nagios.append_message(WARNING_CODE, processor.checkWarning())
nagios.append_message(CRITICAL_CODE, processor.checkCritical())
nagios.append_metrics(processor.checkMetrics())
nagios.append_unknown(processor.checkUnknown())
nagios.append_message(UNKNOWN_CODE, processor.checkUnknown())
self.assertEqual(code, nagios.getCode())

def test_metrics(self):
Expand Down
12 changes: 8 additions & 4 deletions test/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
sys.path.append('..')

from check_http_json import debugPrint
from check_http_json import verbosePrint


class CLITest(unittest.TestCase):
Expand All @@ -31,10 +32,13 @@ def test_debugprint(self):
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_verbose(self):
with mock.patch('builtins.print') as mock_print:
verbosePrint(0, 3, 'verbose')
mock_print.assert_not_called()

verbosePrint(3, 3, 'verbose')
mock_print.assert_called_once_with('verbose')

def test_cli_without_params(self):

Expand Down
34 changes: 34 additions & 0 deletions test/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,37 @@ def test_main_with_http_error_valid_json(self, mock_request, mock_print):
main(args)

self.assertEqual(test.exception.code, 0)

@mock.patch('builtins.print')
def test_main_with_tls(self, mock_print):
args = ['-H', 'localhost',
'--ssl',
'--cacert',
'test/tls/ca-root.pem',
'--cert',
'test/tls/cert.pem',
'--key',
'test/tls/key.pem']

with self.assertRaises(SystemExit) as test:
main(args)

self.assertTrue('https://localhost' in str(mock_print.call_args))
self.assertEqual(test.exception.code, 3)

@mock.patch('builtins.print')
def test_main_with_tls_wrong_ca(self, mock_print):
args = ['-H', 'localhost',
'--ssl',
'--cacert',
'test/tls/key.pem',
'--cert',
'test/tls/cert.pem',
'--key',
'test/tls/key.pem']

with self.assertRaises(SystemExit) as test:
main(args)

self.assertTrue('Error loading SSL CA' in str(mock_print.call_args))
self.assertEqual(test.exception.code, 3)
21 changes: 21 additions & 0 deletions test/tls/ca-root.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDbTCCAlWgAwIBAgIUB6EZDl3ajJgJsoLzyC9DrOQQpKowDQYJKoZIhvcNAQEN
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0yNDAzMTgwODE5MDhaGA8yMDUx
MDgwMzA4MTkwOFowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBALVxioj+6zw6Snr+B1JOivC8Of6YptVYym5ICiHX
wjpbSVVe+Py/P2LDb/uQ1QkAENlpvChFqSaRBZU5keXYS/DaFb2Evb2/zf5qIdWU
2ju8B5V13gXSeaNNetyEn1Ivvk0lOCQo2RwEZXuStpLS4Q32rkRBvkoL+RXDc1NX
c3RwcU1p9ybgBqAC7FYdV82sgHGugIrbzkjfFREJXp1AnqvKAdk39b1CnPxfmPZC
nzPPetfr3iivH8yVO5rodU/LDtQNph22JR94YvPB89QO+bZ9bw2GHtPdAKFew9HF
UxM1fmy381Mq2iS3KUq5vsC1jMe8slUAIFYEDzoPvOz+MpcCAwEAAaNTMFEwHQYD
VR0OBBYEFOmCb+JnMzX29hwgtXSzrN+m6mTDMB8GA1UdIwQYMBaAFOmCb+JnMzX2
9hwgtXSzrN+m6mTDMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQENBQADggEB
AAkTD8K4UO4uO4i6p2BCofbhVm9LYA0ulmLO8Uro0U491TeEDOQpgMFAK+b2gZIU
zvDHoCMn3UPVxHKl7XzDgLZVkYYEc2s9vArxk5vSnFmh3XvlDu2SO5gSLB2sf68A
2+Jz2x6z9tjWWdZCGJWU/iwMbG2Y3JMHyv1NMF8cyOclJaSDNBAwF5c5sdlGTLKb
WHGXzVqHSAFlGcHtQrcEKclHiuzw2G3LZzwghGk0XzxwvyKrnAEy408RY0mfNLtz
32KHqYtrip0RYlGWKP7/7q6i0D8muEFW/I4emFI0z0I/1CcYZZS8tQkWaPf/wCN0
llTD1kKJACsIMaqkkyy+EZM=
-----END CERTIFICATE-----
Loading
Loading