#!/usr/lib64/linuxfabrik-monitoring-plugins/venv/bin/python
# -*- coding: utf-8; py-indent-offset: 4 -*-
#
# Author:  Linuxfabrik GmbH, Zurich, Switzerland
# Contact: info (at) linuxfabrik (dot) ch
#          https://www.linuxfabrik.ch/
# License: The Unlicense, see LICENSE file.

# https://github.com/Linuxfabrik/monitoring-plugins/blob/main/CONTRIBUTING.md

"""See the check's README for more details."""

import argparse
import json
import re
import socket
import sys

import lib.args
import lib.base
import lib.disk
import lib.human
import lib.lftest
import lib.time
import lib.txt
from lib.globals import STATE_OK, STATE_UNKNOWN, STATE_WARN

try:
    import vici
except ImportError:
    print('Python module "vici" is not installed.')
    sys.exit(STATE_UNKNOWN)


__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
__version__ = '2026081401'

DESCRIPTION = """Checks IPSec connection states on a strongSwan VPN gateway. Connects to the charon
daemon via the VICI interface to retrieve IKE SA and CHILD SA states. Alerts on
connections that are not in the expected established state. Connection names can be
filtered out with --ignore, which is useful for gateways that mix permanent site-to-site
peers with transient remote-access clients where only the site-to-site peers should
drive the alert. Supports extended reporting via --lengthy.
Requires root or sudo."""

DEFAULT_LENGTHY = False
DEFAULT_SOCKET = '/run/strongswan/charon.vici'

# IKE SA algorithm keys that VICI only sends once the SA has a
# negotiated proposal, or that depend on the cipher suite in use.
OPTIONAL_ALG_KEYS = ('dh-group', 'encr-alg', 'encr-keysize', 'integ-alg', 'prf-alg')


def parse_args():
    """Parse command line arguments using argparse."""
    parser = argparse.ArgumentParser(
        description=DESCRIPTION,
        epilog=lib.args.epilog(__file__),
        formatter_class=lib.args.HelpFormatter,
    )

    parser.add_argument(
        '-V',
        '--version',
        action='version',
        version=f'%(prog)s: v{__version__} by {__author__}',
    )

    parser.add_argument(
        '--always-ok',
        help=lib.args.help('--always-ok'),
        dest='ALWAYS_OK',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--ignore',
        help='Ignore connections whose VICI key matches this Python regular '
        'expression. Case-sensitive by default; use `(?i)` for case-insensitive '
        'matching. Can be specified multiple times. Example: `--ignore="^RA_"` '
        'to skip transient remote-access clients on a VPN gateway that also '
        'carries permanent site-to-site peers. Example: `--ignore="(?i)test"` '
        '(case-insensitive) to skip any connection with "test" in its name. '
        'Default: %(default)s',
        dest='IGNORE',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--lengthy',
        help=lib.args.help('--lengthy'),
        dest='LENGTHY',
        action='store_true',
        default=DEFAULT_LENGTHY,
    )

    parser.add_argument(
        '--match',
        help='Only check connections whose VICI key matches this Python regular '
        'expression. Case-sensitive by default; use `(?i)` for case-insensitive '
        'matching. Can be specified multiple times. '
        + lib.args.MATCH_IGNORE_PRECEDENCE
        + ' Example: `--match="^S2S_SITE-XY$"` to pin an Icinga service to one '
        'specific site-to-site peer. Example: `--match="(?i)^s2s_"` '
        '(case-insensitive) to check every site-to-site peer on a gateway. '
        'Default: %(default)s',
        dest='MATCH',
        action='append',
        default=None,
    )

    parser.add_argument(
        '--no-perfdata',
        help=lib.args.help('--no-perfdata'),
        dest='NO_PERFDATA',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--socket',
        help='Path to the Versatile IKE Control Interface (VICI) socket. '
        'Default: %(default)s',
        dest='SOCKET',
        default=DEFAULT_SOCKET,
    )

    parser.add_argument(
        '--test',
        help=lib.args.help('--test'),
        dest='TEST',
        type=lib.args.csv,
    )

    args, _ = parser.parse_known_args()
    return args


