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
#!/usr/bin/env python3
2

    
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
     env.bitcoin_configfile /home/your-username/.bitcoin/bitcoin.conf
18

    
19
   Then be sure that the file referenced above (typically: $HOME/.bitcoin/bitcoin.conf)
20
   has the correct authentication info:
21
       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

    
53
import base64
54
import json
55
import os
56
import re
57
import sys
58
import time
59
import urllib.error
60
import urllib.request
61

    
62

    
63
DEBUG = os.getenv('MUNIN_DEBUG') == '1'
64

    
65

    
66
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
def main():
106
    # getinfo variable is read from command name - probably the sym-link name.
107
    request_var = sys.argv[0].split('_', 1)[1] or 'balance'
108
    command = sys.argv[1] if len(sys.argv) > 1 else None
109
    request_labels = {'balance': ('Wallet Balance', 'BTC'),
110
                      'connections': ('Peer Connections', 'Connections'),
111
                      'transactions': ("Transactions", "Transactions",
112
                                       ('confirmed', 'waiting')),
113
                      'block_age': ("Last Block Age", "Seconds"),
114
                      'difficulty': ("Difficulty", ""),
115
                      }
116
    labels = request_labels[request_var]
117
    if len(labels) < 3:
118
        line_labels = [request_var]
119
    else:
120
        line_labels = labels[2]
121

    
122
    if command == 'suggest':
123
        for var_name in request_labels.keys():
124
            print(var_name)
125
        return True
126

    
127
    if command == 'config':
128
        print('graph_category htc')
129
        print('graph_title Bitcoin %s' % labels[0])
130
        print('graph_vlabel %s' % labels[1])
131
        for label in line_labels:
132
            print('%s.label %s' % (label, label))
133
        return True
134

    
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
    error = None
141
    if bitcoin_options.get('rpcuser') is None:
142
        conf_file = os.getenv("bitcoin_configfile")
143
        if not conf_file:
144
            error = "Missing environment settings: rpcuser/rcpassword or bitcoin_configfile"
145
        elif not os.path.exists(conf_file):
146
            error = "Configuration file does not exist: {}".format(conf_file)
147
        else:
148
            bitcoin_options = parse_conf(conf_file)
149

    
150
    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
        (info, minor_version, connect_error) = _rpc_get_initial_info(bitcoin)
162
        if connect_error:
163
            error = "Could not connect to Bitcoin server: {}".format(connect_error)
164

    
165
    if command == 'autoconf':
166
        if error:
167
            print('no ({})'.format(error))
168
        else:
169
            print('yes')
170
        return True
171

    
172
    if error:
173
        print(error, file=sys.stderr)
174
        return False
175

    
176
    if request_var == 'balance':
177
        info = _rpc_get_balance(info, minor_version, bitcoin)
178

    
179
    if request_var in ('transactions', 'block_age'):
180
        (info, error) = bitcoin.getblockchaininfo()
181
        (info, error) = bitcoin.getblock(info['bestblockhash'])
182
        info['block_age'] = int(time.time()) - info['time']
183
        info['confirmed'] = len(info['tx'])
184

    
185
    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

    
192
    for label in line_labels:
193
        print("%s.value %s" % (label, info[label]))
194

    
195
    return True
196

    
197

    
198
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
    except OSError:
215
        # the config file may be missing
216
        pass
217

    
218
    return options
219

    
220

    
221
def get_env_options(*vars):
222
    options = Options()
223
    for var in vars:
224
        value = os.getenv(var)
225
        if value is not None:
226
            options[var] = os.getenv(var)
227
    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
            raise KeyError("Missing required setting{}: {}."
245
                           .format('s' if len(missing) > 1 else '', ', '.join(missing)))
246

    
247

    
248
class ServiceProxy:
249
    """
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
class Proxy:
269
    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
            print("Calling %s(%s) @ %s" % (self.method,
278
                                           ', '.join(arg_strings),
279
                                           self.service.url))
280

    
281
        data = {
282
            'method': self.method,
283
            'params': args,
284
            'id': self.id,
285
        }
286
        request = urllib.request.Request(self.service.url, json.dumps(data).encode())
287
        if self.service.username:
288
            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

    
292
        try:
293
            body = urllib.request.urlopen(request).read()
294
        except urllib.error.URLError as e:
295
            return (None, e)
296

    
297
        if DEBUG:
298
            print('RPC Response (%s): %s' % (self.method, json.dumps(json.loads(body), indent=4)))
299

    
300
        try:
301
            data = json.loads(body)
302
        except ValueError as e:
303
            return (None, e.message)
304
        # TODO: Check that id matches?
305
        return (data['result'], data['error'])
306

    
307

    
308
def get_json_url(url):
309
    request = urllib.request.Request(url)
310
    body = urllib.request.urlopen(request).read()
311
    data = json.loads(body)
312
    return data
313

    
314

    
315
if __name__ == "__main__":
316
    sys.exit(0 if main() else 1)