From 6cd082a78cef82aeaab24ac3b3d8f6354861d22e Mon Sep 17 00:00:00 2001 From: Mike Koss Date: Tue, 14 Feb 2012 20:21:10 -0800 Subject: [PATCH] Bitcoind Multi-Plugin --- plugins/other/bitcoind_ | 212 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100755 plugins/other/bitcoind_ diff --git a/plugins/other/bitcoind_ b/plugins/other/bitcoind_ new file mode 100755 index 00000000..57207310 --- /dev/null +++ b/plugins/other/bitcoind_ @@ -0,0 +1,212 @@ +#!/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 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. """ + + 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']) + + +if __name__ == "__main__": + main()