Projet

Général

Profil

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

root / plugins / currency / bitcoin / bitcoind_ @ 9a2a8138

Historique | Voir | Annoter | Télécharger (9,58 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 9a2a8138 Vincas Dargis
def _get_version(info):
67
    # v0.15.2 version is represented as 150200
68
    return info['version'] // 10000
69
70
71
def _rpc_get_initial_info(connection):
72
    (info, connect_error) = connection.getnetworkinfo()
73
    if connect_error:
74
        if isinstance(connect_error, urllib.error.HTTPError) and connect_error.code == 404:
75
            # getinfo RPC exists in version <= 0.15
76
            (info, connect_error) = connection.getinfo()
77
            if connect_error:
78
                return (None, None, connect_error)
79
        else:
80
            return (None, None, connect_error)  # pass all other not-404 errors
81
82
    return (info, _get_version(info), None)
83
84
85
def _rpc_get_balance(info, minor_version, connection):
86
    # see https://github.com/bitcoin/bitcoin/blob/239d199667888e5d60309f15a38eed4d3afe56c4/
87
    # doc/release-notes/release-notes-0.19.0.1.md#new-rpcs
88
    if minor_version >= 19:
89
        # we use getbalance*s* (plural) as old getbalance is being deprecated,
90
        # and we have to calculate total balance (owned and watch-only) manually now.
91
        (result, error) = connection.getbalances()
92
93
        total = sum(result[wallet_mode]['trusted']
94
                    for wallet_mode in ('mine', 'watchonly')
95
                    if wallet_mode in result)
96
97
        info['balance'] = total
98
        return info
99
    else:
100
        (result, error) = connection.getbalance()
101
        info['balance'] = result
102
        return info
103
104
105 346aa68d Mike Koss
def main():
106
    # getinfo variable is read from command name - probably the sym-link name.
107 4db16377 Mike Koss
    request_var = sys.argv[0].split('_', 1)[1] or 'balance'
108
    command = sys.argv[1] if len(sys.argv) > 1 else None
109 346aa68d Mike Koss
    request_labels = {'balance': ('Wallet Balance', 'BTC'),
110
                      'connections': ('Peer Connections', 'Connections'),
111 4db16377 Mike Koss
                      'transactions': ("Transactions", "Transactions",
112
                                       ('confirmed', 'waiting')),
113
                      'block_age': ("Last Block Age", "Seconds"),
114 516f91a4 freewil
                      'difficulty': ("Difficulty", ""),
115 346aa68d Mike Koss
                      }
116
    labels = request_labels[request_var]
117 4db16377 Mike Koss
    if len(labels) < 3:
118
        line_labels = [request_var]
119
    else:
120
        line_labels = labels[2]
121 346aa68d Mike Koss
122 4db16377 Mike Koss
    if command == 'suggest':
123 346aa68d Mike Koss
        for var_name in request_labels.keys():
124 fffb536e Lars Kruse
            print(var_name)
125 fd45fe6c Lars Kruse
        return True
126 346aa68d Mike Koss
127 4db16377 Mike Koss
    if command == 'config':
128 fffb536e Lars Kruse
        print('graph_category htc')
129
        print('graph_title Bitcoin %s' % labels[0])
130
        print('graph_vlabel %s' % labels[1])
131 4db16377 Mike Koss
        for label in line_labels:
132 fffb536e Lars Kruse
            print('%s.label %s' % (label, label))
133 fd45fe6c Lars Kruse
        return True
134 346aa68d Mike Koss
135
    # Munin should send connection options via environment vars
136
    bitcoin_options = get_env_options('rpcconnect', 'rpcport', 'rpcuser', 'rpcpassword')
137
    bitcoin_options.rpcconnect = bitcoin_options.get('rpcconnect', '127.0.0.1')
138
    bitcoin_options.rpcport = bitcoin_options.get('rpcport', '8332')
139
140 db7b2f28 Lars Kruse
    error = None
141 346aa68d Mike Koss
    if bitcoin_options.get('rpcuser') is None:
142 abdeb7ec Lars Kruse
        conf_file = os.getenv("bitcoin_configfile")
143
        if not conf_file:
144 db7b2f28 Lars Kruse
            error = "Missing environment settings: rpcuser/rcpassword or bitcoin_configfile"
145 abdeb7ec Lars Kruse
        elif not os.path.exists(conf_file):
146 db7b2f28 Lars Kruse
            error = "Configuration file does not exist: {}".format(conf_file)
147 abdeb7ec Lars Kruse
        else:
148
            bitcoin_options = parse_conf(conf_file)
149 346aa68d Mike Koss
150 db7b2f28 Lars Kruse
    if not error:
151
        try:
152
            bitcoin_options.require('rpcuser', 'rpcpassword')
153
        except KeyError as exc:
154
            error = str(exc).strip("'")
155
156
    if not error:
157
        bitcoin = ServiceProxy('http://%s:%s' % (bitcoin_options.rpcconnect,
158
                                                 bitcoin_options.rpcport),
159
                               username=bitcoin_options.rpcuser,
160
                               password=bitcoin_options.rpcpassword)
161 9a2a8138 Vincas Dargis
        (info, minor_version, connect_error) = _rpc_get_initial_info(bitcoin)
162 15f1055e Vincas Dargis
        if connect_error:
163
            error = "Could not connect to Bitcoin server: {}".format(connect_error)
164 4db16377 Mike Koss
165 88e027be Lars Kruse
    if command == 'autoconf':
166
        if error:
167
            print('no ({})'.format(error))
168 346aa68d Mike Koss
        else:
169 88e027be Lars Kruse
            print('yes')
170
        return True
171
172
    if error:
173
        print(error, file=sys.stderr)
174
        return False
175 346aa68d Mike Koss
176 15f1055e Vincas Dargis
    if request_var == 'balance':
177 9a2a8138 Vincas Dargis
        info = _rpc_get_balance(info, minor_version, bitcoin)
178 15f1055e Vincas Dargis
179 4db16377 Mike Koss
    if request_var in ('transactions', 'block_age'):
180 15f1055e Vincas Dargis
        (info, error) = bitcoin.getblockchaininfo()
181
        (info, error) = bitcoin.getblock(info['bestblockhash'])
182 0988b353 Michael Gronager
        info['block_age'] = int(time.time()) - info['time']
183
        info['confirmed'] = len(info['tx'])
184 4db16377 Mike Koss
185 15f1055e Vincas Dargis
    if request_var == 'difficulty':
186
        (info, error) = bitcoin.getblockchaininfo()
187
188
    if request_var == 'transactions':
189
        (memory_pool, error) = bitcoin.getmempoolinfo()
190
        info['waiting'] = memory_pool['size']
191 4db16377 Mike Koss
192
    for label in line_labels:
193 fffb536e Lars Kruse
        print("%s.value %s" % (label, info[label]))
194 346aa68d Mike Koss
195 15f1055e Vincas Dargis
    return True
196
197 346aa68d Mike Koss
198 5f66fcc3 Mike Koss
def parse_conf(filename):
199
    """ Bitcoin config file parser. """
200
201
    options = Options()
202
203
    re_line = re.compile(r'^\s*([^#]*)\s*(#.*)?$')
204
    re_setting = re.compile(r'^(.*)\s*=\s*(.*)$')
205
    try:
206
        with open(filename) as file:
207
            for line in file.readlines():
208
                line = re_line.match(line).group(1).strip()
209
                m = re_setting.match(line)
210
                if m is None:
211
                    continue
212
                (var, value) = (m.group(1), m.group(2).strip())
213
                options[var] = value
214 fffb536e Lars Kruse
    except OSError:
215
        # the config file may be missing
216 5f66fcc3 Mike Koss
        pass
217
218
    return options
219
220
221
def get_env_options(*vars):
222
    options = Options()
223
    for var in vars:
224 ea051b7f Lars Kruse
        value = os.getenv(var)
225
        if value is not None:
226
            options[var] = os.getenv(var)
227 5f66fcc3 Mike Koss
    return options
228
229
230
class Options(dict):
231
    """A dict that allows for object-like property access syntax."""
232
    def __getattr__(self, name):
233
        try:
234
            return self[name]
235
        except KeyError:
236
            raise AttributeError(name)
237
238
    def require(self, *names):
239
        missing = []
240
        for name in names:
241
            if self.get(name) is None:
242
                missing.append(name)
243
        if len(missing) > 0:
244 db7b2f28 Lars Kruse
            raise KeyError("Missing required setting{}: {}."
245
                           .format('s' if len(missing) > 1 else '', ', '.join(missing)))
246 5f66fcc3 Mike Koss
247
248 a0aa955a Lars Kruse
class ServiceProxy:
249 5f66fcc3 Mike Koss
    """
250
    Proxy for a JSON-RPC web service. Calls to a function attribute
251
    generates a JSON-RPC call to the host service. If a callback
252
    keyword arg is included, the call is processed as an asynchronous
253
    request.
254
255
    Each call returns (result, error) tuple.
256
    """
257
    def __init__(self, url, username=None, password=None):
258
        self.url = url
259
        self.id = 0
260
        self.username = username
261
        self.password = password
262
263
    def __getattr__(self, method):
264
        self.id += 1
265
        return Proxy(self, method, id=self.id)
266
267
268 a0aa955a Lars Kruse
class Proxy:
269 5f66fcc3 Mike Koss
    def __init__(self, service, method, id=None):
270
        self.service = service
271
        self.method = method
272
        self.id = id
273
274
    def __call__(self, *args):
275
        if DEBUG:
276
            arg_strings = [json.dumps(arg) for arg in args]
277 fffb536e Lars Kruse
            print("Calling %s(%s) @ %s" % (self.method,
278 5f66fcc3 Mike Koss
                                           ', '.join(arg_strings),
279 fffb536e Lars Kruse
                                           self.service.url))
280 5f66fcc3 Mike Koss
281
        data = {
282
            'method': self.method,
283
            'params': args,
284
            'id': self.id,
285 7063330e Lars Kruse
        }
286 f5de3d19 Lars Kruse
        request = urllib.request.Request(self.service.url, json.dumps(data).encode())
287 5f66fcc3 Mike Koss
        if self.service.username:
288 b6a41513 Lars Kruse
            auth_string = '%s:%s' % (self.service.username, self.service.password)
289
            auth_b64 = base64.urlsafe_b64encode(auth_string.encode()).decode()
290
            request.add_header('Authorization', 'Basic %s' % auth_b64)
291 5f66fcc3 Mike Koss
292
        try:
293 bc20826c Lars Kruse
            body = urllib.request.urlopen(request).read()
294
        except urllib.error.URLError as e:
295 5f66fcc3 Mike Koss
            return (None, e)
296
297
        if DEBUG:
298 15f1055e Vincas Dargis
            print('RPC Response (%s): %s' % (self.method, json.dumps(json.loads(body), indent=4)))
299 5f66fcc3 Mike Koss
300
        try:
301
            data = json.loads(body)
302 fffb536e Lars Kruse
        except ValueError as e:
303 5f66fcc3 Mike Koss
            return (None, e.message)
304
        # TODO: Check that id matches?
305
        return (data['result'], data['error'])
306
307
308 4db16377 Mike Koss
def get_json_url(url):
309 bc20826c Lars Kruse
    request = urllib.request.Request(url)
310
    body = urllib.request.urlopen(request).read()
311 4db16377 Mike Koss
    data = json.loads(body)
312
    return data
313
314
315 5f66fcc3 Mike Koss
if __name__ == "__main__":
316 fd45fe6c Lars Kruse
    sys.exit(0 if main() else 1)