def _collect_keys(entries):
    """Return the sorted list of unique dict keys from a sequence of
    single-key dicts. VICI's `list_conns()` and `list_sas()` both
    yield one-key-per-dict entries, which makes them interchangeable
    for a simple key collection. The production path passes the
    VICI generator directly; the test path passes a pre-loaded
    list from the JSON fixture.

    The keys have to be deduplicated: VICI keys each IKE SA by the
    connection name it uses, which is not unique, so `list_sas()` can
    yield the same key more than once (see `_primary_uniqueids()`).
    `list_conns()` keys are unique, so without the dedup the
    "configured but not active" comparison of the two lists reports a
    spurious warning for every connection that has more than one SA.
    """
    keys = set()
    for entry in entries:
        keys.update(entry.keys())
    return sorted(keys)


def _sa_rank(details):
    """Rank an IKE SA so that the primary one of several SAs sharing a
    connection name can be picked. An established SA beats one in any
    other state, and the most recent SA (the highest `uniqueid`) wins
    if that does not decide it.
    """
    state = lib.txt.to_text(details.get('state', ''))
    try:
        uniqueid = int(lib.txt.to_text(details.get('uniqueid', 0)))
    except ValueError:
        uniqueid = 0
    return (state == 'ESTABLISHED', uniqueid)


def _primary_uniqueids(list_sas, keyset):
    """Return a dict mapping a connection name to the `uniqueid` of the
    SA whose performance data is reported for it.

    VICI keys each IKE SA by the connection name the SA uses, and that
    name is not unique: while an IKE SA is being rekeyed the old and the
    new SA are both reported under it, and a gateway on which several
    peers share one connection name reports one SA per peer. Since the
    performance data labels are built from that name, reporting every SA
    would emit the same label several times in one line. Report the
    primary SA per name instead. The table still lists every SA.
    """
    primary = {}
    for sas in list_sas:
        for key, details in sas.items():
            if key not in keyset:
                continue
            rank = _sa_rank(details)
            if key not in primary or rank > primary[key]:
                primary[key] = rank
    return {key: rank[1] for key, rank in primary.items()}


def keep_connection(name, match_patterns, ignore_patterns):
    """Return True if `name` should be kept by the --match / --ignore
    filter pair, False if it should be dropped. Include first, then
    exclude: a name passes if it matches any `match_patterns` entry
    (or if `match_patterns` is empty) AND does not match any
    `ignore_patterns` entry. Same semantics as the disk-usage plugin
    and the lib.args canonical `--match` / `--ignore` convention.
    """
    if match_patterns and not any(p.search(name) for p in match_patterns):
        return False
    return not any(p.search(name) for p in ignore_patterns)


