Projet

Général

Profil

Paste
Télécharger au format
Statistiques
| Branche: | Révision:

root / plugins / currency / bitcoin / bitcoind_ @ fd45fe6c

Historique | Voir | Annoter | Télécharger (8,04 ko)

1 bc20826c Lars Kruse
#!/usr/bin/env python3
2 3b438369 Lars Kruse
3
"""=cut
4
=head1 NAME
5
6
  bitcoind_ - Track Bitcoin Server Variables
7
8
=head1 CONFIGURATION
9
10
You need to be able to authenticate to the bitcoind server to issue rpc's.
11
This plugin supports two ways to do that:
12
13
1) In /etc/munin/plugin-conf.d/bitcoin.conf place:
14
15
     [bitcoind_*]
16
     user your-username
17 abdeb7ec Lars Kruse
     env.bitcoin_configfile /home/your-username/.bitcoin/bitcoin.conf
18 3b438369 Lars Kruse
19 abdeb7ec Lars Kruse
   Then be sure that the file referenced above (typically: $HOME/.bitcoin/bitcoin.conf)
20
   has the correct authentication info:
21 3b438369 Lars Kruse
       rpcconnect, rpcport, rpcuser, rpcpassword
22
23
2) Place your bitcoind authentication directly in /etc/munin/plugin-conf.d/bitcoin.conf
24
25
     [bitcoind_*]
26
     env.rpcport 8332
27
     env.rpcconnect 127.0.0.1
28
     env.rpcuser your-username-here
29
     env.rpcpassword your-password-here
30
31
To install all available graphs:
32
33
    sudo munin-node-configure --libdir=. --suggest --shell | sudo bash
34
35
Leave out the "| bash" to get a list of commands you can select from to install
36
individual graphs.
37
38
=head1 MAGIC MARKERS
39
40
  #%# family=auto
41
  #%# capabilities=autoconf suggest
42
43
=head1 LICENSE
44
45
MIT License
46
47
=head1 AUTHOR
48
49
Copyright (C) 2012 Mike Koss
50
51
=cut"""
52 5f66fcc3 Mike Koss
53 b6a41513 Lars Kruse
import base64
54 bc20826c Lars Kruse
import json
55 5f66fcc3 Mike Koss
import os
56 bc20826c Lars Kruse
import re
57 5f66fcc3 Mike Koss
import sys
58 4db16377 Mike Koss
import time
59 bc20826c Lars Kruse
import urllib.error
60
import urllib.request
61 5f66fcc3 Mike Koss
62 4db16377 Mike Koss
63 675f1f69 Lars Kruse
DEBUG = os.getenv('MUNIN_DEBUG') == '1'
64 5f66fcc3 Mike Koss
65
66 346aa68d Mike Koss
def main():
67
    # getinfo variable is read from command name - probably the sym-link name.
68 4db16377 Mike Koss
    request_var = sys.argv[0].split('_', 1)[1] or 'balance'
69
    command = sys.argv[1] if len(sys.argv) > 1 else None
70 346aa68d Mike Koss
    request_labels = {'balance': ('Wallet Balance', 'BTC'),
71
                      'connections': ('Peer Connections', 'Connections'),
72 4db16377 Mike Koss
                      'fees': ("Tip Offered", "BTC"),
73
                      'transactions': ("Transactions", "Transactions",
74
                                       ('confirmed', 'waiting')),
75
                      'block_age': ("Last Block Age", "Seconds"),
76 516f91a4 freewil
                      'difficulty': ("Difficulty", ""),
77 346aa68d Mike Koss
                      }
78
    labels = request_labels[request_var]
79 4db16377 Mike Koss
    if len(labels) < 3:
80
        line_labels = [request_var]
81
    else:
82
        line_labels = labels[2]
83 346aa68d Mike Koss
84 4db16377 Mike Koss
    if command == 'suggest':
85 346aa68d Mike Koss
        for var_name in request_labels.keys():
86 fffb536e Lars Kruse
            print(var_name)
87 fd45fe6c Lars Kruse
        return True
88 346aa68d Mike Koss
89 4db16377 Mike Koss
    if command == 'config':
90 fffb536e Lars Kruse
        print('graph_category htc')
