#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# |             ____ _               _        __  __ _  __           |
# |            / ___| |__   ___  ___| | __   |  \/  | |/ /           |
# |           | |   | '_ \ / _ \/ __| |/ /   | |\/| | ' /            |
# |           | |___| | | |  __/ (__|   <    | |  | | . \            |
# |            \____|_| |_|\___|\___|_|\_\___|_|  |_|_|\_\           |
# |                                                                  |
# | Copyright Mathias Kettner 2010             mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk is free software;  you can redistribute it and/or modify it
# under the  terms of the  GNU General Public License  as published by
# the Free Software Foundation in version 2.  check_mk is  distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY;  with-
# out even the implied warranty of  MERCHANTABILITY  or  FITNESS FOR A
# PARTICULAR PURPOSE. See the  GNU General Public License for more de-
# ails.  You should have  received  a copy of the  GNU  General Public
# License along with GNU Make; see the file  COPYING.  If  not,  write
# to the Free Software Foundation, Inc., 51 Franklin St,  Fifth Floor,
# Boston, MA 02110-1301 USA.

ntp_default_levels = (10, 20.0, 50.0) # stratum, ms offset

# Example output from agent:
# <<<ntp>>>
# - 42.202.61.100   .INIT.          16 u    - 1024    0    0.000    0.000   0.000
# * 42.202.62.100   .PPS.            1 u  143  256  377    0.763   -1.424   0.404
# % 42.202.61.14    42.202.62.100    2 u  160  256  357    0.058   -1.532   1.181
# % 42.202.62.14    42.202.62.100    2 u  194  256  357    0.131   -1.364   0.598
# % 42.202.61.10    .INIT.          16 u    - 1024    0    0.000    0.000   0.000
# % 42.202.62.10    .INIT.          16 u    - 1024    0    0.000    0.000   0.000
# + 42.202.61.15    42.202.62.100    2 u  196  256  356    0.058    0.574   0.204
# + 42.202.62.15    42.202.62.100    2 u  186  256  276    0.088    0.716   0.165
# % 127.127.1.0     .LOCL.          10 l   40   64  377    0.000    0.000   0.001

ntp_state_codes = {
  'x' : "falsetick",
  '.' : "excess",
  '-' : "outlyer",
  '+' : "candidat",
  '#' : "selected",
  '*' : "sys.peer",
  'o' : "pps.peer",
  '%' : "discarded",
}


# possible values: "summary", "detailed", "both", None
ntp_inventory_mode = "summary"

# We monitor all servers we have reached at least once
def inventory_ntp(info):
    if ntp_inventory_mode not in [ "detailed", "both" ]:
        return []
    inventory = []
    for statecode, peer, refid, stratum, t, when, poll, reach, delay, offset, jitter in info:
        if reach != "0" and refid != '.LOCL.':
            inventory.append((peer, "ntp_default_levels"))
    return inventory

def inventory_ntp_summary(info):
    if ntp_inventory_mode not in [ "summary", "both" ]:
        return []
    if len(info) > 0:
        return [(None, "ntp_default_levels")]

def ntp_fmt_time(t):
    if t == '-':
        return 0
    elif t[-1] == "m":
        return int(t[:-1]) * 60
    elif t[-1] == "h":
        return int(t[:-1]) * 60 * 60
    elif t[-1] == "d":
        return int(t[:-1]) * 60 * 60 * 24
    else:
        return int(t)

# Helper function that parses the information about one
# peer and returns a tuple of
# 0. Nagios state (0, 1, 2 or 3)
# 1. Nagios check output text
# 2. The offset (only if state != 3)
# 3. The jitter (only if state != 3)
def check_ntp_server_state(line, params):
    statecode, peer, refid, stratum, t, when, poll, reach, delay, offset, jitter = line
    if reach == "0":
        return (3, "peer is unreachable")

    offset  = float(offset)
    jitter  = float(jitter)

    when    = ntp_fmt_time(when)
    poll    = ntp_fmt_time(poll)
    stratum = int(stratum)
    state = ntp_state_codes.get(statecode, "unknown")
    infotext = "%s - stratum %d, offset %.2f ms, jitter %.2f ms" % (state, stratum, offset, jitter)
    if when > 0:
        infotext += ", last reached %d secs ago" % when

    maxstratum, warn, crit = params
    if abs(offset) >= crit:
        return (2, "critical offset " + infotext, offset, jitter)
    elif state in [ "falsetick" ]:
        return (2, infotext, offset, jitter)
    elif stratum >= maxstratum:
        return (2, infotext + (", stratum is too high (max allowed is %d)" % (maxstratum - 1)))
    elif when > 2*poll and when > 128: # when can exceed poll for small polling values
        return (3, "response due since %d secs%s" % (when - poll, infotext), offset, jitter)

    elif abs(offset) >= warn:
        return (1, "offset too high" + infotext, offset, jitter)
    else:
        return (0, infotext, offset, jitter)

def check_ntp(item, params, info):
    for line in info:
        if line[1] == item:
            state = check_ntp_server_state(line, params)
            if len(state) == 4:
                state, text, offset, jitter = state
                maxstratum, warn, crit = params
                perfdata = [ ( "offset",  offset, warn, crit, 0, None ),
                             ( "jitter",  jitter, warn, crit, 0, None ) ]
            else:
                state, text = state
                perfdata = []
            return (state, nagios_state_names[state] + " - " + text, perfdata)

    return (3, "UNKNOWN - peer not found")

def check_ntp_summary(item, params, info):
    # No information at all? NTP daemon not running or timeout in ntpq -p
    if len(info) == 0:
        return (3, "UNKNOWN - no information from NTP: timeout in ntpq -p or NTP daemon not running")

    # We only are interested in our system peer or pulse per second source (pps)
    for line in info:
        if line[0] in [ "*", "o" ]:
            state, text, perfdata = check_ntp(line[1], params, [line])
            text += " (synchronized on %s)" % line[1]
            return (state, text, perfdata)
    return (2, "CRIT - found %d peers, but none is suitable" % len(info))

check_info['ntp']       = (check_ntp,         "NTP Peer %s", 1, inventory_ntp)
check_info['ntp.time']  = (check_ntp_summary, "NTP Time",    1, inventory_ntp_summary)
