From 5f66fcc3f8695c0a082733aa6c2b3742e0f94020 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Tue, 14 Feb 2012 18:07:54 -0800 Subject: [PATCH 1/4] Bitcoin server plugin --- plugins/other/bitcoind_ | 211 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100755 plugins/other/bitcoind_ diff --git a/plugins/other/bitcoind_ b/plugins/other/bitcoind_ new file mode 100755 index 00000000..5d5ed77a --- /dev/null +++ b/plugins/other/bitcoind_ @@ -0,0 +1,211 @@ +#!/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 re +import urllib2 +import json + +DEBUG = False + + +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))) + + +def main(): + # Info variable is read from command name - probably the sym-link name. + request_var = sys.argv[0].split('_')[1] or 'balance' + request_labels = {'balance': ('Wallet Ballance', 'BTC'), + 'blocks': ('Block Number', 'Blocks'), + 'connections': ('Peer Connections', 'Connections'), + 'difficulty': ('Current Difficulty', 'Difficulty'), + 'errors': ("Errors", 'Errors',) + } + labels = request_labels[request_var] + + if len(sys.argv) > 1 and sys.argv[1] == 'suggest': + for var_name in request_labels.keys(): + print var_name + return + + if len(sys.argv) > 1 and sys.argv[1] == 'config': + print """graph_title Bitcoin %s +graph_category bitcoin +graph_vlabel %s +%s.label %s +""" % (labels[0], labels[1], request_var, request_var) + 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 len(sys.argv) > 1 and sys.argv[1] == 'autoconf': + print 'no' + return + else: + raise ValueError("Could not connect to Bitcoin server.") + + if len(sys.argv) > 1 and sys.argv[1] == 'autoconf': + print 'yes' + return + + print "%s.value %s" % (request_var, info[request_var]) + + +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']) + + +if __name__ == "__main__": + main() From 346aa68d48c7e692ec3184081faaa68498bdc808 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Tue, 14 Feb 2012 20:12:03 -0800 Subject: [PATCH 2/4] reorder code - fix limits --- plugins/other/bitcoind_ | 111 ++++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 55 deletions(-) diff --git a/plugins/other/bitcoind_ b/plugins/other/bitcoind_ index 5d5ed77a..57207310 100755 --- a/plugins/other/bitcoind_ +++ b/plugins/other/bitcoind_ @@ -44,6 +44,62 @@ 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] or 'balance' + request_labels = {'balance': ('Wallet Balance', 'BTC'), + 'blocks': ('Block Number', 'Blocks', 160000), + 'connections': ('Peer Connections', 'Connections'), + 'difficulty': ('Current Difficulty', 'Difficulty', 1000000), + 'errors': ("Errors", 'Errors',) + } + labels = request_labels[request_var] + + if len(sys.argv) > 1 and sys.argv[1] == 'suggest': + for var_name in request_labels.keys(): + print var_name + return + + if len(sys.argv) > 1 and sys.argv[1] == 'config': + print 'graph_category bitcoin' + print 'graph_title Bitcoin %s' % labels[0] + print 'graph_vlabel %s' % labels[1] + print '%s.label %s' % (request_var, request_var) + if len(labels) >= 3: + print "graph_args --lower-limit %d" % labels[2] + 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 len(sys.argv) > 1 and sys.argv[1] == 'autoconf': + print 'no' + return + else: + raise ValueError("Could not connect to Bitcoin server.") + + if len(sys.argv) > 1 and sys.argv[1] == 'autoconf': + print 'yes' + return + + print "%s.value %s" % (request_var, info[request_var]) + + def parse_conf(filename): """ Bitcoin config file parser. """ @@ -92,61 +148,6 @@ class Options(dict): ', '.join(missing))) -def main(): - # Info variable is read from command name - probably the sym-link name. - request_var = sys.argv[0].split('_')[1] or 'balance' - request_labels = {'balance': ('Wallet Ballance', 'BTC'), - 'blocks': ('Block Number', 'Blocks'), - 'connections': ('Peer Connections', 'Connections'), - 'difficulty': ('Current Difficulty', 'Difficulty'), - 'errors': ("Errors", 'Errors',) - } - labels = request_labels[request_var] - - if len(sys.argv) > 1 and sys.argv[1] == 'suggest': - for var_name in request_labels.keys(): - print var_name - return - - if len(sys.argv) > 1 and sys.argv[1] == 'config': - print """graph_title Bitcoin %s -graph_category bitcoin -graph_vlabel %s -%s.label %s -""" % (labels[0], labels[1], request_var, request_var) - 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 len(sys.argv) > 1 and sys.argv[1] == 'autoconf': - print 'no' - return - else: - raise ValueError("Could not connect to Bitcoin server.") - - if len(sys.argv) > 1 and sys.argv[1] == 'autoconf': - print 'yes' - return - - print "%s.value %s" % (request_var, info[request_var]) - - class ServiceProxy(object): """ Proxy for a JSON-RPC web service. Calls to a function attribute From c495be36aa4744e19aa6f0f036a9d115a1b40f66 Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Wed, 15 Feb 2012 01:38:13 -0800 Subject: [PATCH 3/4] Fix y-axis pinned to zero - Munin bug workaround --- plugins/other/bitcoind_ | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/other/bitcoind_ b/plugins/other/bitcoind_ index 57207310..7e0ac471 100755 --- a/plugins/other/bitcoind_ +++ b/plugins/other/bitcoind_ @@ -65,8 +65,11 @@ def main(): print 'graph_title Bitcoin %s' % labels[0] print 'graph_vlabel %s' % labels[1] print '%s.label %s' % (request_var, request_var) + # Work-around for Munin bug - extra black line at origin that pins + # y-axis to zero instead of auto-range. + print 'dummy.label dummy' if len(labels) >= 3: - print "graph_args --lower-limit %d" % labels[2] + print 'graph_args --lower-limit %d' % labels[2] return # Munin should send connection options via environment vars From 4db16377074eedc55f6872c818a9d41105535a0f Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Mon, 20 Feb 2012 01:18:47 -0800 Subject: [PATCH 4/4] add fees, transactions, age and remove blocks and difficulty --- plugins/other/bitcoind_ | 57 ++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/plugins/other/bitcoind_ b/plugins/other/bitcoind_ index 7e0ac471..bf75a589 100755 --- a/plugins/other/bitcoind_ +++ b/plugins/other/bitcoind_ @@ -37,39 +37,43 @@ 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] or 'balance' + 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'), - 'blocks': ('Block Number', 'Blocks', 160000), 'connections': ('Peer Connections', 'Connections'), - 'difficulty': ('Current Difficulty', 'Difficulty', 1000000), - 'errors': ("Errors", 'Errors',) + '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 len(sys.argv) > 1 and sys.argv[1] == 'suggest': + if command == 'suggest': for var_name in request_labels.keys(): print var_name return - if len(sys.argv) > 1 and sys.argv[1] == 'config': + if command == 'config': print 'graph_category bitcoin' print 'graph_title Bitcoin %s' % labels[0] print 'graph_vlabel %s' % labels[1] - print '%s.label %s' % (request_var, request_var) - # Work-around for Munin bug - extra black line at origin that pins - # y-axis to zero instead of auto-range. - print 'dummy.label dummy' - if len(labels) >= 3: - print 'graph_args --lower-limit %d' % labels[2] + for label in line_labels: + print '%s.label %s' % (label, label) return # Munin should send connection options via environment vars @@ -89,18 +93,34 @@ def main(): password=bitcoin_options.rpcpassword) (info, error) = bitcoin.getinfo() + if error: - if len(sys.argv) > 1 and sys.argv[1] == 'autoconf': + 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 len(sys.argv) > 1 and sys.argv[1] == 'autoconf': + 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 - print "%s.value %s" % (request_var, info[request_var]) + for label in line_labels: + print "%s.value %s" % (label, info[label]) def parse_conf(filename): @@ -211,5 +231,12 @@ class Proxy(object): 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()