#!/usr/bin/env python # shorewall_accounting v1.3 # # A munin plugin for tracking traffic as recorded by shorewall accounting rules. # See "man shorewall-accounting" for all possible accounting rules. # Basically this plugin examines the output of "shorewall -x show accounting". # See http://atlee.ca/blog/2006/01/20/munin-shorewall/ for a description of # the original script by Chris AtLee. # # Copyright 2010-2012 Lars Kruse # Copyright 2006 Chris AtLee # # Original publication: http://atlee.ca/blog/2006/01/20/munin-shorewall/ # Released under the GPL v3 or later # # You can use symlinks of this script according to the following pattern: # shorewall-accounting_prot: use only rules with a specific protocol # shorewall-accounting_in: use only rules with a specific input interface # shorewall-accounting_out: use only rules with a specific output interface # shorewall-accounting_source: use only rules with a specific source IP # shorewall-accounting_destination: use only rules with a specific destination IP # shorewall-accounting_details: use only rules with specific details (e.g. a port) # # Here "specific" means: non-default (e.g. a protocol was specified). # Combinations are allowed: # shorewall-accounting_prot_in_source: use only rules with a specific protocol, input interface and source IP # # Environment variables: # SHOREWALL_BIN - defaults to /sbin/shorewall # # Changelog: # v1.3 - 2012/04/02 # * renamed plugin file from "shorewall_accounting" to "shorewall-accounting_" # * CAUTION: rename your symlinks and plugin config section! # * added optional SHOREWALL_BIN environment variable # * improved labels for rules (don't mask dots) # #%# family=auto #%# capabilities=autoconf import sys import os import commands import re PLUGIN_BASE_NAME = "shorewall-accounting" SHOREWALL_BIN = os.environ.get("SHOREWALL_BIN", "/sbin/shorewall") ACCOUNTING_LINE_EXP = re.compile(r"^\s*\d+\s+(\d+)\s+(?P\w+)\s+(?P[\w-]+)\s+(?P[\w*]+)\s+(?P[\w*]+)\s+(?P[\w./+-]+)\s+(?P[\w./+-]+)\s*(?P
.*)\s*$") KEY_ORDER = ["prot", "in", "out", "source", "destination", "details"] FILTER_PATTERNS = { "prot": r"^all$", "in": r"^\*$", "out": r"^\*$", "source": r"^0\.0\.0\.0/0$", "destination": r"^0\.0\.0\.0/0$", "details": r"^$", } REPLACE_PATTERNS = { "prot": ("^all$", "allProt"), "in": (r"^\*$", "allIn"), "out": (r"^\*$", "allOut"), "source": (r"^0\.0\.0\.0/0", "allSrc"), "destination": (r"^0\.0\.0\.0/0", "allDst"), "details": (r"^multiport\s+", ""), } def get_accounting_rule_fieldname_and_label(regdict): items = [] # filter and clean all requested keys for key in KEY_ORDER: raw = regdict[key] pattern, replacement = REPLACE_PATTERNS[key] value = re.sub(pattern, replacement, raw).strip() if value: items.append(value) result = "_".join(items) # clean the fieldname: http://munin-monitoring.org/wiki/notes_on_datasource_names result = re.sub(r"^[^A-Za-z_]", "_", result) fieldname = re.sub(r"[^A-Za-z0-9_]", "_", result) # keep dots (for IP addresses) label = re.sub(r"[^A-Za-z0-9_\.]", "_", result) return fieldname, label def is_wanted(regdict, filter_list): for item in filter_list: # is the item empty? if not regdict[item]: return False # is the default value (unfiltered) set? if re.search(FILTER_PATTERNS[item], regdict[item]): return False return True def get_bytes_by_chain(filter_list): status, output = commands.getstatusoutput("'%s' -x show accounting" \ % SHOREWALL_BIN) if status != 0: raise OSError("Error running command (%s)[%i]: %s" % (SHOREWALL_BIN, status, output)) chains = {} for line in output.splitlines(): m = ACCOUNTING_LINE_EXP.match(line) if m is not None: # check if this line was filtered if not is_wanted(m.groupdict(), filter_list): continue fieldname, label = get_accounting_rule_fieldname_and_label(m.groupdict()) bytes = int(m.group(1)) if fieldname in chains: chains[fieldname][1] += bytes else: chains[fieldname] = [label, bytes] retval = [] names = chains.keys() names.sort() for name in names: retval.append((name, chains[name][0], chains[name][1])) return retval # extract the filters from the symlink's name # (e.g. "shorewall-accounting_in_out_details" -> in, out, details) call_name = os.path.basename(sys.argv[0]) if call_name.startswith(PLUGIN_BASE_NAME): suffix = call_name[len(PLUGIN_BASE_NAME):] suffixes = suffix.split("_") # use only suffixes that are listed in FILTER_PATTERNS filter_list = [item for item in suffixes if item in FILTER_PATTERNS] else: filter_list = [] if len(sys.argv) > 1: if sys.argv[1] == "autoconf": status, output = commands.getstatusoutput("'%s' -x show accounting" \ % SHOREWALL_BIN) if (status != 0) or not output: print "no" else: print "yes" elif sys.argv[1] == "config": if not filter_list: title_addon = "all" else: title_addon = " / ".join(filter_list) print "graph_title Shorewall accounting: %s" % title_addon print "graph_category network" print "graph_vlabel bits per ${graph_period}" for chain, label, bytes in get_bytes_by_chain(filter_list): label = " ".join([item for item in label.split("_") if not item.lower().startswith("all")]) if not label: label = "all" print "%s.min 0" % chain print "%s.type DERIVE" % chain print "%s.label %s" % (chain, label) print "%s.cdef %s,8,*" % (chain, chain) sys.exit(0) for chain, label, bytes in get_bytes_by_chain(filter_list): print "%s.value %i" % (chain, bytes)