Projet

Général

Profil

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

root / tools / munin-node-from-hell / muninnode-from-hell @ 8589c6df

Historique | Voir | Annoter | Télécharger (13,1 ko)

1 d33f5b31 Lasse Karstensen
#!/usr/bin/python
2 352a9baf Lasse Karstensen
# .- coding: utf-8 -.
3 d33f5b31 Lasse Karstensen
#
4
# Artificial munin node that behaves in all the ways you would like
5
# ordinary nodes _not_ to behave.
6
#
7
# Intended use is for designing and debugging munin-server poller to handle 
8
# such problems.
9
#
10
# See the file MIT-LICENSE for licensing information.
11
#
12
# Copyright (C) 2011 Karstensen IT
13
# Written by Lasse Karstensen <lasse.karstensen@gmail.com>, Dec 2011.
14
15
import os, sys, time, random
16
import socket
17
import threading
18
import SocketServer
19
import ConfigParser
20
21
VERSION = "muninnode-from-hell v0.1"
22
modules = {}
23
24
class MuninPlugin:
25 fbb6614e Lasse Karstensen
    def __init__(self):
26
        self.current_load = None
27
        self.current_locks = None
28
29 d33f5b31 Lasse Karstensen
    def sleep_fetch(self, conf):
30
        period = None
31
        if conf.get("mode") == "sleepy" and conf.get("sleepyness"):
32
            period = float(conf.get("sleepyness"))
33
        if conf.get("mode") == "exp" and conf.get("lambd"):
34
            period = random.expovariate(1 / float(conf.get("lambd")))
35
36
        if period:
37
            #print "will sleep %.3f seconds" % period
38
            time.sleep(period)
39
            
40
    def sleep_config(self, conf):
41
        return self.sleep_fetch(conf)
42
43 fbb6614e Lasse Karstensen
    def find_load(self):
44
        # At about a thousand node instances you get this:
45
        #IOError: [Errno 24] Too many open files: '/proc/loadavg'
46
        # cache it for a bit..
47
        if (not self.current_load) or random.randint(0,100) == 1:
48
            load = open("/proc/loadavg", "r").read()
49
            load, rest = load.split(" ", 1)
50
            self.current_load = float(load)
51
        return self.current_load
52
            
53
    def find_locks(self):
54
        if (not self.current_locks) or random.randint(0,100) == 1:
55
            fp = open("/proc/locks", "r")
56
            self.current_locks = len(fp.readlines())
57
        return self.current_locks 
58 d33f5b31 Lasse Karstensen
59
class load(MuninPlugin):
60
    def fetch(self, conf):
61
        self.sleep_fetch(conf)
62 fbb6614e Lasse Karstensen
        return "load.value %.2f" % self.find_load()
63 d33f5b31 Lasse Karstensen
64
    def config(self, conf):
65
        self.sleep_config(conf)
66
        return """graph_title Load average
67
graph_args --base 1000 -l 0
68
graph_vlabel load
69
graph_scale no
70
graph_category system
71
load.label load
72
graph_info The load average of the machine describes how many processes are in the run-queue (scheduled to run "immediately").
73
load.info 5 minute load average """
74
modules["load"] = load()
75
76
class locks(MuninPlugin):
77
    def fetch(self, conf):
78
        self.sleep_fetch(conf)
79 fbb6614e Lasse Karstensen
        return "locks.value %i" % self.find_locks()
80 d33f5b31 Lasse Karstensen
81
    def config(self, conf):
82
        self.sleep_config(conf)
83
        return """graph_title Filesystem locks
84
graph_vlabel number of locks
85
graph_scale no
86
graph_info This graph shows file system lock info
87
graph_category system
88
locks.label number of locks
89
locks.info Number of active locks"""
90
modules["locks"] = locks()
91
92
class tarpit(MuninPlugin):
93
    "Nasty plugin that never responds"
94
    def fetch(self, conf):
95
        time.sleep(1000)
96
97
    def config(self, conf):
98
        time.sleep(1000)
