From cb79012769d78094fb4b3bba5352550dc6e15350 Mon Sep 17 00:00:00 2001
From: "th.l" <thl-cmk@outlook.com>
Date: Mon, 1 Jan 2024 14:56:58 +0100
Subject: [PATCH] update project

---
 README.md                           |   1 +
 mkp/check_ntp-0.0.3-20230607.mkp    | Bin 0 -> 6283 bytes
 source/checks/check_ntp             |  64 ++++++
 source/gui/metrics/check_ntp.py     |  88 +++++++++
 source/gui/wato/check_ntp.py        | 184 +++++++++++++++++
 source/lib/nagios/plugins/check_ntp | 297 ++++++++++++++++++++++++++++
 source/packages/check_ntp           |  12 ++
 7 files changed, 646 insertions(+)
 create mode 100644 mkp/check_ntp-0.0.3-20230607.mkp
 create mode 100644 source/checks/check_ntp
 create mode 100644 source/gui/metrics/check_ntp.py
 create mode 100644 source/gui/wato/check_ntp.py
 create mode 100755 source/lib/nagios/plugins/check_ntp
 create mode 100644 source/packages/check_ntp

diff --git a/README.md b/README.md
index b85195d..bdbaba2 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+[PACKAGE]: ../../raw/master/mkp/check_ntp-0.0.3-20230607.mkp "check_ntp-0.0.3-20230607.mkp"
 # Active Check NTP
 
 This plugin is an active check to monitor NTP servers.
diff --git a/mkp/check_ntp-0.0.3-20230607.mkp b/mkp/check_ntp-0.0.3-20230607.mkp
new file mode 100644
index 0000000000000000000000000000000000000000..bc502ffec70c145a38158d4507026770eb3d1fd1
GIT binary patch
literal 6283
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

literal 0
HcmV?d00001

