#!/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"), 'difficulty': ("Difficulty", ""), } 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 htc' 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'): (info, error) = bitcoin.getblockhash(info['blocks']) (info, error) = bitcoin.getblock(info) info['block_age'] = int(time.time()) - info['time'] info['confirmed'] = len(info['tx']) if request_var in ('fees', 'transactions'): (memory_pool, error) = bitcoin.getrawmempool() if memory_pool: info['waiting'] = len(memory_pool) 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()