Lampengeist

Our research and development department uses a little tool called “Lampengeist” for indicating if a build on the CI was successful or not. All we needed to implement this feature is the Jenkins plugin Hudson Post build task and a small shell script – and of course a programmable power connector. We choose the energenie EG-PMS-LAN by gembird because it is manageable via network.

Copy script

The script is very simple. It only needs the tools curl and logger. Attention: This script uses advanced scripting functions of the Bash shell. Maybe it will not work with other Shells – e.g. dash. Take the script below and copy it into directory /usr/local/bin/ on your build server.

#!/bin/bash

# This script toggles a socket on
# "energenie LAN programmable power connector"

typeset -a connectors=( pms.host.lan )
typeset -a passwords=( p4ssw0rd )
typeset toggle="" state_dir="/var/tmp" lock_file="/var/lock/toggle_lamp"
typeset -i i=0 socket=1

typeset -r connectors passwords state_dir

while [ -e ${lock_file} ]; do
    if [ $i -gt 10 ]; then
        logger -p user.error -t `basename $0` -s -- "Could not execute - lock file detected."
        echo "Please contact administrator if problem exists for longer time." >&2
        exit 3
    fi
    i=`expr $i + 1`
    sleep 2
done

touch $lock_file

################# FUNCTIONS ###################

usage() {
    cat << EOF
You called ${0} with unsupported option(s).
Usage: ${0} [1|2|3|4] <on|off>
Numbers 1 to 4 stands for the socket number. If no socket is given, it will
toggle socket 1 per default.
Please try again.
EOF
}