99
modules["tarpit"] = tarpit()
100
101
class always_warning(MuninPlugin):
102 071b1cac Lasse Karstensen
    conftext = """graph_title Always in LEVEL
103 d33f5b31 Lasse Karstensen
graph_vlabel Level
104
graph_scale no
105 071b1cac Lasse Karstensen
graph_info A simple graph that is always in LEVEL
106 851979eb Lasse Karstensen
graph_category always_LEVEL
107 d33f5b31 Lasse Karstensen
generic.label Level
108
generic.info Level usually above warning level
109 071b1cac Lasse Karstensen
generic.warning 5
110
generic.critical 10"""
111
112
    def fetch(self, conf):
113
        return "generic.value 10"
114
115
    def config(self, conf):
116
        return self.conftext.replace("LEVEL","warning")
117 d33f5b31 Lasse Karstensen
modules["always_warning"] = always_warning()
118
119 6df2ce23 Lasse Karstensen
class always_critical(always_warning):
120 d33f5b31 Lasse Karstensen
    def fetch(self, conf):
121
        return "generic.value 20"
122 071b1cac Lasse Karstensen
123
    def config(self, conf):
124
        return self.conftext.replace("LEVEL","critical")
125 6df2ce23 Lasse Karstensen
modules["always_critical"] = always_critical()
126
127 32f9909e Lasse Karstensen
class failing_plugin(MuninPlugin):
128
    "A really broken plugin"
129
    def fetch(self, conf):
130
        return "# Bad exit"
131
132
    def config(self, conf):
133
        return "# Bad exit"
134
modules["failing_plugin"] = failing_plugin()
135
136
class failing_plugin2(MuninPlugin):
137
    def fetch(self, conf):
138
        return "# Bad exit"
139
140
    def config(self, conf):
141
        return """graph_title Config works, fetch fails
142
graph_vlabel Level
143
graph_category failing
144
generic.label generic_label_here
145
generic.info never_really_used"""
146
modules["failing_plugin2"] = failing_plugin2()
147
148
class failing_plugin3(MuninPlugin):
149
    def config(self, conf):
150
        return """graph_title A plugin with two dses but only fetch value for one
151
graph_args --base 1000 -l 0
152
fivemin.label 1 minute load
153
onemin.label 5 minute load"""
154
    def fetch(self, conf):
155
        return "onemin.value 1"
156
modules["failing_plugin3"] = failing_plugin3()
157
158
159 6df2ce23 Lasse Karstensen
class graph_area(MuninPlugin):
160
    "A plugin that uses STACK and AREA. From proc_pri. Use: testing the grapher"
161
    def fetch(self, conf):
162 1c70bc92 Lasse Karstensen
        return """high.value 3
163
low.value 2
164
locked.value 1"""
165 6df2ce23 Lasse Karstensen
166
    def config(self, conf):
167
        return """graph_title AREA and STACK
168 1c70bc92 Lasse Karstensen
graph_order low high locked
169 071b1cac Lasse Karstensen
graph_category graphtest
170 6df2ce23 Lasse Karstensen
graph_info This graph shows nuber of processes at each priority
171
graph_args --base 1000 -l 0
172
graph_vlabel Number of processes
173 1c70bc92 Lasse Karstensen
high.label high priority
174
high.draw STACK
175
high.info The number of high-priority processes (tasks)
176
low.label low priority
177
low.draw AREA
178
low.info The number of low-priority processes (tasks)
179
locked.label locked in memory
180
locked.draw STACK
181
locked.info The number of processes that have pages locked into memory (for real-time and custom IO)
182 6df2ce23 Lasse Karstensen
"""
183
modules["graph_area"] = graph_area()
184 d33f5b31 Lasse Karstensen
185 352a9baf Lasse Karstensen
class utf8_graphcat(MuninPlugin):
186
    "A plugin with a graph category which has UTF-8 in it"
187
    def fetch(self, conf):
188 fbb6614e Lasse Karstensen
        return "apples.value %.2f" % self.find_load()
189 352a9baf Lasse Karstensen
190
    def config(self, conf):
191
        return """graph_title Example UTF-8 graph
192
graph_vlabel apples
193
graph_category foo™
194
apples.label apples
195
graph_info Apples eaten
196
apples.info Apples eaten"""
197
modules["utf8_graphcat"] = utf8_graphcat()
198
199
class utf8_graphname(MuninPlugin):
200
    "A plugin with a UTF-8 name"
