#!/bin/bash

# This test script builds and installs CFEngine, and runs our binaries
# with valgrind to look for memory leaks, and similar.
#
# It is normally run inside a container (see Containerfile next to it).
# If you want to run it without a container, you can.
# It assumes a few things:
# 1. Must be run from inside core (CWD)
# 2. Masterfiles must be at ../masterfiles
# 3. Normal build tools must be installed, and valgrind and diff
# 4. Must be run using sudo / as root, or have necessary permissions
# 5. CFEngine should not already be installed
#
# In order to run this in an Ubuntu VM, you can do something like this:
# sudo apt-get install -y build-essential git libtool autoconf automake bison flex libssl-dev libpcre2-dev libbison-dev libacl1 libacl1-dev lmdb-utils liblmdb-dev libpam0g-dev libtool libyaml-dev libxml2-dev librsync-dev valgrind
# git clone https://github.com/cfengine/masterfiles
# git clone --recursive https://github.com/cfengine/core
# cd core
# sudo bash tests/valgrind-check/valgrind.sh

function print_ps {
    set +e
    echo "CFEngine processes:"
    ps aux | grep [c]f-

    echo "Valgrind processes:"
    ps aux | grep [v]algrind
    set -e
}

function stop_daemons {
    echo "Stopping cfengine daemons"
    pkill -f cf-serverd  || echo "Did not kill cf-serverd"
    pkill -f cf-execd    || echo "Did not kill cf-execd"
    pkill -f cf-monitord || echo "Did not kill cf-monitord"
    pkill -f cf-reactor  || echo "Did not kill cf-reactor"
}

function no_errors {
    set +e
    grep -i "error" $1
    grep -q -i "error" $1 && exit 1
    set -e
}

function check_daemon_output {
    echo "Examining $1:"
    no_errors $1
}

function check_valgrind_output {
    set -e
    if [ ! -f "$1" ]; then
        echo "$1 does not exists!"
        exit 1
    fi
    echo "Looking for problems in $1:"
    grep -i "ERROR SUMMARY: 0 errors" "$1"
    cat $1 | sed -e "/ 0 errors/d" -e "/and suppressed error/d" -e "/a memory error detector/d" -e "/This database contains unknown binary data/d" -e "/error: Problems detected in 1\/1 databases/d" -e "/error: Failed to stat() 'no-such-file.lmdb'/d" -e "/Status of 'no-such-file.lmdb': SYSTEM_ERROR 2/d" > filtered.txt
    no_errors filtered.txt
    set +e
    grep -i "at 0x" filtered.txt
    grep -i -q "at 0x" filtered.txt && exit 1
    grep -i "by 0x" filtered.txt
    grep -i -q "by 0x" filtered.txt && exit 1
    grep -i "Failed to connect" filtered.txt
    grep -i -q "Failed to connect" filtered.txt && exit 1
    set -e
}

function check_masterfiles_and_inputs {
    set -e
    echo "Comparing promises.cf from inputs and masterfiles:"
    diff -u /var/cfengine/inputs/promises.cf /var/cfengine/masterfiles/promises.cf
}

set -e
set -x

# In case things go wrong and this hangs, let's make sure things get properly
# cleaned up from here rather than relying on the outer environment doing the
# cleanup. In case this runs in a container, it could be a big
# difference. Especially in a case of an SPC.
{ sleep 40m && kill -9 $$; } &
auto_destruct_pid=$!
trap "kill $auto_destruct_pid" EXIT

# cf-execd's MailResult connects to localhost:25 whenever a cf-agent run
# produces output. Run smtp-sink (from the postfix package) so the connect
# succeeds and cf-execd's mail code path is exercised under valgrind without
# leaving an "error: Mail report: couldn't connect" line in execd_output.txt.
echo "Starting smtp-sink to absorb cf-execd mail reports"
/usr/sbin/smtp-sink 127.0.0.1:25 1000 &

# Assume we are in core directory
if [ -f ./configure ] ; then
  ./configure -C --enable-debug
else
  ./autogen.sh -C --enable-debug
fi
make -j -l $(getconf _NPROCESSORS_ONLN)
make install

cd ../masterfiles
if [ -f ./configure ] ; then
  ./configure -C --enable-debug
else
  ./autogen.sh -C --enable-debug
fi
make
make install

/var/cfengine/bin/cf-agent --version

VG_OPTS="--leak-check=full --track-origins=yes --error-exitcode=1"
BOOTSTRAP_IP="127.0.0.1"

valgrind $VG_OPTS /var/cfengine/bin/cf-key 2>&1 | tee cf-key.txt
check_valgrind_output cf-key.txt
valgrind $VG_OPTS /var/cfengine/bin/cf-agent -B $BOOTSTRAP_IP 2>&1 | tee bootstrap.txt
check_valgrind_output bootstrap.txt

echo "Running cf-check diagnose --validate on all databases:"
valgrind $VG_OPTS /var/cfengine/bin/cf-check diagnose --validate 2>&1 | tee cf_check_validate_all_1.txt
check_valgrind_output cf_check_validate_all_1.txt

echo "Running cf-check diagnose --validate on non-existent file:"
valgrind $VG_OPTS /var/cfengine/bin/cf-check diagnose --validate no-such-file.lmdb 2>&1 | tee cf_check_validate_non_existent.txt
check_valgrind_output cf_check_validate_non_existent.txt