91
        print('graph_title Bitcoin %s' % labels[0])
92
        print('graph_vlabel %s' % labels[1])
93 4db16377 Mike Koss
        for label in line_labels:
94 fffb536e Lars Kruse
            print('%s.label %s' % (label, label))
95 fd45fe6c Lars Kruse
        return True
96 346aa68d Mike Koss
97
    # Munin should send connection options via environment vars
98
    bitcoin_options = get_env_options('rpcconnect', 'rpcport', 'rpcuser', 'rpcpassword')
99
    bitcoin_options.rpcconnect = bitcoin_options.get('rpcconnect', '127.0.0.1')
100
    bitcoin_options.rpcport = bitcoin_options.get('rpcport', '8332')
101
102
    if bitcoin_options.get('rpcuser') is None:
103 abdeb7ec Lars Kruse
        conf_file = os.getenv("bitcoin_configfile")
104
        if not conf_file:
105
            print("Missing environment settings (rpcuser/rcpassword or bitcoin_configfile)",
106
                  file=sys.stderr)
107 fd45fe6c Lars Kruse
            return False
108 abdeb7ec Lars Kruse
        elif not os.path.exists(conf_file):
109
            print("Configuration file does not exist: {}".format(conf_file), file=sys.stderr)
110 fd45fe6c Lars Kruse
            return False
111 abdeb7ec Lars Kruse
        else:
112
            bitcoin_options = parse_conf(conf_file)
113 346aa68d Mike Koss
114
    bitcoin_options.require('rpcuser', 'rpcpassword')
115
116
    bitcoin = ServiceProxy('http://%s:%s' % (bitcoin_options.rpcconnect,
117
                                             bitcoin_options.rpcport),
118
                           username=bitcoin_options.rpcuser,
119
                           password=bitcoin_options.rpcpassword)
120
121
    (info, error) = bitcoin.getinfo()
122 4db16377 Mike Koss
123 346aa68d Mike Koss
    if error:
124 4db16377 Mike Koss
        if command == 'autoconf':
125 fffb536e Lars Kruse
            print('no')
126 fd45fe6c Lars Kruse
            return True
127 346aa68d Mike Koss
        else:
128 4db16377 Mike Koss
            # TODO: Better way to report errors to Munin-node.
129 0a090e5b Lars Kruse
            print("Could not connect to Bitcoin server.", file=sys.stderr)
130 fd45fe6c Lars Kruse
            return False
131 346aa68d Mike Koss
132 4db16377 Mike Koss
    if request_var in ('transactions', 'block_age'):
133 0988b353 Michael Gronager
        (info, error) = bitcoin.getblockhash(info['blocks'])
134
        (info, error) = bitcoin.getblock(info)
135
        info['block_age'] = int(time.time()) - info['time']
136
        info['confirmed'] = len(info['tx'])
137 4db16377 Mike Koss
138
    if request_var in ('fees', 'transactions'):
139 0988b353 Michael Gronager
        (memory_pool, error) = bitcoin.getrawmempool()
140 4db16377 Mike Koss
        if memory_pool:
141 0988b353 Michael Gronager
            info['waiting'] = len(memory_pool)
142 4db16377 Mike Koss
143
    if command == 'autoconf':
144 fffb536e Lars Kruse
        print('yes')
145 fd45fe6c Lars Kruse
        return True
146 346aa68d Mike Koss
147 4db16377 Mike Koss
    for label in line_labels:
148 fffb536e Lars Kruse
        print("%s.value %s" % (label, info[label]))
149 346aa68d Mike Koss
150
151 5f66fcc3 Mike Koss
def parse_conf(filename):
152
    """ Bitcoin config file parser. """
153
154
    options = Options()
155
156
    re_line = re.compile(r'^\s*([^#]*)\s*(#.*)?$')
157
    re_setting = re.compile(r'^(.*)\s*=\s*(.*)$')
158
    try:
159
        with open(filename) as file:
160
            for line in file.readlines():
161
                line = re_line.match(line).group(1).strip()
162
                m = re_setting.match(line)
163
                if m is None:
164
                    continue
165
                (var, value) = (m.group(1), m.group(2).strip())
166
                options[var] = value
