#!/usr/bin/python # .- coding: utf-8 -. # # Artificial munin node that behaves in all the ways you would like # ordinary nodes _not_ to behave. # # Intended use is for designing and debugging munin-server poller to handle # such problems. # # See the file MIT-LICENSE for licensing information. # # Copyright (C) 2011 Karstensen IT # Written by Lasse Karstensen , Dec 2011. import os, sys, time, random import socket import threading import SocketServer import ConfigParser VERSION = "muninnode-from-hell v0.1" modules = {} class MuninPlugin: def __init__(self): self.current_load = None self.current_locks = None def sleep_fetch(self, conf): period = None if conf.get("mode") == "sleepy" and conf.get("sleepyness"): period = float(conf.get("sleepyness")) if conf.get("mode") == "exp" and conf.get("lambd"): period = random.expovariate(1 / float(conf.get("lambd"))) if period: #print "will sleep %.3f seconds" % period time.sleep(period) def sleep_config(self, conf): return self.sleep_fetch(conf) def find_load(self): # At about a thousand node instances you get this: #IOError: [Errno 24] Too many open files: '/proc/loadavg' # cache it for a bit.. if (not self.current_load) or random.randint(0,100) == 1: load = open("/proc/loadavg", "r").read() load, rest = load.split(" ", 1) self.current_load = float(load) return self.current_load def find_locks(self): if (not self.current_locks) or random.randint(0,100) == 1: fp = open("/proc/locks", "r") self.current_locks = len(fp.readlines()) return self.current_locks class load(MuninPlugin): def fetch(self, conf): self.sleep_fetch(conf) return "load.value %.2f" % self.find_load() def config(self, conf): self.sleep_config(conf) return """graph_title Load average graph_args --base 1000 -l 0 graph_vlabel load graph_scale no graph_category system load.label load graph_info The load average of the machine describes how many processes are in the run-queue (scheduled to run "immediately"). load.info 5 minute load average """ modules["load"] = load() class locks(MuninPlugin): def fetch(self, conf): self.sleep_fetch(conf) return "locks.value %i" % self.find_locks() def config(self, conf): self.sleep_config(conf) return """graph_title Filesystem locks graph_vlabel number of locks graph_scale no graph_info This graph shows file system lock info graph_category system locks.label number of locks locks.info Number of active locks""" modules["locks"] = locks() class tarpit(MuninPlugin): "Nasty plugin that never responds" def fetch(self, conf): time.sleep(1000) def config(self, conf): time.sleep(1000) modules["tarpit"] = tarpit() class always_warning(MuninPlugin): conftext = """graph_title Always in LEVEL graph_vlabel Level graph_scale no graph_info A simple graph that is always in LEVEL graph_category always_LEVEL generic.label Level generic.info Level usually above warning level generic.warning 5 generic.critical 10""" def fetch(self, conf): return "generic.value 10" def config(self, conf): return self.conftext.replace("LEVEL","warning") modules["always_warning"] = always_warning() class always_critical(always_warning): def fetch(self, conf): return "generic.value 20" def config(self, conf): return self.conftext.replace("LEVEL","critical") modules["always_critical"] = always_critical() class failing_plugin(MuninPlugin): "A really broken plugin" def fetch(self, conf): return "# Bad exit" def config(self, conf): return "# Bad exit" modules["failing_plugin"] = failing_plugin() class failing_plugin2(MuninPlugin): def fetch(self, conf): return "# Bad exit" def config(self, conf): return """graph_title Config works, fetch fails graph_vlabel Level graph_category failing generic.label generic_label_here generic.info never_really_used""" modules["failing_plugin2"] = failing_plugin2() class failing_plugin3(MuninPlugin): def config(self, conf): return """graph_title A plugin with two dses but only fetch value for one graph_args --base 1000 -l 0 fivemin.label 1 minute load onemin.label 5 minute load""" def fetch(self, conf): return "onemin.value 1" modules["failing_plugin3"] = failing_plugin3() class graph_area(MuninPlugin): "A plugin that uses STACK and AREA. From proc_pri. Use: testing the grapher" def fetch(self, conf): return """high.value 3 low.value 2 locked.value 1""" def config(self, conf): return """graph_title AREA and STACK graph_order low high locked graph_category graphtest graph_info This graph shows nuber of processes at each priority graph_args --base 1000 -l 0 graph_vlabel Number of processes high.label high priority high.draw STACK high.info The number of high-priority processes (tasks) low.label low priority low.draw AREA low.info The number of low-priority processes (tasks) locked.label locked in memory locked.draw STACK locked.info The number of processes that have pages locked into memory (for real-time and custom IO) """ modules["graph_area"] = graph_area() class utf8_graphcat(MuninPlugin): "A plugin with a graph category which has UTF-8 in it" def fetch(self, conf): return "apples.value %.2f" % self.find_load() def config(self, conf): return """graph_title Example UTF-8 graph graph_vlabel apples graph_category foo™ apples.label apples graph_info Apples eaten apples.info Apples eaten""" modules["utf8_graphcat"] = utf8_graphcat() class utf8_graphname(MuninPlugin): "A plugin with a UTF-8 name" def fetch(self, conf): return "apples.value %.2f" % self.find_load() def config(self, conf): return """graph_title Example UTF-8 graph graph_vlabel apples graph_category system apples.label apples graph_info Apples eaten apples.info Apples eaten""" modules["utf8_™graphname"] = utf8_graphname() class ArgumentTCPserver(SocketServer.ThreadingTCPServer): def __init__(self, server_address, RequestHandlerClass, args): SocketServer.ThreadingTCPServer.__init__(self,server_address, RequestHandlerClass) self.args = args class MuninHandler(SocketServer.StreamRequestHandler): """ Munin server implementation. This is based on munin_node.py by Chris Holcombe / http://sourceforge.net/projects/pythonmuninnode/ Possible commands: list, nodes, config, fetch, version or quit """ def handle(self): if self.server.args.get("verbose"): print "%s: Connection from %s:%s. server args is %s" \ % (self.server.args["name"], self.client_address[0], self.client_address[1], self.server.args) # slow path hostname = self.server.args["name"] full_hostname = hostname moduleprofile = self.server.args["pluginprofile"] modulenames = set(moduleprofile) self.wfile.write("# munin node at %s\n" % hostname) while True: line = self.rfile.readline().strip() try: cmd, args = line.split(" ", 1) except ValueError: cmd = line args = "" if not cmd or cmd == "quit": break if cmd == "list": # List all plugins that are available self.wfile.write(" ".join(self.server.args["plugins"].keys()) + "\n") elif cmd == "nodes": # We just support this host self.wfile.write("%s\n.\n" % full_hostname) elif cmd == "config": # display the config information of the plugin if not self.server.args["plugins"].has_key(args): self.wfile.write("# Unknown service\n.\n" ) else: config = self.server.args["plugins"][args].config(self.server.args) if config is None: self.wfile.write("# Unknown service\n.\n") else: self.wfile.write(config + "\n.\n") elif cmd == "fetch": # display the data information as returned by the plugin if not self.server.args["plugins"].has_key(args): self.wfile.write("# Unknown service\n.\n") else: data = self.server.args["plugins"][args].fetch(self.server.args) if data is None: self.wfile.write("# Unknown service\n.\n") else: self.wfile.write(data + "\n.\n") elif cmd == "version": # display the server version self.wfile.write("munin node on %s version: %s\n" % (full_hostname, VERSION)) else: self.wfile.write("# Unknown command. Try list, nodes, " \ "config, fetch, version or quit\n") def start_servers(instances): # TODO: Listen to IPv6 HOST = "0.0.0.0" servers = {} for iconf in instances: print "Setting up instance %s at port %s" \ % (iconf["name"], iconf["expanded_port"]) server = ArgumentTCPserver((HOST, iconf["expanded_port"]), MuninHandler, iconf) server_thread = threading.Thread(target=server.serve_forever) server_thread.daemon = True server_thread.start() servers[iconf["name"]] = server return servers def usage(): print "Usage: %s [--run] [--verbose] [--muninconf] " % sys.argv[0] def main(): if len(sys.argv) <= 2: usage() sys.exit(1) verbose = False if "--verbose" in sys.argv: verbose = True config = ConfigParser.RawConfigParser() for configfile in sys.argv[1:]: if not configfile.endswith(".conf"): continue if verbose: print "Reading config file %s" % configfile config.read(configfile) instancekeys = [ key for key in config.sections() if key.startswith("instance:") ] servers = {} instances = [] for key in instancekeys: instancename = key.split(":", 2)[1] portrange = [] if config.has_option(key, "port"): portrange = [ config.getint(key, "port") ] if config.has_option(key, "portrange"): rangestr = config.get(key, "portrange") ranges = rangestr.split("-") range_expanded = range(int(ranges[0]), int(ranges[1])+1, 1) portrange += range_expanded if len(portrange) == 0: print "WARN: No port or portrange defined for instance %s" \ % instancename pluginprofile = "pluginprofile:%s" % config.get(key, "pluginprofile") if not config.has_section(pluginprofile): print "WARN: Definition for pluginprofile %s not found, skipping" \ % config.get(key, "pluginprofile") continue plugins = {} tentative_pluginlist = config.get(pluginprofile, "plugins").split(",") assert(len(tentative_pluginlist) > 0) for tentative_plugin in tentative_pluginlist: tentative_plugin = tentative_plugin.strip() if not modules.has_key(tentative_plugin): print "WARN: Pluginprofile %s specifies unknown plugin %s" \ % (pluginprofile, tentative_plugin) continue # support more than one instanciation of the same plugin. plugininstancename = tentative_plugin i=2 while (plugins.has_key(plugininstancename)): plugininstancename = tentative_plugin + str(i) i += 1 plugins[plugininstancename] = modules[tentative_plugin] for portinstance in portrange: instanceconfig = dict() for k,v in config.items(key): instanceconfig[k] = v instanceconfig["plugins"] = plugins instanceconfig["verbose"] = verbose instanceconfig["name"] = "%s-%s" % (instancename, portinstance) instanceconfig["expanded_port"] = portinstance instances.append(instanceconfig) # XXX: need to store what handlers we should have. print instances # output sample munin config for the poller if "--muninconf" in sys.argv: for i in instances: print "[%s;%s]\n\taddress %s\n\tuse_node_name yes\n\tport %s\n" \ % ( "fromhell", i["name"], config.get("base","hostname"), i["port"]) if "--run" in sys.argv: if verbose: print "Starting up.." servers = start_servers(instances) try: while True: time.sleep(0.5) except KeyboardInterrupt: print "Caught Ctrl-c, shutting down.." for port, server in servers.items(): server.shutdown() sys.exit(0) if __name__ == "__main__": main()