check_masterfiles_and_inputs

print_ps

echo "Stopping before relaunch under valgrind"
stop_daemons
sleep 10
print_ps

echo "Starting cf-serverd with valgrind in background:"
valgrind $VG_OPTS --log-file=serverd.txt /var/cfengine/bin/cf-serverd --no-fork 2>&1 > serverd_output.txt &
server_pid="$!"

echo "Starting cf-execd with valgrind in background:"
valgrind $VG_OPTS --log-file=execd.txt /var/cfengine/bin/cf-execd --no-fork 2>&1 > execd_output.txt &
exec_pid="$!"

echo "Starting cf-reactor with valgrind in background:"
valgrind $VG_OPTS --log-file=reactor.txt /var/cfengine/bin/cf-reactor --no-fork 2>&1 > reactor_output.txt &
reactor_pid="$!"

print_ps

# cf-serverd running under valgrind can be really slow to start, let's give it
# some time before we move on and potentially hit the wall if it's actually
# malfunctioning
tries=24 # max 4 minutes
while /var/cfengine/bin/cf-net -H $BOOTSTRAP_IP connect | grep Failed; do
  tries=$((tries - 1))
  if [ $tries -le 0 ]; then
    break;
  fi
  sleep 10
done

echo "Running cf-net:"
valgrind $VG_OPTS /var/cfengine/bin/cf-net GET /var/cfengine/masterfiles/promises.cf 2>&1 | tee get.txt
check_valgrind_output get.txt

echo "Checking promises.cf diff (from cf-net GET):"
diff -u ./promises.cf /var/cfengine/masterfiles/promises.cf

echo "Running update.cf:"
valgrind $VG_OPTS /var/cfengine/bin/cf-agent -K -f update.cf 2>&1 | tee update.txt
check_valgrind_output update.txt
check_masterfiles_and_inputs
echo "Running update.cf without local copy:"
valgrind $VG_OPTS /var/cfengine/bin/cf-agent -K -f update.cf -D mpf_skip_local_copy_optimization 2>&1 | tee update2.txt
check_valgrind_output update2.txt
check_masterfiles_and_inputs
echo "Running promises.cf:"
valgrind $VG_OPTS /var/cfengine/bin/cf-agent -K -f promises.cf 2>&1 | tee promises.txt
check_valgrind_output promises.txt

# Dump all databases, use grep to filter the JSON lines
# (optional whitespace then double quote or curly brackets).
# Some of the databases have strings containing "error"
# which check_valgrind_output greps for.
echo "Running cf-check dump:"
valgrind $VG_OPTS /var/cfengine/bin/cf-check dump 2>&1 | grep -E '\s*[{}"]' --invert-match | tee cf_check_dump.txt
check_valgrind_output cf_check_dump.txt

echo "Running cf-check diagnose on all databases"
valgrind $VG_OPTS /var/cfengine/bin/cf-check diagnose 2>&1 | tee cf_check_diagnose.txt
check_valgrind_output cf_check_diagnose.txt

echo "Running cf-check diagnose --validate on all databases:"
valgrind $VG_OPTS /var/cfengine/bin/cf-check diagnose --validate 2>&1 | tee cf_check_validate_all_2.txt
check_valgrind_output cf_check_validate_all_2.txt

echo "Checking that bootstrap ID doesn't change"
/var/cfengine/bin/cf-agent --show-evaluated-vars | grep bootstrap_id > id_a
/var/cfengine/bin/cf-agent -K --show-evaluated-vars | grep bootstrap_id > id_b
cat id_a
diff id_a id_b

echo "Checking that bootstrap ID has expected length"
[ `cat id_a | awk '{print $2}' | wc -c` -eq 41 ]

print_ps

echo "cf-execd outputs:"
tail execd_output.txt
tail execd.txt

echo "cf-serverd outputs:"
tail serverd_output.txt
tail serverd.txt

echo "cf-reactor outputs:"
tail reactor_output.txt
tail reactor.txt

echo "Checking that serverd, execd and reactor PIDs are still correct/alive:"
ps -p $exec_pid
ps -p $server_pid
ps -p $reactor_pid

echo "Killing valgrind cf-execd"
kill $exec_pid
echo "Killing valgrind cf-serverd"
kill $server_pid
echo "Killing valgrind cf-reactor"
kill $reactor_pid

wait $exec_pid
wait $server_pid
wait $reactor_pid

echo "Output from cf-execd in valgrind:"
cat execd.txt
check_valgrind_output execd.txt
check_daemon_output execd_output.txt

echo "Output from cf-serverd in valgrind:"
cat serverd.txt
check_valgrind_output serverd.txt
check_daemon_output serverd_output.txt

echo "Output from cf-reactor valgrind:"
cat reactor.txt
check_valgrind_output reactor.txt
check_daemon_output reactor_output.txt

stop_daemons

echo "Done killing"
sleep 10
print_ps

echo "Check that bootstrap was successful"
grep "This host assumes the role of policy server" bootstrap.txt
grep "completed successfully!" bootstrap.txt

echo "valgrind_health_check successful! (valgrind.sh)"
