diff --git a/charmhelpers/contrib/charmsupport/nrpe.py b/charmhelpers/contrib/charmsupport/nrpe.py index e4cb06bcd..c78634767 100644 --- a/charmhelpers/contrib/charmsupport/nrpe.py +++ b/charmhelpers/contrib/charmsupport/nrpe.py @@ -29,6 +29,7 @@ import yaml from charmhelpers.core.hookenv import ( + DEBUG, config, hook_name, local_unit, @@ -509,6 +510,39 @@ def add_haproxy_checks(nrpe, unit_name): check_cmd='check_haproxy_queue_depth.sh') +def add_openvswitch_checks(nrpe, unit_name): + """ + Add checks for openvswitch + + :param NRPE nrpe: NRPE object to add check to + :param str unit_name: Unit name to use in check description + """ + enable_sudo_for_openvswitch_checks() + nrpe.add_check( + shortname='openvswitch', + description='Check Open vSwitch {{{0}}}'.format(unit_name), + check_cmd='check_openvswitch.py') + + +def enable_sudo_for_openvswitch_checks(): + sudoers_dir = "/etc/sudoers.d" + sudoers_mode = 0o100440 + ovs_sudoers_file = "99-check_openvswitch" + ovs_sudoers_entry = ("# Juju owned - do not edit #\n" + "nagios ALL=(root) NOPASSWD: /usr/bin/ovs-vsctl " + "--format=json list Interface\n") + dest = os.path.join(sudoers_dir, ovs_sudoers_file) + try: + with open(dest, "w") as sudoer_file: + sudoer_file.write(ovs_sudoers_entry) + os.chmod(dest, sudoers_mode) + os.chown(dest, uid=0, gid=0) + except (OSError, IOError) as e: + log("Failed to setup sudoers file for check_openvswitch: {}".format(e)) + else: + log("Sudoers file for check_openvswitch installed: {}".format(dest), DEBUG) + + def remove_deprecated_check(nrpe, deprecated_services): """ Remove checks fro deprecated services in list diff --git a/charmhelpers/contrib/openstack/files/check_openvswitch.py b/charmhelpers/contrib/openstack/files/check_openvswitch.py new file mode 100755 index 000000000..de45557b0 --- /dev/null +++ b/charmhelpers/contrib/openstack/files/check_openvswitch.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Check for known error statuses in OVS. + +This script currently checks through the Interface table in OVSDB +for errors. The script is named generically to allow for expanded +status checks in the future. +""" + +import argparse +import json +import subprocess +import sys + +# This depends on nagios_plugin3 being installed by charm-nrpe +from nagios_plugin3 import CriticalError, UnknownError, try_check + + +def parse_ovs_interface_errors(): + """Check for errors in OVSDB Interface table.""" + try: + cmd = [ + "/usr/bin/sudo", + "/usr/bin/ovs-vsctl", + "--format=json", + "list", + "Interface", + ] + ovs_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + raise UnknownError( + "UNKNOWN: OVS command '{}' failed. Output: {}".format( + " ".join(cmd), e.output.decode(errors="ignore").rstrip(), + ) + ) + ovs_interfaces = json.loads(ovs_output.decode(errors="ignore")) + ovs_interface_errors = [] + for i in ovs_interfaces["data"]: + # OVSDB internal data is formatted per RFC 7047 5.1 + iface = dict(zip(ovs_interfaces["headings"], i)) + error = iface["error"] + if isinstance(error, list) and len(error) == 2 and error[0] == "set": + # deserialize the set data into csv string elements + error = ",".join(error[1]) + if error: + ovs_interface_errors.append( + "Error on iface {}: {}".format(iface["name"], error) + ) + + if ovs_interface_errors: + raise CriticalError( + "CRITICAL: Found {} interface error(s) in OVSDB: " + "{}".format(len(ovs_interface_errors), ", ".join(ovs_interface_errors)) + ) + + print( + "OK: No errors found across {} interfaces in openvswitch".format( + len(ovs_interfaces["data"]) + ) + ) + + +def parse_args(argv=None): + """Process CLI arguments.""" + parser = argparse.ArgumentParser( + prog="check_openvswitch", + description=( + "this program checks openvswitch interface status and outputs " + "an appropriate Nagios status line" + ), + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + return parser.parse_args(argv) + + +def main(argv): + """Define main subroutine.""" + parse_args(argv) + try_check(parse_ovs_interface_errors) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/tests/contrib/charmsupport/test_nrpe.py b/tests/contrib/charmsupport/test_nrpe.py index 334cf18a2..40d18334a 100644 --- a/tests/contrib/charmsupport/test_nrpe.py +++ b/tests/contrib/charmsupport/test_nrpe.py @@ -494,3 +494,48 @@ def test_copy_nrpe_checks_nrpe_files_dir(self): self.patched['copy2'].assert_called_once_with( 'filea', '/usr/local/lib/nagios/plugins/filea') + + def test_enable_sudo_for_openvswitch_checks(self): + error_msg = 'test failure' + nrpe.enable_sudo_for_openvswitch_checks() + + self.patched['open'].side_effect = IOError(error_msg) + nrpe.enable_sudo_for_openvswitch_checks() + self.patched['open'].side_effect = None + + self.patched['chmod'].side_effect = OSError(error_msg) + nrpe.enable_sudo_for_openvswitch_checks() + self.patched['chmod'].side_effect = None + + self.patched['chown'].side_effect = OSError(error_msg) + nrpe.enable_sudo_for_openvswitch_checks() + self.patched['chown'].side_effect = None + + expected_path = '/etc/sudoers.d/99-check_openvswitch' + expected_success_log = ("Sudoers file for check_openvswitch " + "installed: {}".format(expected_path)) + expected_failure_log = ("Failed to setup sudoers file for " + "check_openvswitch: {}".format(error_msg)) + self.patched['open'].assert_has_calls( + [call(expected_path, 'w')], any_order=True) + self.patched['chmod'].assert_has_calls( + [call(expected_path, 0o100440)], any_order=True) + self.patched['chown'].assert_has_calls( + [call(expected_path, uid=0, gid=0)], any_order=True) + self.patched['log'].assert_has_calls( + [call(expected_success_log, nrpe.DEBUG), + call(expected_failure_log), + call(expected_failure_log), + call(expected_failure_log)]) + self.check_call_counts(log=4, open=4, chmod=3, chown=2) + + @patch.object(nrpe, 'enable_sudo_for_openvswitch_checks') + @patch.object(nrpe.NRPE, 'add_check') + def test_add_openvswitch_checks(self, mock_nrpe_add_check, mock_enable_sudo): + foo = nrpe.NRPE() + nrpe.add_openvswitch_checks(foo, 'testunit') + mock_enable_sudo.assert_called_once() + mock_nrpe_add_check.assert_called_once_with( + shortname='openvswitch', + description='Check Open vSwitch {testunit}', + check_cmd='check_openvswitch.py')