diff --git a/source/checks/check_ntp b/source/checks/check_ntp
new file mode 100644
index 0000000..620b902
--- /dev/null
+++ b/source/checks/check_ntp
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# License: GNU General Public License v2
+#
+#
+# Author: thl-cmk[at]outlook[dot]com
+# URL   : https://thl-cmk.hopto.org
+# Date  : 2022-10-04
+# File  : checks/active_checks_ntp
+#
+# 2022-11-15: changed to use short options, doesn't work with long options
+
+def check_ntp_arguments(params):
+    args = []
+
+    if 'port' in params:
+        args.append(f'-p {params["port"]}')
+
+    if 'timeout' in params:
+        args.append(f'-t {params["timeout"]}')
+
+    if 'server' in params:
+        args.append(f'-H {params["server"]}')
+    else:
+        args.append('-H $HOSTADDRESS$')
+
+    if 'version' in params:
+        args.append(f'-V {params["version"]}')
+
+    if 'offset_levels' in params:
+        args.append(f'-o {params["offset_levels"][0]},{params["offset_levels"][1]}')
+
+    if 'stratum_levels' in params:
+        args.append(f'-s {params["stratum_levels"][0]},{params["stratum_levels"][1]}')
+
+    if 'dispersion_levels' in params:
+        args.append(f'-D {params["dispersion_levels"][0]},{params["dispersion_levels"][1]}')
+
+    if 'delay_levels' in params:
+        args.append(f'-d {params["dispersion_levels"][0]},{params["dispersion_levels"][1]}')
+
+    if 'state_not_synchronized' in params:
+        args.append(f'-n {params["state_not_synchronized"]}')
+
+    if 'state_no_response' in params:
+        args.append(f'-r {params["state_no_response"]}')
+
+    return args
+
+
+def _check_description(params):
+    if 'description' in params:
+        return f'NTP server {params["description"]}'
+
+    return 'NTP server'
+
+
+active_check_info['ntp'] = {
+    'command_line': 'check_ntp $ARG1$',
+    'argument_function': check_ntp_arguments,
+    'service_description': _check_description,
+    'has_perfdata': True,
+}
diff --git a/source/gui/metrics/check_ntp.py b/source/gui/metrics/check_ntp.py
new file mode 100644
index 0000000..3456514
--- /dev/null
+++ b/source/gui/metrics/check_ntp.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# License: GNU General Public License v2
+#
+# Author: thl-cmk[at]outlook[dot]com
+# URL   : https://thl-cmk.hopto.org
+# Date  : 2022-10-06
+# File  : metrics/check_ntp.py
+#
+#
+
+from cmk.gui.i18n import _
+
+from cmk.gui.plugins.metrics.utils import (
+    metric_info,
+    graph_info,
+    perfometer_info
+)
+
+metric_info['ntp_offset'] = {
+    'title': _('Offset'),
+    'unit': 's',
+    'color': '#9a52bf',
+}
+
+
+metric_info['ntp_delay'] = {
+    'title': _('Delay'),
+    'help': _(''),
+    'unit': 's',
+    'color': '26/a',
+}
+
+metric_info['ntp_root_dispersion'] = {
+    'title': _('Root dispersion'),
+    'help': _(''),
+    'unit': 's',
+    'color': '32/a',
+}
+
+
+graph_info['check_ntp_offset'] = {
+    'title': _('Time offset'),
+    'metrics': [
+        ('ntp_offset', 'area'),
+    ],
+    'scalars': [
+        ('ntp_offset:crit', _('Upper critical level')),
+        ('ntp_offset:warn', _('Upper warning level')),
+        ('0,ntp_offset:warn,-', _('Lower warning level')),
+        ('0,ntp_offset:crit,-', _('Lower critical level')),
+    ],
+    'range': ('0,ntp_offset:crit,-', 'ntp_offset:crit'),
+}
+
+graph_info['check_ntp_delay'] = {
+    'title': _('Delay'),
+    'metrics': [
+        ('ntp_delay', 'area'),
+    ],
+    'scalars': [
+        ('ntp_delay:crit', _('Critical')),
+        ('ntp_delay:warn', _('Warning')),
+    ],
+    'range': (0, 'ntp_delay:max'),
+}
+
+
+graph_info['check_ntp_dispersion'] = {
+    'title': _('Root dispersion'),
+    'metrics': [
+        ('ntp_root_dispersion', 'area'),
+    ],
+    'scalars': [
+        ('ntp_root_dispersion:crit', _('Critical')),
+        ('ntp_root_dispersion:warn', _('Warning')),
+    ],
+    'range': (0, 'ntp_root_dispersion:max'),
+}
+
+
+perfometer_info.append({
+    'type': 'logarithmic',
+    'metric': 'ntp_offset',
+    'half_value': 1.0,
+    'exponent': 10.0,
+})
diff --git a/source/gui/wato/check_ntp.py b/source/gui/wato/check_ntp.py
new file mode 100644
index 0000000..451a293
--- /dev/null
+++ b/source/gui/wato/check_ntp.py
@@ -0,0 +1,184 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# License: GNU General Public License v2
+#
+#
+# Author: thl-cmk[at]outlook[dot]com
+# URL   : https://thl-cmk.hopto.org
+# Date  : 2022-10-04
+# File  : wato/active_checks_ntp.py
+#
+
+from cmk.gui.i18n import _
+from cmk.gui.valuespec import (
+    Dictionary,
+    Tuple,
+    Transform,
+    Integer,
+    TextAscii,
+    MonitoringState,
+)
+
+from cmk.gui.plugins.wato.utils import (
+    rulespec_registry,
+    HostRulespec,
+)
+
+from cmk.gui.plugins.wato.active_checks import (
+    RulespecGroupActiveChecks
+)
+
+
+def _valuespec_active_checks_ntp():
+    return Transform(
+        Dictionary(
+            title=_('Check NTP service'),
+            help=_(''),
+            elements=[
+                ('description',
+                 TextAscii(
+                     title=_('Service description'),
+                     help=_(
+                         'Must be unique for every host. The service description starts always with \"NTP server\".'),
+                     size=50,
+                 )),
+                ('server',
+                 TextAscii(
+                     title=_('Server IP-address or name'),
+                     help=_(
+                         'Hostname or IP-address to monitor. Default is the host name/IP-Address of the monitored host.'
+                     ),
+                     size=50,
+                 )),
+                ('port',
+                 Integer(
+                     title=_('NTP port'),
+                     help=_('UDP Port to use. Default is 123.'),
+                     # size=5,
+                     default_value=123,
+                     minvalue=1,
+                     maxvalue=65535,
+                 )),
+                ('version',
+                 Integer(
+                     title=_('NTP version'),
+                     help=_('NTP version for the request. Default is version 4.'),
+                     # size=1,
+                     default_value=4,
+                     minvalue=1,
+                     maxvalue=4,
+                 )),
+                ('timeout',
+                 Integer(
+                     title=_('Request timeout'),
+                     help=_('Timeoute for the request in seconds. Min: 1s, Max: 20, Default is 2 seconds.'),
+                     # size=3,
+                     default_value=2,
+                     minvalue=1,
+                     maxvalue=20,
+                 )),
+                ('state_not_synchronized',
+                 MonitoringState(
+                     title=_('Monitoring state if server is not synchronized'),
+                     help=_('Monitoring state if server is not synchronized. Default is warning.'),
+                     default_value=2,
+                 )),
+                ('state_no_response',
+                 MonitoringState(
+                     default_value=2,
+                     title=_('Monitoring state if server doesn\'t respond (timeout)'),
+                     help=_('Monitoring state if the server doesn\'t respond. Default is "CRIT"')
+                 )),
+                ('stratum_levels',
+                 Tuple(
+                     title=_('max. stratum'),
+                     elements=[
+                         Integer(
+                             title=_('Warning at'),
+                             default_value=10,
+                             maxvalue=255,
+                             minvalue=1,
+                             help=_(
+                                 'The stratum (\'distance\' to the reference clock) at which the check gets warning.'),
+                         ),
+                         Integer(
+                             title=_('Critical at'),
+                             default_value=15,
+                             maxvalue=18,
+                             help=_(
+                                 'The stratum (\'distance\' to the reference clock) at which the check gets critical.'),
+                         )
+                     ],
+                 )),
+                ('offset_levels',
+                 Tuple(
+                     title=_('max. offset in ms'),
+                     help=_('Mean offset in the times reported between this local host and the remote peer or server.'
+                            'Note: This levels will also be used as lower levels.'),
+                     elements=[
+                         Integer(
+                             title=_('Warning at'),
+                             unit='ms',
+                             default_value=200,
+                             help=_('The offset in ms at which a warning state is triggered. Default is 200ms'),
+                         ),
+                         Integer(
+                             title=_('Critical at'),
+                             unit='ms',
+                             default_value=500,
+                             help=_('The offset in ms at which a critical state is triggered. Default is 500ms'),
+                         )
+                     ],
+                 )),
+                ('delay_levels',
+                 Tuple(
+                     title=_('max. delay in ms'),
+                     help=_('Upper levels for delay in milly seconds.'),
+                     elements=[
+                         Integer(
+                             title=_('Warning at'),
+                             unit='ms',
+                             default_value=200,
+                             help=_('The delay in ms at which a warning state is triggered. Default is 200ms'),
+                         ),
+                         Integer(
+                             title=_('Critical at'),
+                             unit='ms',
+                             default_value=500,
+                             help=_('The delay in s at which a critical state is triggered. Default is 500ms'),
+                         )
+                     ],
+                 )),
+                ('dispersion_levels',
+                 Tuple(
+                     title=_('max. root dispersion in s'),
+                     help=_('Upper levels for (root) dispersion in seconds.'),
+                     elements=[
+                         Integer(
+                             title=_('Warning at'),
+                             unit='s',
+                             default_value=3,
+                             help=_('The dispersion in s at which a warning state is triggered. Default is 3s'),
+                         ),
+                         Integer(
+                             title=_('Critical at'),
+                             unit='s',
+                             default_value=5,
+                             help=_('The dispersion in s at which a critical state is triggered. Default is 5s'),
+                         )
+                     ],
+                 )),
+            ],
+        ),
+    )
+
+
+rulespec_registry.register(
+    HostRulespec(
+        group=RulespecGroupActiveChecks,
+        match_type='all',
+        name='active_checks:ntp',
+        valuespec=_valuespec_active_checks_ntp,
+    )
+)
diff --git a/source/lib/nagios/plugins/check_ntp b/source/lib/nagios/plugins/check_ntp
new file mode 100755
index 0000000..f270473
--- /dev/null
+++ b/source/lib/nagios/plugins/check_ntp
@@ -0,0 +1,297 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# License: GNU General Public License v2
+#
+#
+# Author: thl-cmk[at]outlook[dot]com
+# URL   : https://thl-cmk.hopto.org
+# Date  : 2022-10-04
+# File  : active_checks_ntp.py
+#
+# Active check to monitor NTP servers.
+#
+# 2022-10-13: added exception handling for ntp request
+# 2022-11-14: made state on no response configurable
+# 2022-11-15: added short options
+# 2023-06-07: moved gui files to ~/local/lib/chek_mk/gui/plugins/...
+
+from typing import Optional, Sequence, Tuple
+from ipaddress import IPv4Address
+import sys
+import argparse
+import socket
+from time import ctime
+
+import ntplib
+
+no_ntplib = False
+try:
+    from ntplib import NTPClient, NTPStats
+except ModuleNotFoundError:
+    no_ntplib = True
+
+_ntp_leap = {
+    0: 'no warning',
+    1: 'last minute of the day has 61 seconds',
+    2: 'last minute of the day has 59 seconds',
+    3: 'unknown(clock unsynchronized)',
+}
+
+_ntp_mode = {
+    0: 'reserved',
+    1: 'symmetric active',
+    2: 'symmetric passive',
+    3: 'client',
+    4: 'server',
+    5: 'broadcast',
+    6: 'NTP control message',
+    7: 'reserved for private use',
+}
+
+_ntp_ref_id = {
+    # from RFC5905
+    'GOES': 'Geosynchronous Orbit Environment Satellite',
+    'GPS': 'Global Position System',
+    'GAL': 'Galileo Positioning System',
+    'PPS': 'Generic pulse - per - second',
+    'IRIG': 'Inter - Range Instrumentation Group',
+    'WWVB': 'LF Radio WWVB Ft.Collins, CO 60 kHz',
+    'DCF': 'LF Radio DCF77 Mainflingen, DE 77.5 kHz',
+    'HBG': 'LF Radio HBG Prangins, HB 75 kHz',
+    'MSF': 'LF Radio MSF Anthorn, UK 60 kHz',
+    'JJY': 'LF Radio JJY Fukushima, JP 40 kHz, Saga, JP 60 kHz',
+    'LORC': 'MF Radio LORAN C station, 100 kHz',
+    'TDF': 'MF Radio Allouis, FR 162 kHz',
+    'CHU': 'HF Radio CHU Ottawa, Ontario',
+    'WWV': 'HF Radio WWV Ft.Collins, CO',
+    'WWVH': 'HF Radio WWVH Kauai, HI',
+    'NIST': 'NIST telephone modem',
+    'ACTS': 'NIST telephone modem',
+    'USNO': 'USNO telephone modem',
+    'PTB': 'European telephone modem',
+    # from meienberg
+    # 'PPS': '“Pulse Per Second” from a time standard',
+    # 'IRIG': 'Inter-Range Instrumentation Group time code',
+    # 'ACTS': 'American NIST time standard telephone modem',
+    # 'NIST': 'American NIST time standard telephone modem',
+    # 'PTB': 'German PTB time standard telephone modem',
+    # 'USNO': 'American USNO time standard telephone modem',
+    # 'CHU': 'CHU (HF, Ottawa, ON, Canada) time standard radio receiver',
+    'DCFa': 'DCF77 (LF, Mainflingen, Germany) time standard radio receiver',
+    # 'HBG': 'HBG (LF Prangins, Switzerland) time standard radio receiver',
+    # 'JJY': 'JJY (LF Fukushima, Japan) time standard radio receiver',
+    # 'LORC': 'LORAN-C station (MF) time standard radio receiver. Note, no longer operational (superseded by eLORAN)',
+    # 'MSF': 'MSF (LF, Anthorn, Great Britain) time standard radio receiver',
+    # 'TDF': 'TDF (MF, Allouis, France) time standard radio receiver',
+    # 'WWV': 'WWV (HF, Ft. Collins, CO, America) time standard radio receiver',
+    # 'WWVB': 'WWVB (LF, Ft. Collins, CO, America) time standard radio receiver',
+    # 'WWVH': 'WWVH (HF, Kauai, HI, America) time standard radio receiver',
+    # 'GOES': 'American Geosynchronous Orbit Environment Satellite',
+    # 'GPS': 'American GPS',
+    # 'GAL': 'Galileo European GNSS',
+    'ACST': 'manycast server',
+    'AUTO': 'Autokey sequence error',
+    'BCST': 'broadcast server',
+    'MCST': 'multicast server',
+}
+
+_ntp_refids_bad = {
+    'AUTH': 'authentication error',
+    'AUTO': 'Autokey sequence error',
+    'CRYPT': 'Autokey protocol error',
+    'DENY': 'Access denied by server',
+    'INIT': 'Association initialized',
+    'RATE': 'Polling rate exceeded',
+    'LOCL': 'This local host (a place marker at the lowest stratum included in case '
+            'there are no remote peers or servers available)',
+    'STEP': 'Step time change, the offset is less than the panic threshold (1000ms) '
+            'but greater than the step threshold (125ms).',
+    'TIME': 'Association timeout',
+    'XFAC': 'Association changed (IP address changed or lost)',
+}
+
+
+def _ntp_decode_ref_id(stratum: int, ref_id: int):
+    if 1 < stratum < 16:
+        return IPv4Address(ref_id)
+
+    elif stratum in [0, 1]:
+        _byte4 = ref_id % 256
+        _byte3 = (ref_id // 256) % 256
+        _byte2 = (ref_id // 256 // 256) % 256
+        _byte1 = (ref_id // 256 // 256 // 256)
+
+        ref_id = ''
+        for _byte in [_byte1, _byte2, _byte3, _byte4]:
+            if _byte > 31:
+                ref_id += chr(_byte)
+
+        return ref_id
+
+
+def parse_arguments(argv: Sequence[str]) -> argparse.Namespace:
+
+    def _warn_crit(arg: str) -> Optional[Tuple[int, int]]:
+        arg = arg.strip('(').strip(')').split(',')
+        warn, crit = arg
+        try:
+            arg = (int(warn), int(crit))
+        except ValueError as e:
+            raise argparse.ArgumentTypeError(e)
+
+        return arg
+
+    parser = argparse.ArgumentParser(
+        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+        epilog='Add WARN,CRIT levels separated by comma without brackets, like this: "--offset 200,500".'
+               'To use this check plugin you need to install the python "ntplib" in your CMK python environment.'
+    )
+    parser.add_argument(
+        '-H', '--host', required=True,
+        help='Host to query (required)')
+    parser.add_argument(
+        '-p', '--port', type=int, default=123,
+        help='UDP port to use.')
+    parser.add_argument(
+        '-t', '--timeout', type=int, default=2,
+        help='Request timeout in seconds.')
+    parser.add_argument(
+        '-V', '--version', type=int, default=4, choices=[1, 2, 3, 4],
+        help='NTP version to use.')
+    parser.add_argument(
+        '-n', '--state_not_synchronized', type=int, default=2, choices=[0, 1, 2, 3],
+        help='Monitoring state if not synchronized.')
+    parser.add_argument(
+        '-r', '--state_no_response', type=int, default=2, choices=[0, 1, 2, 3],
+        help='Monitoring state if response (timeout) received.')
+    parser.add_argument(
+        '-s', '--stratum', type=_warn_crit, default=(10, 15),
+        help='WARN,CRIT levels for stratum. Use values > 16 to disable.')
+    parser.add_argument(
+        '-o', '--offset', type=_warn_crit, default=(200, 500),
+        help='WARN,CRIT levels for offset in milliseconds.')
+    parser.add_argument(
+        '-d', '--delay', type=_warn_crit, default=(200, 500),
+        help='WARN,CRIT levels for delay in milliseconds.')
+    parser.add_argument(
+        '-D', '--dispersion', type=_warn_crit, default=(200, 500),
+        help='WARN,CRIT levels for dispersion in seconds.')
+
+    args = parser.parse_args(argv)
+    args.host = args.host.strip(' ')
+    return args
+
+
+def get_ntp_time(server: str, port: int, timeout: int, version: int, state_no_response: int):  # -> Optional[NTPStats]
+    # NTPStats is not available if ntplib is not installed
+    c = NTPClient()
+    try:
+        response = c.request(
+            host=server,
+            port=port,
+            timeout=timeout,
+            version=version
+        )
+    except (ntplib.NTPException, socket.gaierror) as e:
+        sys.stdout.write(f'{e}\n')
+        sys.exit(state_no_response)
+    return response
+
+
+def main(args=None):
+    if args is None:
+        args = sys.argv[1:]  # without the path/plugin it self
+
+    args = parse_arguments(args)
+
+    if no_ntplib:
+        sys.stdout.write(
+            f'To use this check plugin you need to install the python ntplib in your CMK python environment.'
+        )
+        sys.exit(3)
+
+    ntp_time = get_ntp_time(args.host, args.port, args.timeout, args.version, args.state_no_response)
+
+    server_time = ctime(ntp_time.tx_time)
+    stratum = int(ntp_time.stratum)
+
+    if stratum == 0:
+        info_text = f'Server not synchronized. Stratum: 0'
+        sys.stdout.write(info_text)
+        return args.state_not_synchronized
+
+    ref_id = _ntp_decode_ref_id(stratum, int(ntp_time.ref_id))
+
+    info_text = ''
+    long_output = ''
+    perfdata = ''
+    status = 0
+    # https://tutorial.eyehunts.com/python/python-strftime-function-milliseconds-examples/
+    # time_format = '%Y-%m-%d %H:%M:%S'
+
+    text = f'Stratum: {stratum}'
+    if stratum >= args.stratum[1]:
+        status = 2
+        text += '(!!)'
+    elif stratum >= args.stratum[0]:
+        status = max(status, 1)
+        text += '(!)'
+
+    info_text += f'{text}, Reference ID: {ref_id}, Time: {server_time}'
+    long_output += f'{text}\n'
+
+    long_output += f'Ref-ID: {ref_id}, {_ntp_ref_id.get(ref_id, "")}\n'
+    long_output += f'Time: {server_time}\n'
+    long_output += f'Mode: {_ntp_mode.get(ntp_time.mode, f"unknown: {ntp_time.mode}")}\n'
+    long_output += f'Version: {ntp_time.version}\n'
+    long_output += f'Poll: {ntp_time.poll}\n'
+    long_output += f'Precision: {ntp_time.precision}\n'
+    long_output += f'Leap: {_ntp_leap.get(ntp_time.leap, f"unknown {ntp_time.leap}")}\n'
+
+    long_output += '\nPerfdata\n'
+    for value, warn, crit, label, metric, unit in [
+        (ntp_time.offset, args.offset[0] / 1000, args.offset[1] / 1000, 'Offset', 'ntp_offset', 's'),
+        (ntp_time.delay, args.delay[0] / 1000, args.delay[1] / 1000, 'Delay', 'ntp_delay', 's'),
+        (ntp_time.root_dispersion, args.dispersion[0], args.dispersion[1], 'Root dispersion', 'ntp_root_dispersion', 's')
+    ]:
+        perfdata += f'{metric}={value};{warn};{crit}; '
+        text = f'{label}: {value:.4f} {unit}'
+        if (crit * - 1) > value or value >= crit:  # use crit as lower and upper level
+            status = 2
+            info_text += f', {text}(!!)'
+            long_output += f'{text}(!!)\n'
+        elif (warn * -1) > value or value >= warn:  # use warn as lower and upper level
+            status = max(status, 1)
+            info_text += f', {text}(!)'
+            long_output += f'{text}(!)\n'
+        else:
+            long_output += f'{text}\n'
+
+    long_output += '\nTimestamps:\n'
+    long_output += f'Reference Timestamp (ref): {ntp_time.ref_timestamp}\n'
+    long_output += f'Origin Timestamp (org): {ntp_time.orig_timestamp}\n'
+    long_output += f'Receive Timestamp (rec): {ntp_time.recv_timestamp}\n'
+    long_output += f'Transmit Timestamp (xmt): {ntp_time.tx_timestamp}\n'
+    long_output += f'Destination Timestamp (dst): {ntp_time.dest_timestamp}\n'
+
+    long_output += '\nTimes\n'
+    for label, value in [
+        ('Reference', ntp_time.ref_time),
+        ('Origin', ntp_time.orig_time),
+        ('Receive', ntp_time.recv_time),
+        ('Transmit', ntp_time.tx_time),
+        ('Destination', ntp_time.dest_time),
+    ]:
+        # long_output += f' {label} time: {strftime(time_format,gmtime(value))}\n'
+        long_output += f' {label} time: {value}\n'
+
+    info_text = info_text.strip(',').strip(' ')
+    sys.stdout.write(f'{info_text}\n{long_output} | {perfdata}\n')
+
+    return status
+
+
+if __name__ == '__main__':
+    exitcode = main()
+    sys.exit(exitcode)
diff --git a/source/packages/check_ntp b/source/packages/check_ntp
new file mode 100644
index 0000000..a19b5ff
--- /dev/null
+++ b/source/packages/check_ntp
@@ -0,0 +1,12 @@
+{'author': 'Th.L. (thl-cmk[at]outlook[dot]com)',
+ 'description': 'Active check to monitor NTP servers\n',
+ 'download_url': 'https://thl-cmk.hopto.org',
+ 'files': {'checks': ['check_ntp'],
+           'gui': ['metrics/check_ntp.py', 'wato/check_ntp.py'],
+           'lib': ['nagios/plugins/check_ntp']},
+ 'name': 'check_ntp',
+ 'title': 'Active check NTP',
+ 'version': '0.0.3-20230607',
+ 'version.min_required': '2.1.0b1',
+ 'version.packaged': '2.2.0p14',
+ 'version.usable_until': '2.2.0b1'}
-- 
GitLab