#!/usr/bin/python3

"""Munin plugin to monitor Libreswan IPsec connections.

=head1 NAME

libreswan - monitor Libreswan IPsec connections

=head1 APPLICABLE SYSTEMS

Systems with Libreswan.

=head1 CONFIGURATION

This plugin requires Munin config /etc/munin/plugin-conf.d/libreswan:

[libreswan]
user root

=head1 AUTHOR

Kim B. Heino <b@bbbs.net>
Paul Wouters <paul@nohats.ca>

=head1 LICENSE

GPLv2

=head1 MAGIC MARKERS

 #%# capabilities=autoconf
 #%# family=auto

=cut

"""

import subprocess
import sys
from collections import defaultdict


def tree():
    """Tree of dicts."""
    return defaultdict(tree)


def get_stats():
    """Get statistics."""
    # Get status output
    try:
        output = subprocess.run(['ipsec', 'whack', '--globalstatus'],
                                stdout=subprocess.PIPE, check=False,
                                encoding='utf-8', errors='ignore')
    except FileNotFoundError:
        return {}

    # Parse output
    values = tree()
    for line in output.stdout.splitlines():
        if '=' not in line:
            continue
        prefix, val = line.split('=')
        prefix = prefix.split('.')
        pos = values
        for key in prefix[:-1]:
            pos = pos[key]
        pos[prefix[-1]] = val
    return values


def derive_gauge(entry, value, config, graph_type):
    """Print config or value."""
    if config:
        print('{entry}.label {entry}'.format(entry=entry))
        print('{}.type {}'.format(entry, graph_type))
        print('{}.min 0'.format(entry))
    else:
        print('{}.value {}'.format(entry, value))


def derive(entry, value, config):
    """Print config or value."""
    derive_gauge(entry, value, config, 'DERIVE')


def gauge(entry, value, config):
    """Print config or value."""
    derive_gauge(entry, value, config, 'GAUGE')


def print_config(name, title, vlabel, config):
    """Print config header."""
    print('multigraph {}'.format(name))
    if config:
        print('graph_title {}'.format(title))
        print('graph_vlabel {}'.format(vlabel))
        print('graph_category security')
        print('graph_args --base 1000 --lower-limit 0')


def updown(name, title, values, config):
    """Print up/down header."""
    print('multigraph {}'.format(name))
    if config:
        print('graph_title {}'.format(title))
        print('graph_category security')
        print('graph_order down up')
        print('graph_args --base 1000')
        print('graph_vlabel bytes in (-) / out (+) per ${graph_period}')
        print('down.label received')
        print('down.type DERIVE')
        print('down.graph no')
        print('down.cdef down,8,*')
        print('down.min 0')
        print('up.label bps')
        print('up.type DERIVE')
        print('up.negative down')
        print('up.cdef up,8,*')
        print('up.min 0')
    for entry, value in values.items():
        orig = 'down' if entry == 'in' else 'up'
        if config:
            print('{}.label {}'.format(orig, entry))
            print('{}.type DERIVE'.format(orig))
            print('{}.min 0'.format(orig))
        else:
            print('{}.value {}'.format(orig, value))


def derive_all(name, title, vlabel, values, config):
    """Print config of value for all items."""
    print_config(name, title, vlabel, config)
    for entry, value in values.items():
        derive(entry, value, config)


def gauge_all(name, title, vlabel, values, config):
    """Print config of value for all items."""
    print_config(name, title, vlabel, config)
    for entry, value in values.items():
        gauge(entry, value, config)