201
    def fetch(self, conf):
202 fbb6614e Lasse Karstensen
        return "apples.value %.2f" % self.find_load()
203 352a9baf Lasse Karstensen
204
    def config(self, conf):
205
        return """graph_title Example UTF-8 graph
206
graph_vlabel apples
207
graph_category system
208
apples.label apples
209
graph_info Apples eaten
210
apples.info Apples eaten"""
211
modules["utf8_™graphname"] = utf8_graphname()
212
213 d33f5b31 Lasse Karstensen
214
class ArgumentTCPserver(SocketServer.ThreadingTCPServer):
215
    def __init__(self, server_address, RequestHandlerClass, args):
216
        SocketServer.ThreadingTCPServer.__init__(self,server_address, RequestHandlerClass)
217
        self.args = args
218
219
220
class MuninHandler(SocketServer.StreamRequestHandler):
221
    """
222
    Munin server implementation.
223
224
    This is based on munin_node.py by Chris Holcombe / http://sourceforge.net/projects/pythonmuninnode/
225
226
    Possible commands:
227
    list, nodes, config, fetch, version or quit
228
    """
229
230
    def handle(self):
231 f110d3dd Lasse Karstensen
        if self.server.args.get("verbose"): print "%s: Connection from %s:%s. server args is %s" \
232 d33f5b31 Lasse Karstensen
            % (self.server.args["name"], self.client_address[0], self.client_address[1], self.server.args)
233
        # slow path
234
        hostname = self.server.args["name"]
235
        full_hostname = hostname
236
237
        moduleprofile = self.server.args["pluginprofile"]
238
        modulenames = set(moduleprofile)
239
240
        self.wfile.write("# munin node at %s\n" % hostname)
241
242
        while True:
243
            line = self.rfile.readline().strip()
244
            try:
245
                cmd, args = line.split(" ", 1)
246
            except ValueError:
247
                cmd = line
248
                args = ""
249
250
            if not cmd or cmd == "quit":
251
                break
252
253
            if cmd == "list":
254
                # List all plugins that are available
255
                self.wfile.write(" ".join(self.server.args["plugins"].keys()) + "\n")
256
            elif cmd == "nodes":
257
                # We just support this host
258
                self.wfile.write("%s\n.\n" % full_hostname)
259
            elif cmd == "config":
260
                # display the config information of the plugin
261
                if not self.server.args["plugins"].has_key(args):
262
                    self.wfile.write("# Unknown service\n.\n" )
263
                else:
264
                    config = self.server.args["plugins"][args].config(self.server.args)
265
                    if config is None:
266
                        self.wfile.write("# Unknown service\n.\n")
267
                    else:
268
                        self.wfile.write(config + "\n.\n")
269
            elif cmd == "fetch":
270
                # display the data information as returned by the plugin
271
                if not self.server.args["plugins"].has_key(args):
272
                    self.wfile.write("# Unknown service\n.\n")
273
                else:
274
                    data = self.server.args["plugins"][args].fetch(self.server.args)
275
                    if data is None:
276
                        self.wfile.write("# Unknown service\n.\n")
277
                    else:
278
                        self.wfile.write(data + "\n.\n")
279
            elif cmd == "version":
280
                # display the server version
281
                self.wfile.write("munin node on %s version: %s\n" %
282
                                 (full_hostname, VERSION))
283
            else:
284
                self.wfile.write("# Unknown command. Try list, nodes, " \
285
                                 "config, fetch, version or quit\n")
286
287
288
def start_servers(instances):
289
    # TODO: Listen to IPv6
290
    HOST = "0.0.0.0"
291
    servers = {}
292
    for iconf in instances:
293
            print "Setting up instance %s at port %s" \
294
                % (iconf["name"], iconf["expanded_port"])
295
296
            server = ArgumentTCPserver((HOST, iconf["expanded_port"]), MuninHandler, iconf)
297
            server_thread = threading.Thread(target=server.serve_forever)
298
            server_thread.daemon = True
299
            server_thread.start()
300
301
            servers[iconf["name"]] = server
302
    return servers
303
304
305
306
def usage():
307 8354c8ba Lasse Karstensen
    print "Usage: %s [--run] [--verbose] [--muninconf] <configfile> <configfileN>" % sys.argv[0]