def format_sas_data(sas):
    """Re-format SAS connection details for a single connection.

    VICI returns all scalars as `bytes` at runtime, while the JSON
    fixtures used by the unit tests hold already-decoded `str`
    values. Both shapes are accepted: `bytes` are decoded via
    `lib.txt.to_text()`, `bytearray` is joined and decoded, and
    anything else (including `str`) is passed through unchanged.
    Decoding uses the Latin-1 fallback so a non-UTF-8 identity
    (for example a certificate DN) does not crash the output.

    Several keys are optional: VICI omits them instead of sending
    them empty, so none of them can be accessed directly.

    * The algorithm keys are only sent once a proposal has been
      negotiated, which happens during IKE_SA_INIT. A connecting SA
      may therefore carry none of them yet.
    * `integ-alg` is missing for an AEAD cipher such as AES_GCM,
      which carries no separate integrity transform (#806).
    * `encr-keysize` is missing for a fixed-key cipher such as 3DES.
    * `established`, `rekey-time` and `reauth-time` are only sent for
      an established SA, so all three are missing not only while the
      SA is connecting, but also while it is rekeying or being
      deleted.
    * `rekey-time` and `reauth-time` are two different things, and are
      sent independently of each other: an SA is renewed by rekeying,
      by re-authentication, or by both, depending on the peer config.

    `established`, `reauth-time` and `rekey-time` come back as `None`
    when VICI omits them, so that the caller can skip their performance
    data instead of emitting a non-numeric value.
    """
    data = {}
    for key, value in sas.items():
        if isinstance(value, bytes):
            value = lib.txt.to_text(value, errors='strict_or_latin1')
        elif isinstance(value, bytearray):
            value = lib.txt.to_text(b', '.join(value), errors='strict_or_latin1')
        if key in OPTIONAL_ALG_KEYS and not value:
            # VICI omits an algorithm key rather than sending it
            # empty. Drop an empty one anyway, so that both shapes
            # take the same "not negotiated" path below.
            continue
        data[key] = value

    # Join the key size to the algorithm only if VICI sent one,
    # otherwise a fixed-key cipher renders a dangling dash ("3DES-").
    encr = data.get('encr-alg', 'None')
    if data.get('encr-keysize'):
        encr = f'{encr}-{data["encr-keysize"]}'
    data['encr'] = (
        f'{encr}'
        f'/{data.get("integ-alg", "None")}'
        f'/{data.get("prf-alg", "None")}'
        f'/{data.get("dh-group", "None")}'
    )

    # `established` counts up from the moment the SA was established,
    # while `reauth-time` and `rekey-time` count down to a deadline.
    data['established'] = _optional_int(data, 'established')
    if data['established'] is None:
        data['established-hr'] = 'n/a'
    else:
        data['established-hr'] = lib.time.epoch2iso(
            lib.time.now(as_type='epoch') - data['established']
        )
    if data['local-id'] != data['local-host']:
        data['local'] = (
            f'{data["local-host"]}:{data["local-port"]} ("{data["local-id"]}")'
        )
    else:
        data['local'] = f'{data["local-host"]}:{data["local-port"]}'
    data['reauth-time'] = _optional_int(data, 'reauth-time')
    if data['reauth-time'] is None:
        data['reauth-time-hr'] = 'n/a'
    else:
        data['reauth-time-hr'] = lib.time.epoch2iso(
            lib.time.now(as_type='epoch') + data['reauth-time']
        )
    data['rekey-time'] = _optional_int(data, 'rekey-time')
    if data['rekey-time'] is None:
        data['rekey-time-hr'] = 'n/a'
    else:
        data['rekey-time-hr'] = lib.time.epoch2iso(
            lib.time.now(as_type='epoch') + data['rekey-time']
        )

    if data['remote-id'] != data['remote-host']:
        data['remote'] = (
            f'{data["remote-host"]}:{data["remote-port"]} ("{data["remote-id"]}")'
        )
    else:
        data['remote'] = f'{data["remote-host"]}:{data["remote-port"]}'
    data['state'] = data['state'].replace('ESTABLISHED', 'EST')
    data['version'] = f'v{data["version"]}'

    return data


def _join_traffic_selectors(values):
    """Join a VICI `local-ts` / `remote-ts` list into a single
    comma-separated text string. VICI returns the list elements as
    `bytes` at runtime; the JSON fixtures used by the unit tests
    hold already-decoded `str` values. Both shapes work.
    """
    if not values:
        return ''
    return ', '.join(lib.txt.to_text(v, errors='strict_or_latin1') for v in values)


def _optional_int(details, key):
    """Return an integer VICI value in seconds or bytes, or `None` if
    VICI omitted it.

    VICI reports the traffic counters and the timers of a CHILD SA only
    once the SA is installed, and reports `life-time` and `rekey-time`
    only if the SA expires at all. It omits a key rather than sending it
    empty, but an empty value is treated as absent too, so that both
    shapes take the same path.

    An absent value is unknown, which is not the same as zero: a zero
    `life-time` reads as "expires right now", and zero `bytes-in` reads
    as "this SA has carried no traffic".
    """
    value = details.get(key)
    if isinstance(value, bytes):
        value = lib.txt.to_text(value)
    if value is None or value == '':
        return None
    return int(value)