def print_values(values, config):
    """Print values or config."""
    if not values:
        return

    derive_all(
        'vpn_ipsec_types', 'IPsec SA Types', 'total',
        values['total']['ipsec']['type'], config)
    derive_all(
        'vpn_ipsec_encr', 'IPsec SA ENCR', 'total',
        values['total']['ipsec']['encr'], config)
    derive_all(
        'vpn_ipsec_integ', 'IPsec SA INTEG', 'total',
        values['total']['ipsec']['integ'], config)

    print_config('vpn_current', 'Current States', 'current', config)
    for entry, value in values['current']['states']. items():
        if entry not in ('enumerate', 'iketype'):
            gauge(entry, value, config)

    gauge_all(
        'vpn_iketype', 'Current IKE types', 'iketypes',
        values['current']['states']['iketype'], config)
    gauge_all(
        'vpn_state_kind', 'Current pluto states', 'pluto_states',
        values['current']['states']['enumerate'], config)
    derive_all(
        'vpn_state_transition_func', 'Pluto STFs', 'total',
        values['total']['pluto']['stf'], config)
    updown(
        'vpn_traffic_ipsec', 'Total IPsec Traffic',
        values['total']['ipsec']['traffic'], config)
    updown(
        'vpn_traffic_ike', 'Total IKE Traffic',
        values['total']['ike']['traffic'], config)
    derive_all(
        'vpn_dpd', 'Total DPD Traffic', 'dpd_traffic',
        values['total']['ike']['dpd'], config)

    print_config('vpn_ike', 'Total IKE Sessions', 'ike_traffic', config)
    for entry in ('ikev2_ok', 'ikev2_fail', 'ikev1_ok', 'ikev1_fail'):
        if config:
            print('{entry}.label {entry}'.format(entry=entry))
            print('{}.type DERIVE'.format(entry))
            print('{}.min 0'.format(entry))
        else:
            ike = entry[:5]
            if 'fail' in entry:
                status = 'failed'
            else:
                status = 'established'
            print('{}.value {}'.format(
                entry, values['total']['ike'][ike][status]))

    derive_all(
        'vpn_ikev1_sent_notifies', 'IKEv1 sent NOTIFIES', 'total',
        values['total']['ikev1']['sent']['notifies']['error'], config)
    derive_all(
        'vpn_ikev2_sent_notifies', 'IKEv2 sent NOTIFIES', 'total',
        values['total']['ikev2']['sent']['notifies']['error'], config)
    derive_all(
        'vpn_ikev1_recv_notifies', 'IKEv1 recv NOTIFIES', 'total',
        values['total']['ikev1']['recv']['notifies']['error'], config)
    derive_all(
        'vpn_ikev2_recv_notifies', 'IKEv2 recv NOTIFIES', 'total',
        values['total']['ikev2']['recv']['notifies']['error'], config)

    # Down from here it is all crypto params

    derive_all(
        'vpn_ikev1_encr', 'IKEv1 ENCR', 'total',
        values['total']['ikev1']['encr'], config)
    derive_all(
        'vpn_ikev2_encr', 'IKEv2 ENCR', 'total',
        values['total']['ikev2']['encr'], config)
    derive_all(
        'vpn_ikev1_integ', 'IKEv1 INTEG', 'total',
        values['total']['ikev1']['integ'], config)
    derive_all(
        'vpn_ikev2_integ', 'IKEv2 INTEG', 'total',
        values['total']['ikev2']['integ'], config)
    derive_all(
        'vpn_ikev1_group', 'IKEv1 GROUP', 'total',
        values['total']['ikev1']['group'], config)
    derive_all(
        'vpn_ikev2_group', 'IKEv2 GROUP', 'total',
        values['total']['ikev2']['group'], config)
    derive_all(
        'vpn_ikev2_recv_badgroup_in', 'IKEv2 recv INVALID GROUP', 'total',
        values['total']['ikev2']['recv']['invalidke']['using'], config)
    derive_all(
        'vpn_ikev2_recv_badgroup_out', 'IKEv2 recv-sent INVALID GROUP',
        'total',
        values['total']['ikev2']['recv']['invalidke']['suggesting'], config)
    derive_all(
        'vpn_ikev2_sent_badgroup_in', 'IKEv2 sent-recv INVALID GROUP', 'total',
        values['total']['ikev2']['sent']['invalidke']['using'], config)
    derive_all(
        'vpn_ikev2_sent_badgroup_out', 'IKEv2 sent-sent INVALID GROUP',
        'total',
        values['total']['ikev2']['sent']['invalidke']['suggesting'], config)


def main():
    """Do it all main program."""
    values = get_stats()
    if len(sys.argv) > 1 and sys.argv[1] == 'autoconf':
        print('yes' if values else 'no (Libreswan is not running)')
    elif len(sys.argv) > 1 and sys.argv[1] == 'config':
        print_values(values, True)
    else:
        print_values(values, False)


if __name__ == '__main__':
    main()
