Projet

Général

Profil

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

root / plugins / currency / bitcoin / bitcoind_ @ 7063330e

Historique | Voir | Annoter | Télécharger (8 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 db7b2f28 Lars Kruse
    error = None
103 346aa68d Mike Koss
    if bitcoin_options.get('rpcuser') is None:
104 abdeb7ec Lars Kruse
        conf_file = os.getenv("bitcoin_configfile")
105
        if not conf_file:
106 db7b2f28 Lars Kruse
            error = "Missing environment settings: rpcuser/rcpassword or bitcoin_configfile"
107 abdeb7ec Lars Kruse
        elif not os.path.exists(conf_file):
108 db7b2f28 Lars Kruse
            error = "Configuration file does not exist: {}".format(conf_file)
109 abdeb7ec Lars Kruse
        else:
110
            bitcoin_options = parse_conf(conf_file)
111 346aa68d Mike Koss
112 db7b2f28 Lars Kruse
    if not error:
113
        try:
114
            bitcoin_options.require('rpcuser', 'rpcpassword')
115
        except KeyError as exc:
116
            error = str(exc).strip("'")
117
118
    if not error:
119
        bitcoin = ServiceProxy('http://%s:%s' % (bitcoin_options.rpcconnect,
120
                                                 bitcoin_options.rpcport),
121
                               username=bitcoin_options.rpcuser,
122
                               password=bitcoin_options.rpcpassword)
123
        (info, connect_error) = bitcoin.getinfo()
124 88e027be Lars Kruse
        error = "Could not connect to Bitcoin server: {}".format(connect_error)
125 4db16377 Mike Koss
126 88e027be Lars Kruse
    if command == 'autoconf':
127
        if error:
128
            print('no ({})'.format(error))
129 346aa68d Mike Koss
        else:
130 88e027be Lars Kruse
            print('yes')
131
        return True
132
133
    if error:
134
        print(error, file=sys.stderr)
135
        return False
136 346aa68d Mike Koss
137 4db16377 Mike Koss
    if request_var in ('transactions', 'block_age'):
138 0988b353 Michael Gronager
        (info, error) = bitcoin.getblockhash(info['blocks'])
139
        (info, error) = bitcoin.getblock(info)
140
        info['block_age'] = int(time.time()) - info['time']
141
        info['confirmed'] = len(info['tx'])
142 4db16377 Mike Koss
143
    if request_var in ('fees', 'transactions'):
144 0988b353 Michael Gronager
        (memory_pool, error) = bitcoin.getrawmempool()
145 4db16377 Mike Koss
        if memory_pool:
146 0988b353 Michael Gronager
            info['waiting'] = len(memory_pool)
147 4db16377 Mike Koss
148
    for label in line_labels:
149 fffb536e Lars Kruse
        print("%s.value %s" % (label, info[label]))
150 346aa68d Mike Koss
151
152 5f66fcc3 Mike Koss
def parse_conf(filename):
153
    """ Bitcoin config file parser. """
154
155
    options = Options()
156
157
    re_line = re.compile(r'^\s*([^#]*)\s*(#.*)?$')
158
    re_setting = re.compile(r'^(.*)\s*=\s*(.*)$')
159
    try:
160
        with open(filename) as file:
161
            for line in file.readlines():
162
                line = re_line.match(line).group(1).strip()
163
                m = re_setting.match(line)
164
                if m is None:
165
                    continue
166
                (var, value) = (m.group(1), m.group(2).strip())
167
                options[var] = value
168 fffb536e Lars Kruse
    except OSError:
169
        # the config file may be missing
170 5f66fcc3 Mike Koss
        pass
171
172
    return options
173
174
175
def get_env_options(*vars):
176
    options = Options()
177
    for var in vars:
178 ea051b7f Lars Kruse
        value = os.getenv(var)
179
        if value is not None:
180
            options[var] = os.getenv(var)
181 5f66fcc3 Mike Koss
    return options
182
183
184
class Options(dict):
185
    """A dict that allows for object-like property access syntax."""
186
    def __getattr__(self, name):
187
        try:
188
            return self[name]
189
        except KeyError:
190
            raise AttributeError(name)
191
192
    def require(self, *names):
193
        missing = []
194
        for name in names:
195
            if self.get(name) is None:
196
                missing.append(name)
197
        if len(missing) > 0:
198 db7b2f28 Lars Kruse
            raise KeyError("Missing required setting{}: {}."
199
                           .format('s' if len(missing) > 1 else '', ', '.join(missing)))
200 5f66fcc3 Mike Koss
201
202 a0aa955a Lars Kruse
class ServiceProxy:
203 5f66fcc3 Mike Koss
    """
204
    Proxy for a JSON-RPC web service. Calls to a function attribute
205
    generates a JSON-RPC call to the host service. If a callback
206
    keyword arg is included, the call is processed as an asynchronous
207
    request.
208
209
    Each call returns (result, error) tuple.
210
    """
211
    def __init__(self, url, username=None, password=None):
212
        self.url = url
213
        self.id = 0
214
        self.username = username
215
        self.password = password
216
217
    def __getattr__(self, method):
218
        self.id += 1
219
        return Proxy(self, method, id=self.id)
220
221
222 a0aa955a Lars Kruse
class Proxy:
223 5f66fcc3 Mike Koss
    def __init__(self, service, method, id=None):
224
        self.service = service
225
        self.method = method
226
        self.id = id
227
228
    def __call__(self, *args):
229
        if DEBUG:
230
            arg_strings = [json.dumps(arg) for arg in args]
231 fffb536e Lars Kruse
            print("Calling %s(%s) @ %s" % (self.method,
232 5f66fcc3 Mike Koss
                                           ', '.join(arg_strings),
233 fffb536e Lars Kruse
                                           self.service.url))
234 5f66fcc3 Mike Koss
235
        data = {
236
            'method': self.method,
237
            'params': args,
238
            'id': self.id,
239 7063330e Lars Kruse
        }
240 f5de3d19 Lars Kruse
        request = urllib.request.Request(self.service.url, json.dumps(data).encode())
241 5f66fcc3 Mike Koss
        if self.service.username:
242 b6a41513 Lars Kruse
            auth_string = '%s:%s' % (self.service.username, self.service.password)
243
            auth_b64 = base64.urlsafe_b64encode(auth_string.encode()).decode()
244
            request.add_header('Authorization', 'Basic %s' % auth_b64)
245 5f66fcc3 Mike Koss
246
        try:
247 bc20826c Lars Kruse
            body = urllib.request.urlopen(request).read()
248
        except urllib.error.URLError as e:
249 5f66fcc3 Mike Koss
            return (None, e)
250
251
        if DEBUG:
252 fffb536e Lars Kruse
            print('RPC Response (%s): %s' % (self.method, json.dumps(body, indent=4)))
253 5f66fcc3 Mike Koss
254
        try:
255
            data = json.loads(body)
256 fffb536e Lars Kruse
        except ValueError as e:
257 5f66fcc3 Mike Koss
            return (None, e.message)
258
        # TODO: Check that id matches?
259
        return (data['result'], data['error'])
260
261
262 4db16377 Mike Koss
def get_json_url(url):
263 bc20826c Lars Kruse
    request = urllib.request.Request(url)
264
    body = urllib.request.urlopen(request).read()
265 4db16377 Mike Koss
    data = json.loads(body)
266
    return data
267
268
269 5f66fcc3 Mike Koss
if __name__ == "__main__":
270 fd45fe6c Lars Kruse
    sys.exit(0 if main() else 1)