167 fffb536e Lars Kruse
    except OSError:
168
        # the config file may be missing
169 5f66fcc3 Mike Koss
        pass
170
171
    return options
172
173
174
def get_env_options(*vars):
175
    options = Options()
176
    for var in vars:
177 ea051b7f Lars Kruse
        value = os.getenv(var)
178
        if value is not None:
179
            options[var] = os.getenv(var)
180 5f66fcc3 Mike Koss
    return options
181
182
183
class Options(dict):
184
    """A dict that allows for object-like property access syntax."""
185
    def __getattr__(self, name):
186
        try:
187
            return self[name]
188
        except KeyError:
189
            raise AttributeError(name)
190
191
    def require(self, *names):
192
        missing = []
193
        for name in names:
194
            if self.get(name) is None:
195
                missing.append(name)
196
        if len(missing) > 0:
197 a0aa955a Lars Kruse
            print("Missing required setting%s: %s." % ('s' if len(missing) > 1 else '',
198
                                                       ', '.join(missing)),
199
                  file=sys.stderr)
200 fd45fe6c Lars Kruse
            return False
201 5f66fcc3 Mike Koss
202
203 a0aa955a Lars Kruse
class ServiceProxy:
204 5f66fcc3 Mike Koss
    """
205
    Proxy for a JSON-RPC web service. Calls to a function attribute
206
    generates a JSON-RPC call to the host service. If a callback
207
    keyword arg is included, the call is processed as an asynchronous
208
    request.
209
210
    Each call returns (result, error) tuple.
211
    """
212
    def __init__(self, url, username=None, password=None):
213
        self.url = url
214
        self.id = 0
215
        self.username = username
216
        self.password = password
217
218
    def __getattr__(self, method):
219
        self.id += 1
220
        return Proxy(self, method, id=self.id)
221
222
223 a0aa955a Lars Kruse
class Proxy:
224 5f66fcc3 Mike Koss
    def __init__(self, service, method, id=None):
225
        self.service = service
226
        self.method = method
227
        self.id = id
228
229
    def __call__(self, *args):
230
        if DEBUG:
231
            arg_strings = [json.dumps(arg) for arg in args]
232 fffb536e Lars Kruse
            print("Calling %s(%s) @ %s" % (self.method,
233 5f66fcc3 Mike Koss
                                           ', '.join(arg_strings),
234 fffb536e Lars Kruse
                                           self.service.url))
235 5f66fcc3 Mike Koss
236
        data = {
237
            'method': self.method,
238
            'params': args,
239
            'id': self.id,
240
            }
241 f5de3d19 Lars Kruse
        request = urllib.request.Request(self.service.url, json.dumps(data).encode())
242 5f66fcc3 Mike Koss
        if self.service.username:
243 b6a41513 Lars Kruse
            auth_string = '%s:%s' % (self.service.username, self.service.password)
244
            auth_b64 = base64.urlsafe_b64encode(auth_string.encode()).decode()
245
            request.add_header('Authorization', 'Basic %s' % auth_b64)
246 5f66fcc3 Mike Koss
247
        try:
248 bc20826c Lars Kruse
            body = urllib.request.urlopen(request).read()
249
        except urllib.error.URLError as e:
250 5f66fcc3 Mike Koss
            return (None, e)
251
252
        if DEBUG:
253 fffb536e Lars Kruse
            print('RPC Response (%s): %s' % (self.method, json.dumps(body, indent=4)))
254 5f66fcc3 Mike Koss
255
        try:
256
            data = json.loads(body)
257 fffb536e Lars Kruse
        except ValueError as e:
258 5f66fcc3 Mike Koss
            return (None, e.message)
259
        # TODO: Check that id matches?
260
        return (data['result'], data['error'])
261
262
263 4db16377 Mike Koss
def get_json_url(url):
264 bc20826c Lars Kruse
    request = urllib.request.Request(url)
265
    body = urllib.request.urlopen(request).read()
266 4db16377 Mike Koss
    data = json.loads(body)
267
    return data
268
269
270 5f66fcc3 Mike Koss
if __name__ == "__main__":
271 fd45fe6c Lars Kruse
    sys.exit(0 if main() else 1)