#!/usr/bin/env python
# bitcoind_ Munin plugin for Bitcoin Server Variables
#
# by Mike Koss
# Feb 14, 2012, MIT License
#
# You need to be able to authenticate to the bitcoind server to issue rpc's.
# This plugin supporst 2 ways to do that:
#
# 1) In /etc/munin/plugin-conf.d/bitcoin.conf place:
#
#      [bitcoind_*]
#      user your-username
#
#    Then be sure your $HOME/.bitcoin/bitcoin.conf has the correct authentication info:
#        rpcconnect, rpcport, rpcuser, rpcpassword
#
# 2) Place your bitcoind authentication directly in /etc/munin/plugin-conf.d/bitcoin.conf
#
#      [bitcoind_*]
#      env.rpcport 8332
#      env.rpcconnect 127.0.0.1
#      env.rpcuser your-username-here
#      env.rpcpassword your-password-here
#
# To install all available graphs:
#
#     sudo munin-node-configure --libdir=. --suggest --shell | sudo bash
#
# Leave out the "| bash" to get a list of commands you can select from to install
# individual graphs.
#
# Munin plugin tags:
#
#%# family=auto
#%# capabilities=autoconf suggest

import os
import sys
import time
import re
import urllib2
import json


DEBUG = False


def main():
    # getinfo variable is read from command name - probably the sym-link name.
    request_var = sys.argv[0].split('_', 1)[1] or 'balance'
    command = sys.argv[1] if len(sys.argv) > 1 else None
    request_labels = {'balance': ('Wallet Balance', 'BTC'),
                      'connections': ('Peer Connections', 'Connections'),
                      'fees': ("Tip Offered", "BTC"),
                      'transactions': ("Transactions", "Transactions",
                                       ('confirmed', 'waiting')),
                      'block_age': ("Last Block Age", "Seconds"),
                      }
    labels = request_labels[request_var]
    if len(labels) < 3:
        line_labels = [request_var]
    else:
        line_labels = labels[2]

    if command == 'suggest':
        for var_name in request_labels.keys():
            print var_name
        return

    if command == 'config':
        print 'graph_category bitcoin'
        print 'graph_title Bitcoin %s' % labels[0]
        print 'graph_vlabel %s' % labels[1]
        for label in line_labels:
            print '%s.label %s' % (label, label)
        return

    # Munin should send connection options via environment vars
    bitcoin_options = get_env_options('rpcconnect', 'rpcport', 'rpcuser', 'rpcpassword')
    bitcoin_options.rpcconnect = bitcoin_options.get('rpcconnect', '127.0.0.1')
    bitcoin_options.rpcport = bitcoin_options.get('rpcport', '8332')

    if bitcoin_options.get('rpcuser') is None:
        conf_file = os.path.join(os.path.expanduser('~/.bitcoin'), 'bitcoin.conf')
        bitcoin_options = parse_conf(conf_file)

    bitcoin_options.require('rpcuser', 'rpcpassword')

    bitcoin = ServiceProxy('http://%s:%s' % (bitcoin_options.rpcconnect,
                                             bitcoin_options.rpcport),
                           username=bitcoin_options.rpcuser,
                           password=bitcoin_options.rpcpassword)

    (info, error) = bitcoin.getinfo()

    if error:
        if command == 'autoconf':
            print 'no'
            return
        else:
            # TODO: Better way to report errors to Munin-node.
            raise ValueError("Could not connect to Bitcoin server.")

    if request_var in ('transactions', 'block_age'):
        block_info = get_json_url('http://blockchain.info/block-height/%d?format=json' %
                                  info['blocks'])
        last_block = block_info['blocks'][0]
        info['block_age'] = int(time.time()) - last_block['time']
        info['confirmed'] = len(last_block['tx'])

    if request_var in ('fees', 'transactions'):
        (memory_pool, error) = bitcoin.getmemorypool()
        if memory_pool:
            info['fees'] = float(memory_pool['coinbasevalue']) / 1e8 - 50.0
            info['waiting'] = len(memory_pool['transactions'])

    if command == 'autoconf':
        print 'yes'
        return

    for label in line_labels:
        print "%s.value %s" % (label, info[label])


def parse_conf(filename):
    """ Bitcoin config file parser. """

    options = Options()

    re_line = re.compile(r'^\s*([^#]*)\s*(#.*)?$')
    re_setting = re.compile(r'^(.*)\s*=\s*(.*)$')
    try:
        with open(filename) as file:
            for line in file.readlines():
                line = re_line.match(line).group(1).strip()
                m = re_setting.match(line)
                if m is None:
                    continue
                (var, value) = (m.group(1), m.group(2).strip())
                options[var] = value
    except:
        pass

    return options


def get_env_options(*vars):
    options = Options()
    for var in vars:
        options[var] = os.getenv(var)
    return options


class Options(dict):
    """A dict that allows for object-like property access syntax."""
    def __getattr__(self, name):
        try:
            return self[name]
        except KeyError:
            raise AttributeError(name)

    def require(self, *names):
        missing = []
        for name in names:
            if self.get(name) is None:
                missing.append(name)
        if len(missing) > 0:
            raise ValueError("Missing required setting%s: %s." %
                             ('s' if len(missing) > 1 else '',
                              ', '.join(missing)))


class ServiceProxy(object):
    """
    Proxy for a JSON-RPC web service. Calls to a function attribute
    generates a JSON-RPC call to the host service. If a callback
    keyword arg is included, the call is processed as an asynchronous
    request.

    Each call returns (result, error) tuple.
    """
    def __init__(self, url, username=None, password=None):
        self.url = url
        self.id = 0
        self.username = username
        self.password = password

    def __getattr__(self, method):
        self.id += 1
        return Proxy(self, method, id=self.id)


class Proxy(object):
    def __init__(self, service, method, id=None):
        self.service = service
        self.method = method
        self.id = id

    def __call__(self, *args):
        if DEBUG:
            arg_strings = [json.dumps(arg) for arg in args]
            print "Calling %s(%s) @ %s" % (self.method,
                                           ', '.join(arg_strings),
                                           self.service.url)

        data = {
            'method': self.method,
            'params': args,
            'id': self.id,
            }
        request = urllib2.Request(self.service.url, json.dumps(data))
        if self.service.username:
            # Strip the newline from the b64 encoding!
            b64 = ('%s:%s' % (self.service.username, self.service.password)).encode('base64')[:-1]
            request.add_header('Authorization', 'Basic %s' % b64)

        try:
            body = urllib2.urlopen(request).read()
        except Exception, e:
            return (None, e)

        if DEBUG:
            print 'RPC Response (%s): %s' % (self.method, json.dumps(body, indent=4))

        try:
            data = json.loads(body)
        except ValueError, e:
            return (None, e.message)
        # TODO: Check that id matches?
        return (data['result'], data['error'])


def get_json_url(url):
    request = urllib2.Request(url)
    body = urllib2.urlopen(request).read()
    data = json.loads(body)
    return data


if __name__ == "__main__":
    main()