get_states() {
# get states of sockets
    if [ $# -ne 1 ]; then
        return 1
    else
        srv=$1
    fi
    states=( $(curl -f http://${srv}/status.html 2>/dev/null | sed -r "s/(.*)((ctl.*)([0|1]),([0|1]),([0|1]),([0|1]))(.*)/\4 \5 \6 \7/") )
}

toggle() {
    local server="" str_state=""
    local -i i=0 state sckt

    if [ $# -ne 3 ]; then
        return 1
    fi

    while [ $# -gt 0 ]; do
        case $1 in
            1|2|3|4)
                sckt=${1}
                shift
                ;;
            "on")
                str_state="${1}"
                state=1
                shift
                ;;
            "off")
                str_state="${1}"
                state=0
                shift
                ;;
            *)
                server="${1}"
                shift
                ;;
        esac
    done

    # poll status and toggle only if needed
    get_states ${server}
    if [ ${state} -ne ${states[$( expr ${sckt} - 1 )]} ]; then
        curl -f -d "ctl${sckt}=${state}" http://${server}/ &>/dev/null
        logger -p user.info -t `basename $0` -- "state of ${server} socket ${sckt} toggled ${str_state} by ${LOGNAME}"
    fi
}

persist() {
# for cron job use only
# saves state of sockets

    local state_file
    local -i i=0 j=0
    while [ ${i} -lt ${#connectors[*]} ]; do
        state_file=${state_dir}/${connectors[$i]}

        if (curl -f -d "pw=${passwords[$i]}" http://${connectors[$i]}/login.html 2>/dev/null | grep -q 'status.html'); then
            logger -p user.info -t `basename $0` -- "Save states of ${connectors[$i]} to file ${state_file}"
            # get states
            get_states ${connectors[$i]}
            echo "SavedStates=( ${states[*]} )" > ${state_file}

            j=0
            while [ $j -lt ${#states[*]} ]; do
                j=`expr ${j} + 1`
                toggle ${j} off ${connectors[$i]}
                sleep 1
            done

            curl -f http://${connectors[$i]}/login.html &>/dev/null
            logger -p user.info -t `basename $0` -- "States saved and all sockets switched off"
        else
            logger -p user.error -t `basename $0` -s -- "Login to ${connectors[$i]} failed."
        fi

        i=`expr ${i} + 1`
        typeset +r state_file
    done
}

recover() {
# recovers states from state file

    local state_file new_state
    local -a SavedStates
    local -i i=0 j=0

    i=0
    while [ ${i} -lt ${#connectors[*]} ]; do
        typeset -r state_file=${state_dir}/${connectors[$i]}

        if [ -r ${state_file} ]; then

            source ${state_file}
            if (curl -f -d "pw=${passwords[$i]}" http://${connectors[$i]}/login.html 2>/dev/null | grep -q 'status.html'); then

                logger -p user.info -t `basename $0` -- "Restore socket states from ${state_file} to ${connectors[$i]}"
                j=0
                while [ ${j} -lt ${#SavedStates[*]} ]; do
                    if [ ${SavedStates[$j]} -eq 0 ]; then
                        new_state="off"
                    else
                        new_state="on"
                    fi
                    j=`expr ${j} + 1`
                    toggle ${j} ${new_state} ${connectors[$i]}
                    sleep 1
                done

                curl -f http://${connectors[$i]}/login.html &>/dev/null
                logger -p user.info -t `basename $0` -- "Socket states restored and switched on if needed."
            else
                logger -p user.error -t `basename $0` -s -- "Login to ${connectors[$i]} failed."
            fi
            rm ${state_file}

        else
            logger -p user.error -t `basename $0` -s -- "Could not read file ${state_file}"
        fi

        i=`expr ${i} + 1`
    done
}

common() {
# common mode

local -i i=0

while  [ ${i} -lt ${#connectors[*]} ]; do
    state_file=${state_dir}/${connectors[$i]}
    if [ -e ${state_file} ]; then
        # state file exists -> do not toggle life, change in state file only
        if [ ${new_state} = "on" ]; then
            new_state=1
        elif [ ${new_state} = "off" ]; then
            new_state=0
        fi
        socket=`expr ${socket} - 1`

        source $state_file
        if [ ${SavedStates[${socket}]} -ne ${new_state} ]; then
            SavedStates[${socket}]=$new_state
            echo "SavedStates=( ${SavedStates[*]} )" > ${state_file}
            logger -p user.info -t `basename $0` -- "Toggled state of socket ${socket} to ${new_state} in state file by ${LOGNAME}"
        fi

    else
        if (curl -f -d "pw=${passwords[$i]}" http://${connectors[$i]}/login.html 2>/dev/null | grep -q 'status.html'); then
            toggle ${socket} ${new_state} ${connectors[$i]}
#            curl -f -d "ctl${socket}=${new_state}" http://${connectors[$i]}/ &>/dev/null
            curl -f http://${connectors[$i]}/login.html &>/dev/null
        else
            logger -p user.error -t `basename $0` -s -- "Login to ${connectors[$i]} failed."
        fi
    fi
    i=$( expr $i + 1 )
done

}
############# END FUNCTIONS ##################

typeset -r curl_bin="$(which curl | head -n 1)"

if [ -z "${curl_bin}" ]; then
    echo "Tool curl not found. Please install it."
    exit 1
fi

if [ $# -lt 1 ]; then
    echo "No action provided. What should I do?"
    exit 1
fi

while [ $# -ge 1 ]; do
    case ${1} in
        "on"|"off")
            new_state=${1}
            mode="common"
            ;;
        1|2|3|4)
            socket=${1}
            mode="common"
            ;;
        "-r"|"--recover")
            mode="recover"
            ;;
        "-s"|"--save")
            mode="save"
            ;;
        *)
            usage
            rm $lock_file && exit 2
            ;;
    esac

    shift
done

case ${mode} in
    "recover")
        recover
        ;;
    "save")
        persist
        ;;
    "common")
        common
        ;;
esac

rm $lock_file && exit 0 || exit 1

You can entitle the file as you want. But the file name of the script will be taken as “tag” in the logger utility. This means the file name will be posted at the 4th field in syslog file. The name of the user executing the script will posted into syslog, too. And finally the action – toggle socket X on|off – will posted to syslog file. Messages like these will posted to your system log file:

Mar 19 15:00:13 hostname toggle_lamp: state of pms.host.lan socket 1 toggled on by jenkins
Mar 19 15:19:08 hostname toggle_lamp: state of pms.host.lan socket 1 toggled off by jenkins

After copying the script onto the build server, you should make it executable (for jenkins). Go to /usr/local/bin and execute chmod 755 toggle_lamp. Edit file and change pms.host.lan and p4ssw0rd to the hostname or IP and password of your manageable power connector.

Configure post build task

Now you can configure your project for switching sockets on or off. Go to the Jenkins start page and choose the project/job which should toggle your lamp. Click the link configure and scroll to the end of configure page. In section Post-Build-Actions you will find a new option called Post build task. Activate this option and it will expand. The Hudson post build task plugin will scan the log file generated by Jenkins.

In our environment we are scanning the build log for ABORTED, BUILD SECCESSFUL and BUILD FAILED. If the build successfully finished we call /usr/local/bin/toggle_lamp 1 off to switch off the lamp. And we call /usr/local/bin/toggle_lamp on if build failed or was aborted. Scroll to the end of the page and click the button Save if you have defined your tasks.

Power-saving cron jobs

The script posted above has implemented an advanced feature. You can create a cron job which will scan the state of all sockets of your power management socket, persist the state in a file and then switch off all sockets. In the morning a second cron job will read the state file and restore the state of the sockets. The user calling the script via cron will need write permissions (create and delete files) on directory /var/tmp/.

Examples:

# save state of manageable power connector and switch off all sockets
30 20 * * 1-5	test -x /usr/local/bin/toggle_lamp && /usr/local/bin/toggle_lamp --save
# restore state of manageable power connector
30 06 * * 1-5	test -x /usr/local/bin/toggle_lamp && /usr/local/bin/toggle_lamp --recover

The cron jobs above will save the states to file at 20:30 each day from Monday until Friday. On Saturday and Sunday all sockets will be disabled. The saved states will be restored to power connector sockets at 06:30 in the morning.

Advertisements

2 thoughts on “Lampengeist

  1. I once built something similar, yet more complex:

    a own plugin in Hudson which send udp packets with the status of the build – obviously this was written in java.

    the client side was a tcl script which then took the udp packet and would, based on the project that failed, set a certain colour on an RGB LED. The controller for the RGB LED was connected via udp and was giving various amounts of power to the RGB LED. If multiple projects had failed, the lamp would loop through the colours every few seconds.

    The network communication allowed the lamp to be located anywhere in the offices.

    Pictures of the result can be found at http://pub.dihedral.de/images/bubbles/

    The bubbles were triggered with a simple controllable plug interface.

  2. Great! Simple and effective, and just what I was looking for! I have for the past five years used the old USB version of this Gembird device http://www.gmb-online.nl/item_view.aspx?id=2755 . Since I have moved around between several projects, departments and companies during this period, I made a more loose coupling to the Hudson/Jenkins CI. I use the RSS feed that is published by the Hudson/Jenkins.

    This means that it is fairly non-intrusive and that can be an advantage if you do not have access or in control of the actual Hudson/Jenkins server. Maybe on larger projects, the people responsible for your source code repository and continuous integration setup are slightly conservative (sometimes rightfully so…) or do not share your ‘agile’ thoughts of installing a plugin in software in ‘their’ managed environment. Maybe you have never experienced this, and don’t know what I’m talking about, and then just consider yourself lucky :-). Anyway, back to what it is all about – a simple, elegant and effective solution to propagate awareness and fast feedback to the people who need to know the current state of the CI…!

    The downside of my current/old solution with the USB device is that I need a small computer to manage it (monitor the RSS feed, issue the switching commands and turning it on and of on workdays). Although I now have a RaspberryPi to manage it, it doesn’t really scale to my current needs. We now have developers scattered over 5-6 offices, and I decided it was time to upgrade to a network-based device, that would allow us to easily plugin new devices in offices whenever we need to. My only concern about the EG-PMS-LAN device was weather or not it would be possible to communicate with it through some API/scripting interface. And then I found your blog entry where you elegantly show how it done – thanks a lot for posting it!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s