diff --git a/tools/munin-node-from-hell/MIT-LICENSE b/tools/munin-node-from-hell/MIT-LICENSE new file mode 100644 index 00000000..89de3547 --- /dev/null +++ b/tools/munin-node-from-hell/MIT-LICENSE @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/tools/munin-node-from-hell/README.rst b/tools/munin-node-from-hell/README.rst new file mode 100644 index 00000000..5731e589 --- /dev/null +++ b/tools/munin-node-from-hell/README.rst @@ -0,0 +1,43 @@ +munin-node from hell +==================== + +This is a simple implementation of a munin node (http://munin-monitoring.org/) +that is made to give the polling server a hard time. + +In practice this is the munin-node that we use to develop and test the awesome +stuff we do at http://hostedmunin.com/ . Use it as you feel fit :) + +Current features controlled via config file: + +* Respond slowly or never to queries. +* Have plugins that always are in warning or alarm. +* Extensive number of plugins. +* Run on multiple ports at the same time, to test huge amounts of clients. + + +Usage +----- + +munin-node-from-hell takes two arguments; the mode and which config file to +use. Mode is either --run or --muninconf. + +This software is meant to run as an ordinary unix user, please don't run +it as root without some thought. + +You probably want: + + ./munin-node-from-hell --run simple.conf + +To make a config snippet to put in munin.conf: + + ./munin-node-from-hell --muninconf simple.conf > snippet.conf + +License +------- + +See the file MIT-LICENSE for details. + +Contact +------- + +Lasse Karstensen diff --git a/tools/munin-node-from-hell/example-config.conf b/tools/munin-node-from-hell/example-config.conf new file mode 100644 index 00000000..180b0530 --- /dev/null +++ b/tools/munin-node-from-hell/example-config.conf @@ -0,0 +1,68 @@ +# example config file for muninnode-from-hell. +# +# Flow: +# * basic stuff in [base]. +# * a set of plugins should be defined in a [pluginprofile:PROFILENAME] +# * an instance (ie, server running on a local port) is added with +# [instance:INSTANCENAME] +# +# Instances has: +# port = XXXX +# AND/OR +# portrange = 2000-2005 +# +# mode = sleepy|exp +# sleepyness = 10 # when mode=sleepy, this is the uniform sleep time. +# lambd = 10 # when mode=exp, this is the mean sleep time. +# exp is good to emulate load peaks, it can easily sleep 0.02 or 20 seconds. +# (but less often 30 :)) + +[base] +# when building an example config with --muninconf, what hostname to output. +hostname = localhost + +[pluginprofile:tarpit++] +plugins = tarpit, load, locks, locks, load, tarpit, load, locks, locks, load, load, load + +[pluginprofile:base] +plugins = load, locks, locks, load, load, locks, locks, load, load, load + +[pluginprofile:manyservices] +plugins = load, locks, locks, load, load, locks, locks, load, load, load, load, locks, locks, load, load, locks, locks, load, load, load, load, locks, locks, load, load, locks, locks, load, load, load, load, locks, locks, load, load, locks, locks, load, load, load, load, locks, locks, load, load, locks, locks, load, load, load, load, locks, locks, load, load, locks, locks, load, load, load, load, locks, locks, load, load, locks, locks, load, load, load, load, locks, locks, load, load, locks, locks, load, load, load + +[instance:bar] +pluginprofile = base +port = 4000 +mode = sleepy +# 40*10 should be aborted due to server time constraints. +sleepyness = 40 +#lambd = 5 + +[instance:25sec] +pluginprofile = base +port = 4001 +mode = exp +#sleepyness = 30 +# mean 2sec +lambd = 2 + + +[instance:baz] +pluginprofile = base +pluginmultiplier = 2 +port = 4940 +mode = sleepy +sleepyness = 5 +#10 +#lambd = 5 + +# bringer of chaos +[instance:qux] +pluginprofile = base +port = 4948 +mode = exp +lambd = 10 + +[instance:tarpit] +pluginprofile = tarpit++ +port = 3000 diff --git a/tools/munin-node-from-hell/muninnode-from-hell b/tools/munin-node-from-hell/muninnode-from-hell new file mode 100755 index 00000000..187c4138 --- /dev/null +++ b/tools/munin-node-from-hell/muninnode-from-hell @@ -0,0 +1,290 @@ +#!/usr/bin/python +# +# 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 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) + + +class load(MuninPlugin): + def fetch(self, conf): + self.sleep_fetch(conf) + load = open("/proc/loadavg", "r").read() + load, rest = load.split(" ", 1) + load = float(load) + return "load.value %.2f" % 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) + fp = open("/proc/locks", "r") + fdata = fp.readlines() + return "locks.value %i" % len(fdata) + + 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): + def fetch(self, conf): + return "generic.value 10" + + def config(self, conf): + return """graph_title Always in warning +graph_vlabel Level +graph_scale no +graph_info A simple graph that is always in warning or alarm +graph_category active_notification +generic.label Level +generic.info Level usually above warning level +generic.warn 5 +generic.crit 10""" +modules["always_warning"] = always_warning() + +class always_alarm(always_warning): + def fetch(self, conf): + return "generic.value 20" +modules["always_alarm"] = always_alarm() + + +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): + 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] [--muninconf] " % sys.argv[0] + +def main(): + if len(sys.argv) <= 2: + usage() + sys.exit(1) + + config = ConfigParser.RawConfigParser() + config.read(sys.argv[2]) + + 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["name"] = "%s-%s" % (instancename, portinstance) + instanceconfig["expanded_port"] = portinstance + + instances.append(instanceconfig) + # XXX: need to store what handlers we should have. + + # 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: + 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() diff --git a/tools/munin-node-from-hell/notifications.conf b/tools/munin-node-from-hell/notifications.conf new file mode 100644 index 00000000..5a7b2c91 --- /dev/null +++ b/tools/munin-node-from-hell/notifications.conf @@ -0,0 +1,19 @@ +# Example config file for muninnode-from-hell. +# +# Run an instance with plugins that always has some +# notification level. (unknown / warning / critical) + +[instance:notifications] +pluginprofile = notif +port = 3000 + +# +#[instance:baz] +#port = 4940 +#sleepyness = 30 +[pluginprofile:notif] +plugins = always_warning, always_alarm + +[base] +# when building an example config with --muninconf, what hostname to output. +hostname = localhost diff --git a/tools/munin-node-from-hell/simple.conf b/tools/munin-node-from-hell/simple.conf new file mode 100644 index 00000000..ad95c296 --- /dev/null +++ b/tools/munin-node-from-hell/simple.conf @@ -0,0 +1,16 @@ +# Example config file for muninnode-from-hell. +# +# This is the simplest possible config, just run an ordinary munin-node +# with a trivial amount of plugins on a single port. +# + +[instance:simple] +pluginprofile = base +port = 4000 + +[pluginprofile:base] +plugins = load, locks + +[base] +# when building an example config with --muninconf, what hostname to output. +hostname = localhost diff --git a/tools/munin-node-from-hell/tarpit.conf b/tools/munin-node-from-hell/tarpit.conf new file mode 100644 index 00000000..3a0995de --- /dev/null +++ b/tools/munin-node-from-hell/tarpit.conf @@ -0,0 +1,16 @@ +# Config file for muninnode-from-hell. +# + +[instance:tarpit] +pluginprofile = tarpit +port = 3000 + +[pluginprofile:tarpit] +plugins = tarpit, load, locks + +[pluginprofile:base] +plugins = load, locks, locks, load, load, locks, locks, load, load, load + +[base] +# when building an example config with --muninconf, what hostname to output. +hostname = localhost