#-*- coding: ISO-8859-1 -*-
# collect.py: the python collectd-unixsock module.
#
# Requires collectd to be configured with the unixsock plugin, like so:
#
# LoadPlugin unixsock
# <Plugin unixsock>
#   SocketFile "/var/run/collectd-unixsock"
#   SocketPerms "0775"
# </Plugin>
#
# Copyright (C) 2008 Clay Loveless <clay@killersoft.com>
#
# This software is provided 'as-is', without any express or implied
# warranty.  In no event will the author be held liable for any damages
# arising from the use of this software.
#
# Permission is granted to anyone to use this software for any purpose,
# including commercial applications, and to alter it and redistribute it
# freely, subject to the following restrictions:
#
# 1. The origin of this software must not be misrepresented; you must not
#    claim that you wrote the original software. If you use this software
#    in a product, an acknowledgment in the product documentation would be
#    appreciated but is not required.
# 2. Altered source versions must be plainly marked as such, and must not be
#    misrepresented as being the original software.
# 3. This notice may not be removed or altered from any source distribution.

import socket
import sys


class Collectd():

    def __init__(self, path='/var/run/collectd-unixsock', noisy=False):
        self.noisy = noisy
        self.path = path
        self._sock = self._connect()

    def flush(self, timeout=None, plugins=[], identifiers=[]):
        """Send a FLUSH command.

        Full documentation:
            http://collectd.org/wiki/index.php/Plain_text_protocol#FLUSH

        """
        # have to pass at least one plugin or identifier
        if not plugins and not identifiers:
            return None
        args = []
        if timeout:
            args.append("timeout=%s" % timeout)
        if plugins:
            plugin_args = map(lambda x: "plugin=%s" % x, plugins)
            args.extend(plugin_args)
        if identifiers:
            identifier_args = map(lambda x: "identifier=%s" % x, identifiers)
            args.extend(identifier_args)
        return self._cmd('FLUSH %s' % ' '.join(args))

    def getthreshold(self, identifier):
        """Send a GETTHRESHOLD command.

        Full documentation:
            http://collectd.org/wiki/index.php/Plain_text_protocol#GETTHRESHOLD

        """
        numvalues = self._cmd('GETTHRESHOLD "%s"' % identifier)
        lines = []
        if not numvalues or numvalues < 0:
            raise KeyError("Identifier '%s' not found" % identifier)
        lines = self._readlines(numvalues)
        return lines

    def getval(self, identifier, flush_after=True):
        """Send a GETVAL command.

        Also flushes the identifier if flush_after is True.

        Full documentation:
            http://collectd.org/wiki/index.php/Plain_text_protocol#GETVAL

        """
        numvalues = self._cmd('GETVAL "%s"' % identifier)
        lines = []
        if not numvalues or numvalues < 0:
            raise KeyError("Identifier '%s' not found" % identifier)
        lines = self._readlines(numvalues)
        if flush_after:
            self.flush(identifiers=[identifier])
        return lines

    def listval(self):
        """Send a LISTVAL command.

        Full documentation:
            http://collectd.org/wiki/index.php/Plain_text_protocol#LISTVAL

        """
        numvalues = self._cmd('LISTVAL')
        lines = []
        if numvalues:
            lines = self._readlines(numvalues)
        return lines

    def putnotif(self, message, options={}):
        """Send a PUTNOTIF command.

        Options must be passed as a Python dictionary. Example:
          options={'severity': 'failure', 'host': 'example.com'}

        Full documentation:
            http://collectd.org/wiki/index.php/Plain_text_protocol#PUTNOTIF

        """
        args = []
        if options:
            options_args = map(lambda x: "%s=%s" % (x, options[x]), options)
            args.extend(options_args)
        args.append('message="%s"' % message)
        return self._cmd('PUTNOTIF %s' % ' '.join(args))

    def putval(self, identifier, values, options={}):
        """Send a PUTVAL command.

        Options must be passed as a Python dictionary. Example:
          options={'interval': 10}

        Full documentation:
            http://collectd.org/wiki/index.php/Plain_text_protocol#PUTVAL

        """
        args = []
        args.append('"%s"' % identifier)
        if options:
            options_args = map(lambda x: "%s=%s" % (x, options[x]), options)
            args.extend(options_args)
        values = map(str, values)
        args.append(':'.join(values))
        return self._cmd('PUTVAL %s' % ' '.join(args))

    def _cmd(self, c):
        try:
            return self._cmdattempt(c)
        except socket.error, (errno, errstr):
            sys.stderr.write("[error] Sending to socket failed: [%d] %s\n"
                             % (errno, errstr))
            self._sock = self._connect()
            return self._cmdattempt(c)

    def _cmdattempt(self, c):
        if self.noisy:
            print "[send] %s" % c
        if not self._sock:
            sys.stderr.write("[error] Socket unavailable. Can not send.")
            return False
        self._sock.send(c + "\n")
        status_message = self._readline()
        if self.noisy:
            print "[receive] %s" % status_message
        if not status_message:
            return None
        code, message = status_message.split(' ', 1)
        if int(code):
            return int(code)
        return False

    def _connect(self):
        try:
            sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            sock.connect(self.path)
            if self.noisy:
                print "[socket] connected to %s" % self.path
            return sock
        except socket.error, (errno, errstr):
            sys.stderr.write("[error] Connecting to socket failed: [%d] %s"
                             % (errno, errstr))
            return None

    def _readline(self):
        """Read single line from socket"""
        if not self._sock:
            sys.stderr.write("[error] Socket unavailable. Can not read.")
            return None
        try:
            data = ''
            buf = []
            recv = self._sock.recv
            while data != "\n":
                data = recv(1)
                if not data:
                    break
                if data != "\n":
                    buf.append(data)
            return ''.join(buf)
        except socket.error, (errno, errstr):
            sys.stderr.write("[error] Reading from socket failed: [%d] %s"
                             % (errno, errstr))
            self._sock = self._connect()
            return None

    def _readlines(self, sizehint=0):
        """Read multiple lines from socket"""
        total = 0
        list = []
        while True:
            line = self._readline()
            if not line:
                break
            list.append(line)
            total = len(list)
            if sizehint and total >= sizehint:
                break
        return list

    def __del__(self):
        if not self._sock:
            return
        try:
            self._sock.close()
        except socket.error, (errno, errstr):
            sys.stderr.write("[error] Closing socket failed: [%d] %s"
                             % (errno, errstr))


if __name__ == '__main__':
    """Collect values from socket and dump to STDOUT"""

    c = Collectd('/var/run/collectd-unixsock', noisy=True)
    list = c.listval()
    for val in list:
        stamp, identifier = val.split()
        print "\n%s" % identifier
        print "\tUpdate time: %s" % stamp

        values = c.getval(identifier)
        print "\tValue list: %s" % ', '.join(values)

        # don't fetch thresholds by default because collectd will crash
        # if there is no treshold for the given identifier
        #thresholds = c.getthreshold(identifier)
        #print "\tThresholds: %s" % ', '.join(thresholds)