def format_child_data(child):
    """Re-format child connection details for a single sub connection.
    This is much more volatile (depending on the conn state), so list all expected keys manually
    and return empty defaults if necessary.

    `child-bytes-in`, `child-bytes-out`, `child-install-time`,
    `child-life-time` and `child-rekey-time` are `None` when VICI omits
    them, so that the caller can skip their performance data instead of
    trending a made-up zero.
    """
    data = {}
    data['child-bytes-in'] = _optional_int(child, 'bytes-in')
    data['child-bytes-out'] = _optional_int(child, 'bytes-out')
    data['child-dh-group'] = lib.txt.to_text(child.get('dh-group', ''))
    data['child-encr-alg'] = lib.txt.to_text(child.get('encr-alg', ''))
    data['child-encr-keysize'] = lib.txt.to_text(child.get('encr-keysize', ''))
    data['child-install-time'] = _optional_int(child, 'install-time')
    data['child-integ-alg'] = lib.txt.to_text(child.get('integ-alg', 'None'))
    data['child-life-time'] = _optional_int(child, 'life-time')
    data['child-local-ts'] = _join_traffic_selectors(child.get('local-ts'))
    data['child-mode'] = lib.txt.to_text(child.get('mode', ''))
    # child-name is the admin-configured connection name and may carry non-ASCII
    # bytes, so decode it tolerantly like the SAS scalars (Linuxfabrik/lib#256).
    data['child-name'] = lib.txt.to_text(
        child.get('name', ''), errors='strict_or_latin1'
    )
    data['child-protocol'] = lib.txt.to_text(child.get('protocol', ''))
    data['child-rekey-time'] = _optional_int(child, 'rekey-time')
    data['child-remote-ts'] = _join_traffic_selectors(child.get('remote-ts'))
    data['child-state'] = lib.txt.to_text(child.get('state', ''))

    if data['child-bytes-in'] is None:
        data['child-bytes-in-hr'] = 'n/a'
    else:
        data['child-bytes-in-hr'] = lib.human.bytes2human(data['child-bytes-in'])
    if data['child-bytes-out'] is None:
        data['child-bytes-out-hr'] = 'n/a'
    else:
        data['child-bytes-out-hr'] = lib.human.bytes2human(data['child-bytes-out'])
    # Join the key size to the algorithm only if VICI sent one,
    # otherwise a fixed-key cipher renders a dangling dash ("3DES-").
    child_encr = data['child-encr-alg']
    if data['child-encr-keysize']:
        child_encr = f'{child_encr}-{data["child-encr-keysize"]}'
    data['child-encr'] = (
        f'{data["child-protocol"]}:'
        f'{child_encr}'
        f'/{data["child-integ-alg"]}'
        f'/{data["child-dh-group"]}'
    )
    if data['child-install-time'] is None:
        data['child-install-time-hr'] = 'n/a'
    else:
        data['child-install-time-hr'] = lib.time.epoch2iso(
            lib.time.now(as_type='epoch') - data['child-install-time']
        )
    if data['child-life-time'] is None:
        data['child-life-time-hr'] = 'n/a'
    else:
        data['child-life-time-hr'] = lib.time.epoch2iso(
            lib.time.now(as_type='epoch') + data['child-life-time']
        )
    data['child-mode-state'] = f'{data["child-mode"]}:{data["child-state"]}'
    if data['child-rekey-time'] is None:
        data['child-rekey-time-hr'] = 'n/a'
    else:
        data['child-rekey-time-hr'] = lib.time.epoch2iso(
            lib.time.now(as_type='epoch') + data['child-rekey-time']
        )

    return data


