#!/bin/bash -e
#
# collect-bonding-info -- Collect all bonding information to a file
#

# Return code for plugin to indicate that it cannot run due to missing
# requirements
#
REQUIREMENTS_MISSING=99

# Use an associative array to enable/disable plugins
#
declare -A plugins


# check_binary <binary>
#
# Returns true if binary is available in the path
#
function check_binary() {
    which $1 >/dev/null 2>&1
}


# contains [item] [listitem]...
#
# Returns true if item is contained in any of the listitems given.
#
function contains() {
    item=$1
    shift
    for listitem in $@ ; do
        if [ "$listitem" = "$item" ] ; then
            return 0
        fi
    done
    return 1
}


# get_dest
#
# Get the path to write files to
#
function get_dest() {
    if [ -z $PLUGIN ] ; then
        echo $DEST
    else
        echo $DEST/$PLUGIN
    fi
}


# get_logfile
#
# Get the logfile to write to
#
function get_logfile() {
    echo $(get_dest)/log
}

# get_net_namespaces
#
# Get network namespaces only if node is a PWAN router
#
function get_net_namespaces() {
    if [ $(get_node_type) = "privatewanrouter" ] ; then
        echo namespaces="$(ip netns list | cut -d " " -f 1)"
    fi
}

# log <msg>...
#
# Log message to output and logfile
#
function log() {
    msg="$@"
    echo "$msg"

    echo "$msg" >> $(get_logfile)
}


# save_exec <name> <cmd>...
#
# Log output of cmd to a file
#
function save_exec() {
    name=$1
    shift
    cmd="$@"

    filename=$(get_dest)/$name

    mkdir -p $(dirname $filename)

    log "SAVE EXEC: $cmd -> $name"
    bash -c "$cmd" >> $filename 2>>$(get_logfile)
    log "SAVE EXEC END: $cmd"
}


# log_exec <cmd>...
#
# Log output of cmd to logfile
#
function log_exec() {
    cmd="$@"

    log "LOG EXEC START: $cmd"
    bash -c "$cmd" >> $(get_logfile)
    log "LOG EXEC END: $cmd"
}


# save_files <files>...
#
# Save files
#
function save_files() {
    if [ ! -z "$PLUGIN" ] ; then
        path=$DEST/$PLUGIN/
    else
        path=$DEST/
    fi
    mkdir -p $path

    for file in "$@" ; do
        if [ -h "$file" ] ; then
            src_file=$(readlink -f "$file")
        else
            src_file="$file"
        fi
        if [ -e "$file" ] ; then
            log "SAVE_FILES $file -> $path"
            target="$path$(dirname $file)"
            mkdir -p $target
            cp -r "$src_file" "$target" 2>>$(get_logfile)
        else
            log "!!! SAVE_FILES $file does not exist"
        fi
    done
}



# Plugins
#