308 d33f5b31 Lasse Karstensen
309
def main():
310
    if len(sys.argv) <= 2:
311
        usage()
312
        sys.exit(1)
313
314 0442899e Lasse Karstensen
    verbose = False
315
    if "--verbose" in sys.argv:
316
        verbose = True
317
318 d33f5b31 Lasse Karstensen
    config = ConfigParser.RawConfigParser()
319 8354c8ba Lasse Karstensen
    for configfile in sys.argv[1:]:
320
        if not configfile.endswith(".conf"):
321
            continue
322
        if verbose:
323
            print "Reading config file %s" % configfile
324
        config.read(configfile)
325 d33f5b31 Lasse Karstensen
326
    instancekeys = [ key for key in config.sections() if key.startswith("instance:") ]
327
    servers = {}
328
329
    instances = []
330
331
    for key in instancekeys:
332
        instancename = key.split(":", 2)[1]
333
        portrange = []
334
        if config.has_option(key, "port"):
335
            portrange = [ config.getint(key, "port") ]
336
        if config.has_option(key, "portrange"):
337
            rangestr = config.get(key, "portrange")
338
            ranges = rangestr.split("-")
339
            range_expanded = range(int(ranges[0]), int(ranges[1])+1, 1)
340
            portrange += range_expanded
341
342
        if len(portrange) == 0:
343
            print "WARN: No port or portrange defined for instance %s" \
344
                % instancename
345
346
        pluginprofile = "pluginprofile:%s" % config.get(key, "pluginprofile")
347
        if not config.has_section(pluginprofile):
348
            print "WARN: Definition for pluginprofile %s not found, skipping" \
349
                % config.get(key, "pluginprofile")
350
            continue
351
352
        plugins = {}
353
        tentative_pluginlist = config.get(pluginprofile, "plugins").split(",")
354
        assert(len(tentative_pluginlist) > 0)
355
        for tentative_plugin in tentative_pluginlist:
356
            tentative_plugin = tentative_plugin.strip()
357
            if not modules.has_key(tentative_plugin):
358
                print "WARN: Pluginprofile %s specifies unknown plugin %s" \
359
                    % (pluginprofile, tentative_plugin)
360
                continue
361
362 8589c6df klemens
            # support more than one instantiation of the same plugin.
363 d33f5b31 Lasse Karstensen
            plugininstancename = tentative_plugin
364
            i=2
365
            while (plugins.has_key(plugininstancename)):
366
                plugininstancename = tentative_plugin + str(i)
367
                i += 1
368
369
            plugins[plugininstancename] = modules[tentative_plugin]
370
371
        for portinstance in portrange:
372
            instanceconfig = dict()
373
374
            for k,v in config.items(key):
375
                instanceconfig[k] = v
376
377
            instanceconfig["plugins"] = plugins
378 8354c8ba Lasse Karstensen
            instanceconfig["verbose"] = verbose
379 d33f5b31 Lasse Karstensen
380
            instanceconfig["name"] = "%s-%s" % (instancename, portinstance)
381
            instanceconfig["expanded_port"] = portinstance
382
383
            instances.append(instanceconfig)
384
            # XXX: need to store what handlers we should have.
385 8354c8ba Lasse Karstensen
    print instances
386 d33f5b31 Lasse Karstensen
387
    # output sample munin config for the poller
388
    if "--muninconf" in sys.argv:
389
        for i in instances:
390
            print "[%s;%s]\n\taddress %s\n\tuse_node_name yes\n\tport %s\n" \
391
                % ( "fromhell", i["name"], config.get("base","hostname"), i["port"])
392
393
394
    if "--run" in sys.argv:
395 0442899e Lasse Karstensen
        if verbose: print "Starting up.."
396 d33f5b31 Lasse Karstensen
        servers = start_servers(instances)
397
398
        try:
399
            while True:
400
                time.sleep(0.5)
401
        except KeyboardInterrupt:
402
            print "Caught Ctrl-c, shutting down.."
403
            for port, server in servers.items():
404
                server.shutdown()
405
                sys.exit(0)
406
407
if __name__ == "__main__":
408
    main()