def main():
    """The main function. This is where the magic happens."""

    # parse the command line
    try:
        args = parse_args()
    except SystemExit:
        sys.exit(STATE_UNKNOWN)

    if args.IGNORE is None:
        args.IGNORE = []
    if args.MATCH is None:
        args.MATCH = []

    # compile --match and --ignore patterns (case-sensitive by default,
    # matching the lib.args convention; the user can opt into
    # case-insensitive matching with the inline `(?i)` flag)
    try:
        match_patterns = [re.compile(p) for p in args.MATCH]
        ignore_patterns = [re.compile(p) for p in args.IGNORE]
    except re.error as e:
        lib.base.cu(f'Invalid regular expression: {e}')

    # fetch data
    if args.TEST is None:
        s = socket.socket(socket.AF_UNIX)
        try:
            s.connect(args.SOCKET)
        except OSError as e:
            s.close()
            # A socket that is not there is what a host without a running strongSwan
            # looks like, not a defect worth a Python stack trace.
            lib.base.cu(
                f'Failed to connect to the VICI socket at {args.SOCKET}: {e}. '
                'Check that strongSwan runs and that its vici plugin is loaded.',
                traceback=False,
            )
        try:
            session = vici.Session(s)
            list_conns = list(session.list_conns())
            # list_sas() returns a generator backed by the VICI
            # socket; materialise it to a list now so we can close
            # the socket cleanly before processing the data.
            list_sas = list(session.list_sas())
        finally:
            s.close()
    else:
        # Single-file test fixture: a JSON object with `list_conns`
        # and `list_sas` keys, both holding the raw VICI "list of
        # single-key dicts" shape. `active_connection_keys` is
        # derived from `list_sas` below via the same `_collect_keys`
        # helper the production path uses, so the test and the
        # production paths cannot drift.
        fixture_path = args.TEST[0] if args.TEST else ''
        if not fixture_path or not lib.disk.file_exists(fixture_path, allow_empty=True):
            hint = ''
            for legacy in (
                '-possible_connection_keys',
                '-active_connection_keys',
                '-list_sas',
            ):
                if fixture_path.endswith(legacy):
                    hint = (
                        f' (the three-file convention was removed; drop the '
                        f'`{legacy}` suffix and pass '
                        f'`{fixture_path[: -len(legacy)]}` instead)'
                    )
                    break
            lib.base.cu(f'Test fixture not found: {fixture_path}{hint}')
        stdout, _stderr, _retc = lib.lftest.test(args.TEST)
        try:
            fixture = json.loads(stdout)
        except json.JSONDecodeError as e:
            lib.base.cu(f'Malformed JSON in test fixture {fixture_path}: {e}')
        list_conns = fixture.get('list_conns', [])
        list_sas = fixture.get('list_sas', [])
    possible_connection_keys = _collect_keys(list_conns)
    active_connection_keys = _collect_keys(list_sas)

    # Apply the --match / --ignore filter to both the configured and
    # the active connection lists. Filtering before the "configured
    # but not active" comparison means an ignored connection that is
    # configured but currently down does not trigger the warning,
    # which is exactly the point of --ignore.
    possible_connection_keys = [
        k
        for k in possible_connection_keys
        if keep_connection(k, match_patterns, ignore_patterns)
    ]
    active_connection_keys = [
        k
        for k in active_connection_keys
        if keep_connection(k, match_patterns, ignore_patterns)
    ]

    if not possible_connection_keys:
        lib.base.oao(
            'No connections configured.', STATE_UNKNOWN, always_ok=args.ALWAYS_OK
        )
    if not active_connection_keys:
        lib.base.oao(
            'There are no active connections at all.',
            STATE_WARN,
            always_ok=args.ALWAYS_OK,
        )

    # init some vars
    msg = ''
    state = STATE_OK
    perfdata = ''
    table_data = []

    if possible_connection_keys != active_connection_keys:
        conf_state = STATE_WARN
        state = lib.base.get_worst(state, conf_state)
        msg += (
            f'One or more connections are configured '
            f'but not active'
            f'{lib.base.state2str(conf_state, prefix=" ")}. '
        )

    # analyze data. `list_sas` is a list of single-key dicts where
    # each key is the connection name and the value is the SA
    # details; iterate the dict entries directly instead of trying
    # `sas[name]` for every `active_connection_keys` item (which was
    # O(n*m) and relied on a bare `except` to probe for presence).
    active_keyset = set(active_connection_keys)
    # VICI keys each IKE SA by a non-unique connection name, so several
    # SAs can share one key. Report the performance data of one primary
    # SA per key, otherwise a rekeying IKE SA or a shared connection
    # name emits the same label more than once. The table lists them all.
    primary_uniqueids = _primary_uniqueids(list_sas, active_keyset)
    for sas in list_sas:
        for key, details in sas.items():
            if key not in active_keyset:
                continue
            row = format_sas_data(details)
            row['conn'] = key
            uniqueid = lib.txt.to_text(details.get('uniqueid', ''))
            is_primary = uniqueid == str(primary_uniqueids.get(key, ''))
            # The timers are absent unless the SA is established, and
            # performance data values have to be numeric.
            if is_primary and row['established'] is not None:
                perfdata += lib.base.get_perfdata(
                    f'{key}_established',
                    row['established'],
                    uom='s',
                    _min=0,
                )
            # No _min on the two below: VICI reports the time left,
            # which goes negative once the deadline is overdue.
            if is_primary and row['reauth-time'] is not None:
                perfdata += lib.base.get_perfdata(
                    f'{key}_reauth-time',
                    row['reauth-time'],
                    uom='s',
                )
            if is_primary and row['rekey-time'] is not None:
                perfdata += lib.base.get_perfdata(
                    f'{key}_rekey-time',
                    row['rekey-time'],
                    uom='s',
                )

            children = details.get('child-sas') or None
            if children == {}:
                children = None

            if children is not None:
                for child_key in children:
                    child_row = format_child_data(children[child_key])
                    # combine two dictionaries using dictionary comprehension
                    table_data.append(
                        {k: v for d in (row, child_row) for k, v in d.items()}
                    )
                    if not is_primary:
                        continue
                    # The counters and timers are absent unless the
                    # CHILD SA is installed, and the two lifetimes also
                    # depend on the SA expiring at all.
                    if child_row['child-bytes-in'] is not None:
                        perfdata += lib.base.get_perfdata(
                            f'{key}_{child_row["child-name"]}_bytes-in',
                            child_row['child-bytes-in'],
                            uom='B',
                            _min=0,
                        )
                    if child_row['child-bytes-out'] is not None:
                        perfdata += lib.base.get_perfdata(
                            f'{key}_{child_row["child-name"]}_bytes-out',
                            child_row['child-bytes-out'],
                            uom='B',
                            _min=0,
                        )
                    if child_row['child-install-time'] is not None:
                        perfdata += lib.base.get_perfdata(
                            f'{key}_{child_row["child-name"]}_install-time',
                            child_row['child-install-time'],
                            uom='s',
                            _min=0,
                        )
                    # No _min on the two below: VICI reports the time
                    # left, which goes negative once they are overdue.
                    if child_row['child-life-time'] is not None:
                        perfdata += lib.base.get_perfdata(
                            f'{key}_{child_row["child-name"]}_life-time',
                            child_row['child-life-time'],
                            uom='s',
                        )
                    if child_row['child-rekey-time'] is not None:
                        perfdata += lib.base.get_perfdata(
                            f'{key}_{child_row["child-name"]}_rekey-time',
                            child_row['child-rekey-time'],
                            uom='s',
                        )
            elif is_primary:
                # Only the primary SA drives the alert: a transient
                # second SA under the same name (during an IKE rekey)
                # may legitimately carry no child yet.
                child_state = STATE_WARN
                state = lib.base.get_worst(state, child_state)
                msg += (
                    f'{key} not connected at child level'
                    f'{lib.base.state2str(child_state, prefix=" ")}. '
                )

    # build the message
    if state == STATE_OK:
        msg = 'Everything is ok.'

    # over and out
    if table_data:
        if not args.LENGTHY:
            keys = [
                'conn',
                'state',
                'rekey-time-hr',
                'child-name',
                'child-mode-state',
                'child-rekey-time-hr',
                'child-life-time-hr',
                'child-bytes-in-hr',
                'child-bytes-out-hr',
            ]
            headers = [
                'Conn.',
                'State',
                'IKE Re-Keying',
                'Child',
                'Mode:State',
                'Re-Keying',
                'Expires',
                'Rx',
                'Tx',
            ]
        else:
            keys = [
                'conn',
                'state',
                'established-hr',
                'reauth-time-hr',
                'rekey-time-hr',
                'version',
                'local',
                'remote',
                'encr',
                'child-name',
                'child-mode-state',
                'child-local-ts',
                'child-remote-ts',
                'child-encr',
                'child-install-time-hr',
                'child-rekey-time-hr',
                'child-life-time-hr',
                'child-bytes-in-hr',
                'child-bytes-out-hr',
            ]
            headers = [
                'Conn.',
                'State',
                'Established',
                'Re-Authentication',
                'Re-Keying',
                'IKE',
                'Local',
                'Remote',
                'Encryption/Integrity/Pseudo Random/DH',
                'Child',
                'Mode:State',
                'Local',
                'Remote',
                'Prot:Encryption/Integrity/DH',
                'Installed',
                'Re-Keying',
                'Expires',
                'Rx',
                'Tx',
            ]
        lib.base.oao(
            f'{msg}\n\n{lib.base.get_table(table_data, keys, header=headers)}',
            state,
            perfdata,
            always_ok=args.ALWAYS_OK,
            no_perfdata=args.NO_PERFDATA,
        )
    else:
        lib.base.oao(
            msg, state, perfdata, always_ok=args.ALWAYS_OK, no_perfdata=args.NO_PERFDATA
        )


if __name__ == '__main__':
    try:
        main()
    except Exception:
        lib.base.cu()
