#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Univention Nagios
#
# Like what you see? Join us!
# https://www.univention.com/about-us/careers/vacancies/
#
# Copyright 2004-2024 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.

import getopt
import os
import pickle  # noqa: S403
import subprocess
import sys


class ReplicationCheck:

    def __init__(self):
        self.PROGNAME = 'check_univention_replication'
        self.REVISION = '1.0'
        self.FAILED_LDIF_FN = '/var/lib/univention-directory-replication/failed.ldif'
        self.CACHEFN = '/var/lib/univention-nagios/check_univention_replication.cache'
        self.PROG_GETNOTIFIERID = ['/usr/share/univention-directory-listener/get_notifier_id.py']
        self.PROG_GETLISTENERID = ['cat', '/var/lib/univention-directory-listener/notifier_id']
        self.diff_warning = 0
        self.diff_critical = 0
        self.history_size = 0
        self.verbose = 0

        self.STATE = {
            'OK': 0,
            'WARNING': 1,
            'CRITICAL': 2,
            'UNKNOWN': 3,
        }
        self.history = []

    def print_revision(self):
        print('%s: version %s' % (self.PROGNAME, self.REVISION))

    def print_usage(self):
        print('Usage: %s [-v] [-w <cnt>] [-c <cnt>] [-n <cnt>]' % self.PROGNAME)
        print('Usage: %s --help' % self.PROGNAME)
        print('Usage: %s --version' % self.PROGNAME)

    def print_help(self):
        self.print_revision()
        print('')
        self.print_usage()
        print('')
        print(' -v        verbose debug output')
        print(' -w <cnt>  WARNING if difference of transaction IDs is >= <cnt>')
        print(' -c <cnt>  CRITICAL if difference of transaction IDs is >= <cnt>')
        print(' -n <cnt>  CRITICAL if no change of transaction ID over last <cnt> checks')

    def load_history(self):
        if os.path.exists(self.CACHEFN) and os.stat(self.CACHEFN)[6] > 0:
            try:
                with open(self.CACHEFN, "rb") as fd:
                    cnt = pickle.load(fd)  # noqa: S301
                    for _i in range(cnt):
                        nid = pickle.load(fd)  # noqa: S301
                        lid = pickle.load(fd)  # noqa: S301
                        self.history.append((nid, lid))

                if self.verbose > 1:
                    print('History loaded:', self.history)
            except EOFError as err:
                if self.verbose > 1:
                    print('Loading history failed!  EOF error: %s' % err)
            except OSError as err:
                if self.verbose > 1:
                    print('Loading history failed!  OS error: %s' % err)
        else:
            if self.verbose > 1:
                print('History not loaded!')

    def save_history(self):
        try:
            with open(self.CACHEFN, 'wb') as f:
                pickle.dump(len(self.history), f)
                for nid, lid in self.history:
                    pickle.dump(nid, f)
                    pickle.dump(lid, f)
        except EOFError as err:
            self.exit_with_status_msg('UNKNOWN', 'error while reading %s, EOF error: %s' % (self.CACHEFN, err))
        except OSError as err:
            self.exit_with_status_msg('UNKNOWN', 'error while reading %s, OS error: %s' % (self.CACHEFN, err))
        if self.verbose > 1:
            print('History saved:', self.history)

    def exit_with_status(self, state, msg, nid, lid):
        print('%s: %s (nid=%s lid=%s)' % (state, msg, nid, lid))
        sys.exit(self.STATE[state])

    def exit_with_status_msg(self, state, msg):
        print('%s: %s' % (state, msg))
        sys.exit(self.STATE[state])

    def main(self):
        # parse command line
        try:
            (opts, pargs) = getopt.getopt(sys.argv[1:], 'c:hn:vw:', ['help', 'version'])
        except getopt.GetoptError:
            self.print_usage()
            sys.exit(self.STATE['UNKNOWN'])

        # get command line data
        for opt in opts:
            if opt[0] == '-c':
                self.diff_critical = int(opt[1])
                if self.diff_critical < 0:
                    self.diff_critical = 0
            elif opt[0] == '-h' or opt[0] == '--help':
                self.print_help()
                sys.exit(self.STATE['UNKNOWN'])
            elif opt[0] == '-n':
                self.history_size = int(opt[1])
                if self.history_size < 1:
                    self.history_size = 1
            elif opt[0] == '-v':
                self.verbose += 1
            elif opt[0] == '-w':
                self.diff_warning = int(opt[1])
                if self.diff_warning < 0:
                    self.diff_warning = 0
            elif opt[0] == '--version':
                self.print_revision()
                sys.exit(self.STATE['UNKNOWN'])

        self.load_history()

        # get actual transaction id
        try:
            result = subprocess.check_output(self.PROG_GETNOTIFIERID)
            notifier_id = result.decode("utf-8", "replace").rstrip()
        except subprocess.CalledProcessError:
            self.exit_with_status_msg('CRITICAL', '%s failed' % ' '.join(self.PROG_GETNOTIFIERID))

        try:
            result = subprocess.check_output(self.PROG_GETLISTENERID)
            listener_id = result.decode("utf-8", "replace").rstrip()
        except subprocess.CalledProcessError:
            self.exit_with_status_msg('CRITICAL', '%s failed' % " ".join(self.PROG_GETLISTENERID))

        # DEBUG CODE
        if len(pargs) == 2:
            notifier_id = int(pargs[0])
            listener_id = int(pargs[1])

        # add values to history
        self.history.insert(0, (notifier_id, listener_id))
        if len(self.history) > self.history_size:
            del self.history[self.history_size:]

        self.save_history()

        # CRITICAL if failed.ldif exists
        if os.path.exists(self.FAILED_LDIF_FN):
            self.exit_with_status('CRITICAL', 'failed.ldif exists', notifier_id, listener_id)

        if notifier_id == listener_id:
            self.exit_with_status('OK', 'replication complete', notifier_id, listener_id)
        else:
            # check if listener id changed during last checks
            change = 0
            beenequal = 0
            for (nid, lid) in self.history:
                if nid == lid:
                    beenequal = 1
                if listener_id != lid:
                    change = 1
            if change == 0 and beenequal == 0 and len(self.history) == self.history_size:
                self.exit_with_status('CRITICAL', 'no change of listener transaction id for last %s checks' % len(self.history), notifier_id, listener_id)

            # check if difference of nid and lid has been larger than given values
            mindiff = None
            for (nid, lid) in self.history:
                diff = int(nid) - int(lid)
                if not mindiff:
                    mindiff = diff
                else:
                    if mindiff > diff:
                        mindiff = diff

            if self.verbose:
                print('minimal difference:', mindiff)

            if self.diff_critical and self.diff_critical <= mindiff:
                self.exit_with_status('CRITICAL', 'difference was >= %s over last %s checks' % (self.diff_critical, len(self.history)), notifier_id, listener_id)
            if self.diff_warning and self.diff_warning <= mindiff:
                self.exit_with_status('WARNING', 'difference was >= %s over last %s checks' % (self.diff_warning, len(self.history)), notifier_id, listener_id)

        self.exit_with_status('OK', 'replication is in process', notifier_id, listener_id)


obj = ReplicationCheck()
obj.main()