function plugin_system() {
    save_exec uptime uptime
    save_exec memory free -k
    save_exec disk-usage df -h
    save_exec inode-usage df -i
    save_files /boot/grub2/grub.cfg
    save_exec uname uname -a
    save_files /proc/cmdline
    save_files /proc/modules
    module_params=$DEST/$PLUGIN/module-parameters
    for module in $(cut -d " " -f 1 /proc/modules) ; do
        param_path=/sys/module/$module/parameters
        if [ -d $param_path ] ; then
            echo "Module $module parameters" >> $module_params
            for param in $param_path/* ; do
                echo "    $(basename $param): $(cat $param)" >> $module_params
            done
        fi
    done
    save_exec getconf getconf -a
    save_exec ipc-limits ipcs -l
    save_exec ipc-usage ipcs -a
    save_exec dmesg dmesg
}
plugins[system]=1


function plugin_sysctl() {
    save_exec sysctl sysctl -A
    save_files /etc/sysctl.*
}
plugins[sysctl]=1


function plugin_vmstat() {
    save_exec vmstat vmstat 1 4
}
plugins[vmstat]=1


function plugin_processes() {
    save_exec processes ps axfwwo user,pid,ppid,%cpu,%mem,vsz,rss,stat,time,cmd
}
plugins[processes]=1


function plugin_lsof() {
    check_binary lsof || return $REQUIREMENTS_MISSING
    save_exec lsof lsof -n
}
plugins[lsof]=1


function plugin_systemd() {
    check_binary systemctl || return $REQUIREMENTS_MISSING
    save_exec status systemctl
    save_exec timers systemctl list-timers
    save_exec jobs systemctl list-jobs
    save_exec sockets systemctl list-sockets
    save_exec parameters systemctl show
    save_exec failed systemctl --failed
    save_exec bus-status busctl --system list
    save_exec timedate-status timedatectl status
    save_exec analyze systemd-analyze blame
    save_exec cgtop systemd-cgtop --batch --iterations=1
    save_exec cgls systemd-cgls --all --full
    mkdir $(get_dest)/services
    for service in $(systemctl list-unit-files | tail -n+2 | head -n-2 | cut -d ' ' -f 1) ; do
        systemctl status "$service" > $(get_dest)/services/status.$service
    done
}
plugins[systemd]=1


function plugin_journal() {
    check_binary journalctl || return $REQUIREMENTS_MISSING
    # Get everything since boot
    log_exec journalctl -b
}
plugins[journal]=1


function plugin_distribution() {
    save_files /etc/os-release
}
plugins[distribution]=1


function plugin_apt() {
    save_files /etc/apt
    save_exec packages dpkg -l
    save_files /var/log/dpkg.log*
}
plugins[apt]=1


function plugin_hardware() {
    save_files /proc/cpuinfo
    check_binary hwinfo && save_exec hwinfo hwinfo --all
    save_exec lspci lspci -nn
    save_exec lspci-verbose lspci -vvv
    save_exec lsusb lsusb
    save_exec lsusb-verbose lsusb -vv
    save_exec dmidecode dmidecode
}
plugins[hardware]=1


function get_bond_ids() {
    cat << "EOF" | python
import json, sys

config = json.load(open('/var/lib/bonding/configuration.json'))
if "bond" in config:
    print(config["bond"]["id"])
elif "bonds" in config:
    for bond in config["bonds"]:
        print(bond["id"])
EOF
}


function get_bridge_ids() {
    cat << "EOF" | python
import json, sys

config = json.load(open('/var/lib/bonding/configuration.json'))
if "bond" in config and config["bond"]["bridge_enabled"]:
    print(config["bond"]["id"])
elif "bonds" in config:
    for bond in config["bonds"]:
        if bond["bridge_enabled"]:
            print(bond["id"])
EOF
}


function get_node_type() {
    cat << "EOF" | python
import json, sys

config = json.load(open('/var/lib/bonding/configuration.json'))
if "bonder" in config:
    print('bonder')
elif "aggregator" in config:
    print('aggregator')
elif "privatewanrouter" in config:
    print('privatewanrouter')
else:
    print('unknown')
EOF
}


function plugin_bonding() {
    node_type=$(get_node_type)
    save_files /etc/bonding/
    save_files /var/lib/bonding/
    save_files /var/run/bonding/openvpn.mtun0.status

    log_exec bondsh node ping
    save_exec node-status bondsh node status

    log_exec bondsh config ping
    save_exec config-status bondsh config status
    save_exec config-config bondsh config config

    log_exec bondsh subprocess ping
    save_exec subprocess-status bondsh subprocess status

    if [ "$node_type" = "bonder" ] ; then
        save_files /var/run/bonding/dnsmasq.*
        save_files /var/run/bonding/uacctd.conf
        save_exec node-legs bondsh node legs
        save_exec node-dhcpclients bondsh node dhcpclients

        if pidof cell ; then
            log_exec bondsh cell ping
            save_exec cell-connection bondsh cell connection
            save_exec cell-modems bondsh cell modemsq
            save_exec cell-signal bondsh cell signal
        fi
    fi

    if contains "$node_type" bonder aggregator ; then
        for tunnel_id in $(get_bond_ids) ; do
            log_exec bondsh tunnel $tunnel_id ping
            save_exec tunnel-$tunnel_id-status bondsh tunnel $tunnel_id status
            save_exec tunnel-$tunnel_id-rates bondsh tunnel $tunnel_id rates
            save_exec tunnel-$tunnel_id-reorderer bondsh tunnel $tunnel_id reorderer
            save_exec tunnel-$tunnel_id-balancer bondsh tunnel $tunnel_id balancer
        done

        for bridge_id in $(get_bridge_ids) ; do
            log_exec bondsh bridge $bridge_id ping
            save_exec bridge-$bridge_id-status bondsh bridge $bridge_id status
            save_exec bridge-$bridge_id-insideconnections bondsh bridge $bridge_id insideconnections
            save_exec bridge-$bridge_id-outsideconnections bondsh bridge $bridge_id outsideconnections
        done
    fi
}
plugins[bonding]=1


function plugin_network() {
    save_exec ip-xfrm-state ip xfrm state
    save_exec ip-xfrm-policy ip xfrm policy
    save_files $(ls -d /etc/bonding/nftables/*)

    for netns in "" $(get_net_namespaces) ; do
        if [ ! -z "$netns" ] ; then
            wrapper="ip netns exec $netns"
            prefix=namespaces/$netns/
        else
            wrapper=
            prefix=
        fi

        ip="$wrapper ip"
        conntrack="$wrapper conntrack"
        nftables="$wrapper nft"

        save_exec ${prefix}conntrack-4 $conntrack -L --family ipv4
        save_exec ${prefix}conntrack-6 $conntrack -L --family ipv6
        save_exec ${prefix}links $ip -s link
        save_exec ${prefix}neighbours-4 $ip neigh
        save_exec ${prefix}neighbours-6 $ip ntable
        save_exec ${prefix}nftables $nftables list ruleset
        for proto in 4 6 ; do
            save_exec ${prefix}addresses-$proto $ip -$proto addr
            save_exec ${prefix}rules-$proto $ip -$proto rule
            save_exec ${prefix}tunnels-$proto $ip -$proto tunnel
            tables=$($ip -$proto rule | sed -e 's/.*lookup \(.*\)/\1/g' | sed -e 's/\[l3mdev-table\]//' | sort | uniq)
            for table in $tables ; do
                save_exec ${prefix}routes-$proto-table-$table $ip -$proto route show table $table
            done
        done
    done
}
plugins[network]=1


function plugin_bird() {
    node_type=$(get_node_type)

    if [ "$node_type" = "aggregator" ] ; then
        declare -A tablespacekey
        tablespacekey[krt2000ipv4]=_global
        tablespacekey[krt2000ipv6]=_global
        while read vrf table ; do
            tablespacekey[krt${table}ipv4]=${vrf:4}
            tablespacekey[krt${table}ipv6]=${vrf:4}
        done < <(ip vrf | tail -n +3)
    fi

    for netns in "" $(get_net_namespaces) ; do
        if [ ! -z "$netns" ] ; then
            birdc="pwanbirdc $netns"
            suffix="-$netns"
            spacedir="$netns"
        else
            birdc="pwanbirdc -"
            suffix=
            spacedir="_global"
        fi

        config=$($birdc configure check | tail -n 2 | head -n 1 | cut -d " " -f 4)
        save_files $config

        while read proto_name proto_type _ ; do
            if contains "$proto_type" Babel BGP OSPF Pipe Static ; then
                # Differentiate spaces on aggregators based on VRF tables
                if [ "$node_type" = "aggregator" ] ; then
                    table_name=$($birdc sho pro all $proto_name | grep Table: | head -n 1 | awk '{print $2}')
                    spacedir=${tablespacekey[$table_name]}
                fi
                save_exec $spacedir/protocols/$proto_name $birdc show protocol all $proto_name
                save_exec $spacedir/imported-routes/$proto_name $birdc show route protocol $proto_name all
                save_exec $spacedir/exported-routes/$proto_name $birdc show route export $proto_name all
            fi
            if [ "$proto_type" = "Babel" ] ; then
                for field in entries interface neighbors routes ; do
                    save_exec $spacedir/babel-$field/$proto_name $birdc show babel $field $proto_name
                done
            elif [ "$proto_type" = "OSPF" ] ; then
                for field in interface lsadb neighbors state topology ; do
                    save_exec $spacedir/ospf-$field/$proto_name $birdc show ospf $field $proto_name
                done
            elif [ "$proto_type" = "Static" ] ; then
                save_exec $spacedir/static/$proto_name $birdc show static $proto_name
            fi
        done < <($birdc show protocols | tail -n +3)
    done
}
plugins[bird]=1


# run_plugins <dest>
#
# Run all enabled plugins saving data under dest
#
function run_plugins() {
    for plugin in "${!plugins[@]}" ; do
        if [ "${plugins[$plugin]}" = 1 ] ; then
            log "### Start plugin: $plugin"
            PLUGIN=$plugin
            mkdir $DEST/$plugin
            set +e
            plugin_$plugin
            ret=$?
            set -e
            unset PLUGIN
            if [ $ret = $REQUIREMENTS_MISSING ] ; then
                log "Plugin requirements missing"
                rmdir $DEST/$plugin
            elif [ $ret = 0 ] ; then
                log "Plugin execution completed"
            else
                log "!!! Plugin execution failed"
            fi
            log "### End plugin: $plugin"
        fi
    done
}


# set_plugin_enabled <plugin> <enabled>
#
# Set whether plugin is is enabled. The enabled value must be 1 or 0
#
function set_plugin_enabled() {
    plugin=$1
    enabled=$2

    if ! contains $plugin ${!plugins[@]} ; then
        echo "Unknown plugin $plugin" >&2
        return 1
    fi

    plugins[$plugin]=$enabled
}


function get_bonding_key() {
    if [ -f /etc/bonding/bonding.conf ] ; then
        key=$(grep -e "^key" /etc/bonding/bonding.conf | sed -e 's/.*=\s//g')
    fi
    if [ ! -z "$key" ] ; then
        echo "$key"
    else
        echo "UNCONFIGURED"
    fi
}

function usage() {
    echo "$(basename $0) [-h] [<+|->plugin]..."
    echo
    echo "Plugins can be enabled or disabled by prepending them with + or - respectively"
    echo
    echo "Available plugins"
    field_len=0
    for plugin in "${!plugins[@]}" ; do
        len=${#plugin}
        test $len -gt $field_len && field_len=$len
    done

    for plugin in "${!plugins[@]}" ; do
        if [ "${plugins[$plugin]}" = "1" ] ; then
            enabled="enabled"
        else
            enabled="disabled"
        fi
        printf "    %-*s  %s\n" $field_len $plugin $enabled
    done
}

if contains $1 -h --help  ; then
    usage
    exit 0
fi

for arg in $@ ; do
    if [ "${arg::1}" = '+' ] ; then
        set_plugin_enabled ${arg:1} 1
    fi
    if [ "${arg::1}" = '-' ] ; then
        set_plugin_enabled ${arg:1} 0
    fi
done

TMP=$(mktemp -d)
trap 'rm -rf $TMP' EXIT

KEY="$(get_bonding_key)"
TIMESTAMP=$(date +%s)
DUMPNAME="bonding-info-$KEY-$TIMESTAMP"
DEST=$TMP/$DUMPNAME
mkdir -p $DEST

log "# Support dump for $KEY at $TIMESTAMP"

log "## Running plugins"
run_plugins

DUMPFILE="$(basename $DEST).tar.gz"
log "## Generating $DUMPFILE"
tar -C $(dirname $DEST) -czf $DUMPFILE $(basename $DEST)
log "## Done"
