├── trapdoor ├── static │ ├── favicon.ico │ ├── images │ │ └── skulls │ │ │ └── skulls.png │ ├── js │ │ └── hb_templates.min.js │ └── css │ │ └── trapdoor.css ├── __init__.py ├── Makefile.inc ├── templates │ ├── errors │ │ └── notfound.html │ ├── base.html │ └── index.html ├── handlebars │ └── trap_details.handlebars ├── routes.py ├── settings.py ├── utils.py └── handlers.py ├── trapperkeeper ├── cmds │ ├── __init__.py │ └── sync_db.py ├── version.py ├── __init__.py ├── exceptions.py ├── templates │ ├── default_email_text.tmpl │ └── default_email_html.tmpl ├── constants.py ├── config.py ├── dde.py ├── utils.py ├── callbacks.py └── models.py ├── images └── trapdoor.png ├── Makefile ├── MANIFEST.in ├── requirements.txt ├── .gitignore ├── LICENSE ├── tests ├── mibs │ ├── TRAPPERKEEPER-MIB.my │ └── DROPBOX-SMI.my └── send_traps.sh ├── conf ├── trapdoor.yaml └── trapperkeeper.yaml ├── setup.py ├── bin ├── trapdoor └── trapperkeeper ├── README.md └── README /trapdoor/static/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /trapperkeeper/cmds/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /trapperkeeper/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.6.1" 2 | -------------------------------------------------------------------------------- /trapperkeeper/__init__.py: -------------------------------------------------------------------------------- 1 | from version import __version__ 2 | -------------------------------------------------------------------------------- /trapdoor/__init__.py: -------------------------------------------------------------------------------- 1 | from trapperkeeper.version import __version__ 2 | -------------------------------------------------------------------------------- /images/trapdoor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/trapperkeeper/HEAD/images/trapdoor.png -------------------------------------------------------------------------------- /trapperkeeper/exceptions.py: -------------------------------------------------------------------------------- 1 | class Error(Exception): 2 | pass 3 | 4 | class ConfigError(Error): 5 | pass 6 | -------------------------------------------------------------------------------- /trapdoor/static/images/skulls/skulls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/trapperkeeper/HEAD/trapdoor/static/images/skulls/skulls.png -------------------------------------------------------------------------------- /trapdoor/Makefile.inc: -------------------------------------------------------------------------------- 1 | .PHONY: handlebars 2 | 3 | handlebars: 4 | handlebars -mf trapdoor/static/js/hb_templates.min.js trapdoor/handlebars/*.handlebars 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: README 2 | 3 | all: README handlebars 4 | 5 | README: 6 | pandoc --from=markdown --to=rst --output=README README.md 7 | 8 | include trapdoor/Makefile.inc 9 | 10 | -------------------------------------------------------------------------------- /trapdoor/templates/errors/notfound.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

(404) Not Found.

6 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /trapperkeeper/templates/default_email_text.tmpl: -------------------------------------------------------------------------------- 1 | {{ trap.oid|to_mibname }} 2 | 3 | Source Host: {{ trap.host|hostname_or_ip }} 4 | Destination Host: {{ dest_host }} 5 | 6 | {% for varbind in trap.varbinds %} 7 | {{ varbind.oid|to_mibname }}: {{ varbind|varbind_value }} 8 | {% endfor %} 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README 3 | include README.md 4 | include requirements.txt 5 | recursive-include tests * 6 | recursive-include trapdoor/handlebars * 7 | recursive-include trapdoor/static * 8 | recursive-include trapdoor/templates * 9 | recursive-include trapperkeeper/templates * 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Jinja2==2.7.1 2 | MarkupSafe==0.18 3 | MySQL-python==1.2.5 4 | PyYAML==3.10 5 | SQLAlchemy==0.9.1 6 | backports.ssl-match-hostname==3.4.0.2 7 | expvar==0.0.2 8 | oid-translate==0.2.2 9 | pyasn1==0.1.7 10 | pycrypto==2.6.1 11 | pysnmp==4.2.5 12 | pytz==2014.1 13 | tornado==3.2 14 | wsgiref==0.1.2 15 | -------------------------------------------------------------------------------- /trapdoor/handlebars/trap_details.handlebars: -------------------------------------------------------------------------------- 1 | {{#ifEmpty varbinds}} 2 | No Variable Bindings... 3 | {{else}} 4 | {{#each varbinds}} 5 |
6 |
{{mibFormat this.name}}
7 |
{{this.pretty_value}}
8 |
9 | {{/each}} 10 | {{/ifEmpty}} 11 | -------------------------------------------------------------------------------- /trapperkeeper/templates/default_email_html.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 |

{{ trap.oid|to_mibname }}

4 | 5 | Source Host: {{ trap.host|hostname_or_ip }}
6 | Destination Host: {{ dest_host }}
7 |
8 | 9 | {% for varbind in trap.varbinds %} 10 | 11 | 12 | 13 | 14 | {% endfor %} 15 |
{{ varbind.oid|to_mibname }}{{ varbind|varbind_value }}
16 | -------------------------------------------------------------------------------- /trapdoor/routes.py: -------------------------------------------------------------------------------- 1 | 2 | from trapdoor import handlers 3 | 4 | HANDLERS = [ 5 | (r"/", handlers.Index), 6 | (r"/resolve/?", handlers.Resolve), 7 | (r"/resolve_all/?", handlers.ResolveAll), 8 | 9 | # API 10 | (r"/api/varbinds/(?P\d+)", handlers.ApiVarBinds), 11 | (r"/api/activetraps/?", handlers.ApiActiveTraps), 12 | (r"/api/traps/?", handlers.ApiTraps), 13 | 14 | # Default 15 | (r"/.*", handlers.NotFound), 16 | ] 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | *.py[cod] 3 | *.log 4 | *.sqlite 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Packages 10 | *.egg 11 | *.egg-info 12 | dist 13 | build 14 | eggs 15 | parts 16 | var 17 | sdist 18 | develop-eggs 19 | .installed.cfg 20 | lib 21 | lib64 22 | __pycache__ 23 | 24 | # Installer logs 25 | pip-log.txt 26 | 27 | # Unit test / coverage reports 28 | .coverage 29 | .tox 30 | nosetests.xml 31 | 32 | # Translations 33 | *.mo 34 | 35 | # Mr Developer 36 | .mr.developer.cfg 37 | .project 38 | .pydevproject 39 | 40 | tests/mibs/.index 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Dropbox, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /tests/mibs/TRAPPERKEEPER-MIB.my: -------------------------------------------------------------------------------- 1 | TRAPPERKEEPER-MIB DEFINITIONS ::= BEGIN 2 | IMPORTS applications FROM DROPBOX-SMI; 3 | 4 | trapperkeeper OBJECT IDENTIFIER ::= { applications 1 } 5 | 6 | testtraps OBJECT IDENTIFIER ::= { trapperkeeper 1 } 7 | 8 | testV1Trap TRAP-TYPE 9 | ENTERPRISE testtraps 10 | VARIABLES { sysLocation } 11 | DESCRIPTION "Used for testing v1 traps on trapperkeeper" 12 | ::= 1 13 | 14 | testV2Trap NOTIFICATION-TYPE 15 | OBJECTS { sysLocation } 16 | STATUS current 17 | DESCRIPTION "An example of an SMIv2 notification" 18 | ::= { testtraps 1 } 19 | 20 | END 21 | -------------------------------------------------------------------------------- /trapperkeeper/cmds/sync_db.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from trapperkeeper import models 4 | from trapperkeeper import config 5 | 6 | if __name__ == "__main__": 7 | 8 | parser = argparse.ArgumentParser(description="Create schema on configured database.") 9 | parser.add_argument("-c", "--config", default="/etc/trapperkeeper.yaml", 10 | help="Path to config file.") 11 | args = parser.parse_args() 12 | 13 | config = config.Config.from_file(args.config, False) 14 | 15 | db_engine = models.get_db_engine(config["database"]) 16 | models.Model.metadata.create_all(db_engine) 17 | -------------------------------------------------------------------------------- /trapdoor/settings.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | import yaml 3 | 4 | settings = { 5 | "debug": False, 6 | "database": None, 7 | "num_processes": 1, 8 | "port": 8888, 9 | "timezone": pytz.timezone("UTC"), 10 | "date_format": "%Y-%m-%d %I:%M %p", 11 | } 12 | 13 | def update_from_config(filename): 14 | with open(filename) as config: 15 | data = yaml.safe_load(config.read()) 16 | 17 | for key, value in data.iteritems(): 18 | key = key.lower() 19 | 20 | if key not in settings: 21 | continue 22 | 23 | if key == "timezone": 24 | try: 25 | settings["timezone"] = pytz.timezone(value) 26 | except pytz.exceptions.UnknownTimeZoneError: 27 | continue 28 | else: 29 | settings[key] = value 30 | -------------------------------------------------------------------------------- /tests/mibs/DROPBOX-SMI.my: -------------------------------------------------------------------------------- 1 | DROPBOX-SMI DEFINITIONS ::= BEGIN 2 | 3 | IMPORTS 4 | MODULE-IDENTITY, 5 | OBJECT-IDENTITY, 6 | enterprises 7 | FROM SNMPv2-SMI; 8 | 9 | dropbox MODULE-IDENTITY 10 | 11 | LAST-UPDATED "201403130000Z" 12 | ORGANIZATION "Dropbox, Inc." 13 | CONTACT-INFO 14 | "Dropbox 15 | 16 | E-mail: pen-contact@dropbox.com" 17 | 18 | DESCRIPTION 19 | "The Structure of Management Information for the 20 | Dropbox enterprise." 21 | 22 | REVISION "201403130000Z" 23 | DESCRIPTION 24 | "Initial version of this module." 25 | 26 | ::= { enterprises 42921 } 27 | 28 | 29 | applications OBJECT-IDENTITY 30 | STATUS current 31 | DESCRIPTION 32 | "Namespace for various applications at Dropbox." 33 | ::= { dropbox 1 } 34 | 35 | END 36 | -------------------------------------------------------------------------------- /tests/send_traps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BASE_DIR=$( dirname $0 ) 4 | 5 | DEST="localhost" 6 | IPv6_DEST="udp6:[::1]:162" 7 | COMMON_OPTS="-Ln -mTRAPPERKEEPER-MIB -M+"${BASE_DIR}/mibs" -c public" 8 | PRIV_COMMON_OPTS="-Ln -mTRAPPERKEEPER-MIB -M+"${BASE_DIR}/mibs" -c private" 9 | SYSLOC_VARBIND="SNMPv2-MIB::sysLocation.0 s TrapperKeeper-Test" 10 | 11 | snmptrap -v 1 $COMMON_OPTS $DEST TRAPPERKEEPER-MIB::testtraps "" 6 1 "" $SYSLOC_VARBIND 12 | snmptrap -v 2c $COMMON_OPTS $DEST "" TRAPPERKEEPER-MIB::testV2Trap $SYSLOC_VARBIND 13 | snmptrap -v 1 $PRIV_COMMON_OPTS $DEST TRAPPERKEEPER-MIB::testtraps "" 6 1 "" $SYSLOC_VARBIND 14 | 15 | snmptrap -v 1 $COMMON_OPTS $IPv6_DEST TRAPPERKEEPER-MIB::testtraps "" 6 1 "" $SYSLOC_VARBIND 16 | snmptrap -v 2c $COMMON_OPTS $IPv6_DEST "" TRAPPERKEEPER-MIB::testV2Trap $SYSLOC_VARBIND 17 | snmptrap -v 1 $PRIV_COMMON_OPTS $IPv6_DEST TRAPPERKEEPER-MIB::testtraps "" 6 1 "" $SYSLOC_VARBIND 18 | -------------------------------------------------------------------------------- /trapdoor/static/js/hb_templates.min.js: -------------------------------------------------------------------------------- 1 | !function(){var a=Handlebars.template,n=Handlebars.templates=Handlebars.templates||{};n.trap_details=a(function(a,n,r,i,e){function s(){return"\n No Variable Bindings...\n"}function t(a,n){var i,e="";return e+="\n ",i=r.each.call(a,a&&a.varbinds,{hash:{},inverse:f.noop,fn:f.program(4,l,n),data:n}),(i||0===i)&&(e+=i),e+="\n"}function l(a,n){var i,e,s,t="";return t+='\n
\n
'+d((e=r.mibFormat||a&&a.mibFormat,s={hash:{},data:n},e?e.call(a,a&&a.name,s):h.call(a,"mibFormat",a&&a.name,s)))+'
\n
'+d((i=a&&a.pretty_value,typeof i===v?i.apply(a):i))+"
\n
\n "}this.compilerInfo=[4,">= 1.0.0"],r=this.merge(r,a.helpers),e=e||{};var o,m,p,c="",h=r.helperMissing,d=this.escapeExpression,v="function",f=this;return m=r.ifEmpty||n&&n.ifEmpty,p={hash:{},inverse:f.program(3,t,e),fn:f.program(1,s,e),data:e},o=m?m.call(n,n&&n.varbinds,p):h.call(n,"ifEmpty",n&&n.varbinds,p),(o||0===o)&&(c+=o),c+="\n"})}(); -------------------------------------------------------------------------------- /conf/trapdoor.yaml: -------------------------------------------------------------------------------- 1 | # Takes a SqlAlchemy URL to the database to store traps. More details 2 | # can be found at the following URL: 3 | # http://docs.sqlalchemy.org/en/rel_0_9/core/engines.html#database-urls 4 | # 5 | # Type: str 6 | database: "sqlite:///trapperkeeper.sqlite" 7 | 8 | # Number of worker processes to fork for receving requests. This option 9 | # is mutually exclusive with debug. 10 | # 11 | # Type: int 12 | num_processes: 8 13 | 14 | # The port to listen to requests on. 15 | # 16 | # Type: int 17 | port: 8888 18 | 19 | # All traps are stored in the database in UTC. This option chooses the 20 | # timezone for displaying datetime values. 21 | # 22 | # Type: str 23 | timezone: "America/Los_Angeles" 24 | 25 | # The format in which to display dates in the interface. More details here: 26 | # https://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior 27 | # 28 | # Type: str 29 | date_format: "%Y-%m-%d %I:%M %p" 30 | 31 | # Passing debug option down tornado. Useful for development to 32 | # automatically reload code. 33 | # 34 | # Type: bool 35 | debug: false 36 | -------------------------------------------------------------------------------- /trapperkeeper/constants.py: -------------------------------------------------------------------------------- 1 | from pysnmp.proto import rfc1155 2 | from pysnmp.proto import rfc1902 3 | from pyasn1.type import univ 4 | 5 | 6 | SEVERITIES = ("informational", "warning", "critical") 7 | 8 | SNMP_VERSIONS = { 9 | 0: "v1", 10 | 1: "v2c", 11 | } 12 | 13 | SNMP_TRAP_OID = "1.3.6.1.6.3.1.1.4.1.0" 14 | 15 | ASN_TO_NAME_MAP = { 16 | rfc1902.OctetString: "octet", 17 | univ.OctetString: "octet", 18 | rfc1902.TimeTicks: "timeticks", 19 | rfc1902.Integer: "integer", 20 | univ.ObjectIdentifier: "oid", 21 | rfc1902.IpAddress: "ipaddress", 22 | rfc1155.IpAddress: "ipaddress", 23 | univ.Boolean: "boolean", 24 | univ.BitString: "bit", 25 | rfc1902.Unsigned32: "unsigned", 26 | univ.Null: "null", 27 | rfc1155.Opaque: "opaque", 28 | rfc1902.Opaque: "opaque", 29 | rfc1155.Counter: "counter", 30 | rfc1902.Counter32: "counter", 31 | rfc1902.Counter64: "counter64", 32 | } 33 | 34 | NAME_TO_PY_MAP = { 35 | "octet": str, 36 | "oid": str, 37 | "opaque": str, 38 | "ipaddress": str, 39 | "bit": str, 40 | "timeticks": float, 41 | "integer": long, 42 | "unsigned": long, 43 | "counter": long, 44 | "counter64": long, 45 | "boolean": bool, 46 | "null": lambda x: None, 47 | } 48 | -------------------------------------------------------------------------------- /trapdoor/utils.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | import tornado.web 3 | import urllib 4 | 5 | from trapdoor.settings import settings 6 | 7 | 8 | class TrapdoorHandler(tornado.web.RequestHandler): 9 | 10 | def initialize(self): 11 | self.db = self.application.my_settings.get("db_session")() 12 | self.debug = self.application.my_settings.get("debug", False) 13 | self.debug_user = self.application.my_settings.get("debug_user") 14 | 15 | def on_finish(self): 16 | self.db.close() 17 | 18 | def render_template(self, template_name, **kwargs): 19 | template = self.application.my_settings["template_env"].get_template(template_name) 20 | content = template.render(kwargs) 21 | return content 22 | 23 | def render(self, template_name, **kwargs): 24 | kwargs.update(self.get_template_namespace()) 25 | self.write(self.render_template(template_name, **kwargs)) 26 | 27 | def notfound(self): 28 | self.set_status(404) 29 | self.render("errors/notfound.html") 30 | 31 | 32 | def print_date(date_obj): 33 | if date_obj is None: 34 | return "" 35 | 36 | date_obj = date_obj.astimezone(settings["timezone"]) 37 | return date_obj.strftime(settings["date_format"]) 38 | 39 | 40 | jinja2_filters = { 41 | "print_date": print_date, 42 | } 43 | 44 | 45 | def update_qs(qs, **kwargs): 46 | qs = qs.copy() 47 | qs.update(kwargs) 48 | return "?" + urllib.urlencode(qs, True) 49 | 50 | jinja2_globals = { 51 | "update_qs": update_qs, 52 | } 53 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import setuptools 5 | from distutils.core import setup 6 | 7 | execfile('trapperkeeper/version.py') 8 | 9 | with open('requirements.txt') as requirements: 10 | required = requirements.read().splitlines() 11 | 12 | 13 | package_data = {} 14 | def get_package_data(package, base_dir): 15 | for dirpath, dirnames, filenames in os.walk(base_dir): 16 | dirpath = dirpath[len(package)+1:] # Strip package dir 17 | for filename in filenames: 18 | package_data.setdefault(package, []).append(os.path.join(dirpath, filename)) 19 | for dirname in dirnames: 20 | get_package_data(package, dirname) 21 | 22 | get_package_data("trapdoor", "trapdoor/handlebars") 23 | get_package_data("trapdoor", "trapdoor/static") 24 | get_package_data("trapdoor", "trapdoor/templates") 25 | get_package_data("trapperkeeper", "trapperkeeper/templates") 26 | 27 | kwargs = { 28 | "name": "trapperkeeper", 29 | "version": str(__version__), 30 | "packages": ["trapperkeeper", "trapperkeeper.cmds", "trapdoor"], 31 | "package_data": package_data, 32 | "scripts": ["bin/trapperkeeper", "bin/trapdoor"], 33 | "description": "SNMP Trap Daemon.", 34 | # PyPi, despite not parsing markdown, will prefer the README.md to the 35 | # standard README. Explicitly read it here. 36 | "long_description": open("README").read(), 37 | "author": "Gary M. Josack", 38 | "maintainer": "Gary M. Josack", 39 | "author_email": "gary@dropbox.com", 40 | "maintainer_email": "gary@dropbox.com", 41 | "license": "Apache", 42 | "install_requires": required, 43 | "url": "https://github.com/dropbox/trapperkeeper", 44 | "download_url": "https://github.com/dropbox/trapperkeeper/archive/master.tar.gz", 45 | "classifiers": [ 46 | "Programming Language :: Python", 47 | "Topic :: Software Development", 48 | "Topic :: Software Development :: Libraries", 49 | "Topic :: Software Development :: Libraries :: Python Modules", 50 | ] 51 | } 52 | 53 | setup(**kwargs) 54 | -------------------------------------------------------------------------------- /trapdoor/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block title %} Trapdoor {% endblock %} 5 | 6 | 7 | 8 | {% block extra_head %} {% endblock %} 9 | 10 | 11 |
12 |
13 | 16 |
17 | 18 |
19 | {% block content %} {% endblock %} 20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 50 | 51 | {% block script %} {% endblock %} 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /trapperkeeper/config.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import copy 3 | import logging 4 | from oid_translate import ObjectId 5 | import yaml 6 | 7 | from trapperkeeper.exceptions import ConfigError 8 | 9 | 10 | class Config(object): 11 | 12 | REQUIRED = set(["database", "trap_port", "stats_port"]) 13 | 14 | def __init__(self, config, handlers): 15 | self._config = config 16 | self.handlers = handlers 17 | 18 | for required in Config.REQUIRED: 19 | if required not in self: 20 | raise ConfigError("Invalid Config. Missing %s" % required) 21 | 22 | def __getitem__(self, index): 23 | return self._config[index] 24 | 25 | def __contains__(self, elem): 26 | return elem in self._config 27 | 28 | def get(self, key, default=None): 29 | return self._config.get(key, default) 30 | 31 | @staticmethod 32 | def from_file(config_filename, handlers=True): 33 | with open(config_filename) as config_file: 34 | config = yaml.safe_load(config_file) 35 | if handlers: 36 | _handlers = Handlers.from_dict(config) 37 | else: 38 | _handlers = None 39 | return Config(config.get("config", {}), _handlers) 40 | 41 | 42 | class Handlers(object): 43 | 44 | def __init__(self, defaults=None, traphandlers=None): 45 | self._defaults = defaults or {} 46 | self._traphandlers = traphandlers or {} 47 | 48 | def __getitem__(self, index): 49 | if not index.startswith("."): 50 | index = "." + index 51 | return self._traphandlers.get(index, self._defaults) 52 | 53 | @staticmethod 54 | def update(original, update_from): 55 | for key, value in update_from.iteritems(): 56 | if isinstance(value, collections.Mapping): 57 | original[key] = Handlers.update(original.get(key, {}), value) 58 | else: 59 | original[key] = update_from[key] 60 | return original 61 | 62 | @staticmethod 63 | def from_dict(config): 64 | defaults = { 65 | "expiration": "15m", 66 | "mail": { 67 | "subject": "%(hostname)s %(trap_name)s", 68 | }, 69 | "severity": "warning", 70 | "blackhole": False, 71 | "mail_on_duplicate": False, 72 | } 73 | Handlers.update(defaults, config.get("defaults", {})) 74 | 75 | templates = config.get("templates", {}) 76 | traphandlers = {} 77 | 78 | for name, config in config.get("traphandlers", {}).iteritems(): 79 | full_config = copy.deepcopy(defaults) 80 | if "use" in config: 81 | template = config.pop("use") 82 | Handlers.update(full_config, copy.deepcopy( 83 | templates.get(template, {}) 84 | )) 85 | Handlers.update(full_config, config) 86 | try: 87 | oid = ObjectId(name).oid 88 | traphandlers[oid] = full_config 89 | except ValueError, err: 90 | logging.warning("Failed to process traphandler %s: %s", name, err) 91 | 92 | return Handlers(defaults, traphandlers) 93 | -------------------------------------------------------------------------------- /trapperkeeper/dde.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from oid_translate import ObjectId 3 | 4 | 5 | class DdeNotification(object): 6 | def __init__(self, notification, handler): 7 | self._notification = notification 8 | self.handler = deepcopy(handler) 9 | 10 | @property 11 | def host(self): 12 | return self._notification.host 13 | 14 | @property 15 | def sent(self): 16 | return self._notification.sent 17 | 18 | @property 19 | def trap_type(self): 20 | return self._notification.trap_type 21 | 22 | @property 23 | def request_id(self): 24 | return self._notification.request_id 25 | 26 | @property 27 | def version(self): 28 | return self._notification.version 29 | 30 | @property 31 | def notification(self): 32 | return ObjectId(self._notification.oid) 33 | 34 | @property 35 | def varbinds(self): 36 | return [ 37 | (ObjectId(varbind.oid), varbind.value_type, varbind.value) 38 | for varbind in self._notification.varbinds 39 | ] 40 | 41 | @property 42 | def severity(self): 43 | return self.handler["severity"] 44 | 45 | @severity.setter 46 | def severity(self, severity): 47 | self.handler["severity"] = severity 48 | 49 | @property 50 | def expiration(self): 51 | return self.handler["expiration"] 52 | 53 | @expiration.setter 54 | def expiration(self, expiration): 55 | self.handler["expiration"] = expiration 56 | 57 | @property 58 | def blackhole(self): 59 | return self.handler["blackhole"] 60 | 61 | @blackhole.setter 62 | def blackhole(self, blackhole): 63 | self.handler["blackhole"] = blackhole 64 | 65 | @property 66 | def mail_recipients(self): 67 | return self.handler.get("mail", {}).get("recipients") 68 | 69 | @mail_recipients.setter 70 | def mail_recipients(self, recipients): 71 | if "mail" not in self.handler: 72 | self.handler["mail"] = {} 73 | self.handler["mail"]["recipients"] = recipients 74 | 75 | @property 76 | def mail_subject(self): 77 | return self.handler.get("mail", {}).get("subject") 78 | 79 | @mail_subject.setter 80 | def mail_subject(self, subject): 81 | if "mail" not in self.handler: 82 | self.handler["mail"] = {} 83 | self.handler["mail"]["subject"] = subject 84 | 85 | 86 | # Deprecated handlers 87 | 88 | def set_severity(self, severity): 89 | self.handler["severity"] = severity 90 | 91 | def set_expiration(self, expiration): 92 | self.handler["expiration"] = expiration 93 | 94 | def set_blackhole(self, blackhole): 95 | self.handler["blackhole"] = blackhole 96 | 97 | def set_mail_recipients(self, recipients): 98 | if "mail" not in self.handler: 99 | self.handler["mail"] = {} 100 | self.handler["mail"]["recipients"] = recipients 101 | 102 | def set_mail_subject(self, subject): 103 | if "mail" not in self.handler: 104 | self.handler["mail"] = {} 105 | self.handler["mail"]["subject"] = subject 106 | 107 | 108 | -------------------------------------------------------------------------------- /bin/trapdoor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import logging 5 | from oid_translate import load_mibs 6 | import os 7 | import sys 8 | import tornado.ioloop 9 | import tornado.httpserver 10 | import tornado.web 11 | 12 | import trapdoor 13 | from trapdoor.routes import HANDLERS 14 | from trapdoor.utils import jinja2_filters, jinja2_globals 15 | from trapdoor.settings import settings, update_from_config 16 | from trapperkeeper.models import get_db_engine, Session 17 | from trapperkeeper.utils import get_template_env, get_loglevel, CachingResolver 18 | 19 | 20 | 21 | sa_log = logging.getLogger("sqlalchemy.engine.base.Engine") 22 | 23 | 24 | class Application(tornado.web.Application): 25 | def __init__(self, *args, **kwargs): 26 | self.my_settings = kwargs.pop("my_settings", {}) 27 | super(Application, self).__init__(*args, **kwargs) 28 | 29 | 30 | def main(argv): 31 | 32 | parser = argparse.ArgumentParser(description="SNMP Trap Web Viewer.") 33 | parser.add_argument("-c", "--config", default="/etc/trapdoor.yaml", 34 | help="Path to config file.") 35 | parser.add_argument("-v", "--verbose", action="count", default=0, help="Increase logging verbosity.") 36 | parser.add_argument("-q", "--quiet", action="count", default=0, help="Decrease logging verbosity.") 37 | parser.add_argument("-V", "--version", action="version", 38 | version="%%(prog)s %s" % trapdoor.__version__, 39 | help="Display version information.") 40 | args = parser.parse_args() 41 | update_from_config(args.config) 42 | 43 | if settings["debug"] and settings["num_processes"] > 1: 44 | logging.fatal("Debug mode does not support multiple processes.") 45 | sys.exit(1) 46 | 47 | tornado_settings = { 48 | "static_path": os.path.join(os.path.dirname(trapdoor.__file__), "static"), 49 | "debug": settings["debug"], 50 | "xsrf_cookies": True, 51 | } 52 | 53 | resolver = CachingResolver(timeout=300) 54 | template_env = get_template_env( 55 | "trapdoor", 56 | hostname_or_ip=resolver.hostname_or_ip, 57 | **jinja2_filters 58 | ) 59 | template_env.globals.update(jinja2_globals) 60 | 61 | db_engine = get_db_engine(settings["database"]) 62 | Session.configure(bind=db_engine) 63 | my_settings = { 64 | "debug": settings["debug"], 65 | "db_engine": db_engine, 66 | "db_session": Session, 67 | "template_env": template_env, 68 | } 69 | 70 | log_level = get_loglevel(args) 71 | logging.basicConfig( 72 | level=log_level, 73 | format="%(asctime)-15s\t%(levelname)s\t%(message)s" 74 | ) 75 | 76 | if log_level < 0: 77 | sa_log.setLevel(logging.INFO) 78 | 79 | application = Application(HANDLERS, my_settings=my_settings, **tornado_settings) 80 | 81 | server = tornado.httpserver.HTTPServer(application) 82 | server.bind(settings["port"]) 83 | server.start(settings["num_processes"]) 84 | try: 85 | load_mibs() 86 | tornado.ioloop.IOLoop.instance().start() 87 | except KeyboardInterrupt: 88 | tornado.ioloop.IOLoop.instance().stop() 89 | finally: 90 | print "Bye" 91 | 92 | 93 | if __name__ == "__main__": 94 | main(sys.argv) 95 | -------------------------------------------------------------------------------- /bin/trapperkeeper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | from expvar.stats import stats 5 | import logging 6 | import oid_translate 7 | from pysnmp.carrier.asynsock.dispatch import AsynsockDispatcher 8 | from pysnmp.carrier.asynsock.dgram import udp, udp6 9 | import threading 10 | import tornado.ioloop 11 | import tornado.web 12 | 13 | from trapperkeeper.callbacks import TrapperCallback 14 | from trapperkeeper.config import Config 15 | from trapperkeeper.models import get_db_engine, Session 16 | from trapperkeeper.utils import get_template_env, get_loglevel, CachingResolver 17 | from trapperkeeper import __version__ 18 | 19 | 20 | def stats_server(port): 21 | class Stats(tornado.web.RequestHandler): 22 | def get(self): 23 | return self.write(stats.to_dict()) 24 | 25 | application = tornado.web.Application([ 26 | (r"/debug/stats", Stats), 27 | ]) 28 | 29 | application.listen(port) 30 | tornado.ioloop.IOLoop.instance().start() 31 | 32 | 33 | 34 | def main(): 35 | 36 | parser = argparse.ArgumentParser(description="SNMP Trap Collector.") 37 | parser.add_argument("-c", "--config", default="/etc/trapperkeeper.yaml", 38 | help="Path to config file.") 39 | parser.add_argument("-v", "--verbose", action="count", default=0, help="Increase logging verbosity.") 40 | parser.add_argument("-q", "--quiet", action="count", default=0, help="Decrease logging verbosity.") 41 | parser.add_argument("-V", "--version", action="version", 42 | version="%%(prog)s %s" % __version__, 43 | help="Display version information.") 44 | 45 | args = parser.parse_args() 46 | 47 | oid_translate.load_mibs() 48 | 49 | config = Config.from_file(args.config) 50 | 51 | db_engine = get_db_engine(config["database"]) 52 | community = config["community"] 53 | if not community: 54 | community = None 55 | ipv6_server = config["ipv6"] 56 | if not ipv6_server: 57 | ipv6_server = None 58 | Session.configure(bind=db_engine) 59 | 60 | conn = Session() 61 | resolver = CachingResolver(timeout=300) 62 | template_env = get_template_env( 63 | hostname_or_ip=resolver.hostname_or_ip 64 | ) 65 | cb = TrapperCallback(conn, template_env, config, resolver, community) 66 | 67 | logging.basicConfig( 68 | level=get_loglevel(args), 69 | format="%(asctime)-15s\t%(levelname)s\t%(message)s" 70 | ) 71 | 72 | transport_dispatcher = AsynsockDispatcher() 73 | transport_dispatcher.registerRecvCbFun(cb) 74 | if ipv6_server: 75 | transport_dispatcher.registerTransport( 76 | udp6.domainName, udp6.Udp6SocketTransport().openServerMode(("::1", int(config["trap_port"]))) 77 | ) 78 | transport_dispatcher.registerTransport( 79 | udp.domainName, udp.UdpSocketTransport().openServerMode(("0.0.0.0", int(config["trap_port"]))) 80 | ) 81 | 82 | transport_dispatcher.jobStarted(1) 83 | stats_thread = threading.Thread(target=stats_server, args=(str(config["stats_port"]),)) 84 | 85 | try: 86 | stats_thread.start() 87 | transport_dispatcher.runDispatcher() 88 | except KeyboardInterrupt: 89 | pass 90 | finally: 91 | print "Stopping Transport Dispatcher..." 92 | transport_dispatcher.closeDispatcher() 93 | print "Stopping Stats Thread..." 94 | tornado.ioloop.IOLoop.instance().stop() 95 | stats_thread.join() 96 | print "Bye" 97 | 98 | 99 | if __name__ == "__main__": 100 | main() 101 | -------------------------------------------------------------------------------- /trapdoor/static/css/trapdoor.css: -------------------------------------------------------------------------------- 1 | body { 2 | /* Background image comes from http://subtlepatterns.com/ 3 | * and is CC BY-SA 3.0 - Subtle Patterns © Atle Mo. 4 | * 5 | * http://subtlepatterns.com/skulls/ 6 | */ 7 | background-image:url('/static/images/skulls/skulls.png'); 8 | 9 | /* Force scrollbar so collapsed divs don't jerk 10 | * the page around when they grow larger than 11 | * the page. 12 | */ 13 | overflow-y: scroll; 14 | margin-bottom: 20px; 15 | } 16 | 17 | .container { 18 | background: #f9f9f9; 19 | margin-top: 0px; 20 | box-shadow: 4px 4px 5px 1px rgba(0, 0, 0, 0.4); 21 | } 22 | 23 | .content { 24 | background: #f9f9f9; 25 | padding: 10px 25px 25px 25px; 26 | margin: -10px 0px 0px 0px; 27 | } 28 | 29 | .center { 30 | text-align: center; 31 | margin-left: auto; 32 | margin-right: auto; 33 | } 34 | 35 | a.link, .clickable-row { 36 | cursor: pointer; 37 | } 38 | 39 | table>tbody>tr>td.trap-extend { 40 | margin: 0px; 41 | padding: 0px; 42 | } 43 | 44 | .toggle-details { 45 | padding: 8px; 46 | } 47 | 48 | .table>tbody>tr.danger>td { 49 | background-color: #ffa1a1; 50 | color: #000; 51 | } 52 | 53 | .table>tbody>tr.warning>td { 54 | background-color: #ffea94; 55 | color: #000; 56 | } 57 | 58 | .table>tbody>tr.success>td { 59 | background-color: #9fe483; 60 | color: #000; 61 | } 62 | 63 | td.buttons-column { 64 | width: 68px; 65 | } 66 | 67 | /* Bootstrap Navbar */ 68 | .navbar-default { 69 | background-color: #e74c3c; 70 | border-color: #c0392b; 71 | border-radius: 0px; 72 | } 73 | .navbar-default .navbar-brand { 74 | color: #ecf0f1; 75 | } 76 | .navbar-default .navbar-brand:hover, .navbar-default .navbar-brand:focus { 77 | color: #ffbbbc; 78 | } 79 | .navbar-default .navbar-text { 80 | color: #ecf0f1; 81 | } 82 | .navbar-default .navbar-nav > li > a { 83 | color: #ecf0f1; 84 | } 85 | .navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus { 86 | color: #ffbbbc; 87 | } 88 | .navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus { 89 | color: #ffbbbc; 90 | background-color: #c0392b; 91 | } 92 | .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus { 93 | color: #ffbbbc; 94 | background-color: #c0392b; 95 | } 96 | .navbar-default .navbar-toggle { 97 | border-color: #c0392b; 98 | } 99 | .navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus { 100 | background-color: #c0392b; 101 | } 102 | .navbar-default .navbar-toggle .icon-bar { 103 | background-color: #ecf0f1; 104 | } 105 | .navbar-default .navbar-collapse, 106 | .navbar-default .navbar-form { 107 | border-color: #ecf0f1; 108 | } 109 | .navbar-default .navbar-link { 110 | color: #ecf0f1; 111 | } 112 | .navbar-default .navbar-link:hover { 113 | color: #ffbbbc; 114 | } 115 | 116 | @media (max-width: 767px) { 117 | .navbar-default .navbar-nav .open .dropdown-menu > li > a { 118 | color: #ecf0f1; 119 | } 120 | .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { 121 | color: #ffbbbc; 122 | } 123 | .navbar-default .navbar-nav .open .dropdown-menu > .active > a, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { 124 | color: #ffbbbc; 125 | background-color: #c0392b; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TrapperKeeper 2 | 3 | ## Description 4 | TrapperKeeper is a suite of tools for ingesting and displaying SNMP traps. This 5 | is designed as a replacement for snmptrapd and to supplement existing stateful 6 | monitoring solutions. 7 | 8 | Normally traps are stateless in nature which makes it difficult to monitor with 9 | a system like nagios which requires polling a source. TrapperKeeper will store 10 | traps in an active state for a configured amount of time before expiring. This 11 | makes it possible to poll the service for active traps and alert off of those 12 | traps. 13 | 14 | One example might be a humidity alert. If you cross over the humidity threshold 15 | and it clears immediately you might not want to be paged at 3am. But if it 16 | continues to send a trap every 5 minutes while it's over that threshold the 17 | combination of (host, oid, severity) will remain in an active state as 18 | long as that trap's expiration duration is longer than 5 minutes. This allows 19 | something like nagios to alarm when a single trap remains active for greater 20 | than some period of time. 21 | 22 | Another benefit is allowing aggregation of pages. Previously we'd just had an 23 | e-mail to a pager per trap but now we're only paged based on the alert interval 24 | regardless of how many traps we receive. This also allows us to schedule 25 | downtime for a device during scheduled maintenance to avoid trap storms. 26 | 27 | ## Requirements 28 | 29 | ### Ubuntu 30 | 31 | ```bash 32 | $ sudo apt-get install libmysqlclient-dev libsnmp-dev 33 | ``` 34 | 35 | ## Installation 36 | 37 | New versions will be updated to PyPI pretty regularly so it should be as easy 38 | as: 39 | 40 | ```bash 41 | $ pip install trapperkeeper 42 | ``` 43 | 44 | Once you've created a configuration file with your database information you 45 | can run the following to create the database schema. 46 | 47 | ```bash 48 | $ python -m trapperkeeper.cmds.sync_db -c /path/to/trapperkeeper.yaml 49 | ``` 50 | ## Tools 51 | 52 | ### trapperkeeper 53 | 54 | The trapperkeeper command receives SNMP traps and handles e-mailing and writing 55 | to the database. An example configuration file with documentation is available [here.](conf/trapperkeeper.yaml) 56 | 57 | ### trapdoor 58 | 59 | trapdoor is a webserver that provides a view into the existing traps as well as an 60 | API for viewing the state of traps. An example configuration file with documentation is available [here.](conf/trapdoor.yaml) 61 | 62 | ![Screenshot](https://raw.githubusercontent.com/dropbox/trapperkeeper/master/images/trapdoor.png) 63 | 64 | #### API 65 | 66 | ##### /api/activetraps 67 | _*Optional Parameters:*_ 68 | * host 69 | * oid 70 | * severity 71 | 72 | _*Returns:*_ 73 | ```javascript 74 | [ 75 | (, , ) 76 | ] 77 | ``` 78 | 79 | ##### /api/varbinds/ 80 | 81 | _*Returns:*_ 82 | ```javascript 83 | [ 84 | { 85 | "notification_id": , 86 | "name": , 87 | "pretty_value": , 88 | "oid": , 89 | "value": , 90 | "value_type": 91 | } 92 | ] 93 | ``` 94 | ## MIB Configuration 95 | 96 | `trapperkeeper` and `trapdoor` use the default mibs via netsnmp. You can see the default path for your system by running `net-snmp-config --default-mibdirs`. You can use the following environment variables usually documented in the `snmpcmd` man page 97 | 98 | > MIBS - The list of MIBs to load. Defaults to SNMPv2-TC:SNMPv2-MIB:IF-MIB:IP-MIB:TCP-MIB:UDP-MIB:SNMP-VACM-MIB. 99 | 100 | > MIBDIRS - The list of directories to search for MIBs. Defaults to /usr/share/snmp/mibs. 101 | 102 | For example I run both the `trapperkeeper` and `trapdoor` commands with the following environment to add a directory to the path and load all mibs. 103 | 104 | `MIBS=ALL MIBDIRS=+/usr/share/mibs/local/` 105 | 106 | ## TODO 107 | 108 | * Allow Custom E-mail templates for TrapperKeeper 109 | * cdnjs prefix for local cdnjs mirrors 110 | * User ACLs for resolution 111 | * Logging resolving user 112 | 113 | ## Known Issues 114 | 115 | * Doesn't currently support SNMPv3 116 | * Doesn't currently support inform 117 | * Certain devices have been known to send negative TimeTicks. pyasn1 fails to handle this. 118 | -------------------------------------------------------------------------------- /conf/trapperkeeper.yaml: -------------------------------------------------------------------------------- 1 | # Trap handlers are how we react to receiving specified traps. All 2 | # handlers inherit from the default settings which are configured 3 | # under the defaults header as well as the possibility of inheriting 4 | # from a template. 5 | # 6 | # Defaults: 7 | defaults: 8 | # Expiration is how long a trap stays in an active state. a null 9 | # value will indicate the trap needs to be manually resolved. 10 | # 11 | # Type: str (time spec) 12 | expiration: "15m" 13 | 14 | # Blackhole allows you to drop traps on the floor, never 15 | # making it to e-mail or the database. This is useful for 16 | # very chatty traps that you don't care about. 17 | # 18 | # Type: bool 19 | blackhole: false 20 | 21 | # Sets the default severity of all traps. Severity is useful 22 | # in use with external monitoring. For example you might want to 23 | # Page on critical, E-mail on warning, and ignore informational. 24 | # 25 | # Type str (Allowed informational, warning, critical) 26 | severity: "warning" 27 | 28 | # If you don't want mail sent by default you would comment 29 | # out this section. If mail is null no e-mail will be sent. 30 | mail: 31 | # The subject to use for mail that is sent. The following 32 | # format strings are available to use: 33 | # hostname: The hostname of the agent that sent the trap. 34 | # ipaddress: The IP address of the agent that sent the trap. 35 | # trap_name: The textual name for the received trap. 36 | # trap_oid: The OID for the received trap. 37 | # 38 | # Type: str 39 | subject: "%(hostname)s %(trap_name)s" 40 | 41 | # The recipients of the message, comma delimited, for a given 42 | # trap. If this value is falsey then no e-mail is sent. 43 | # 44 | # Type: str 45 | recipients: "root@localhost" 46 | 47 | # When running multiple instances of trapperkeeper as a manager for a 48 | # given device, the message will be deduplicated in the database. If 49 | # you want the e-mail to be sent even when the message has been 50 | # deduplicated then mark this true 51 | # 52 | # Type: bool 53 | mail_on_duplicate: false 54 | 55 | # Templates allow you to set up a class of traps to reduce the amount of 56 | # configuration required per handler. All options available above (in the 57 | # defaults section) are available under templates. 58 | # 59 | # Templates: 60 | templates: 61 | # This is the name for the template. You can use any name you like 62 | # though I'm overloading the severity names as they map pretty cleanly 63 | # to the class of traps I normally receive. 64 | critical: 65 | expiration: null 66 | severity: "critical" 67 | 68 | informational: 69 | expiration: "0s" 70 | severity: "informational" 71 | mail: null 72 | 73 | # Finally you have the actual configuration for various traps. In this 74 | # section you actually have configuration for individual traps. Again 75 | # all of the options above (defaults) are available with use additional 76 | # keyword, use, to inherit from templates. 77 | # 78 | # Trap Handlers: 79 | traphandlers: 80 | # Name or OID of the trap to match on. 81 | # 82 | # Type: str 83 | "SNMPv2-MIB::coldStart": 84 | # Template to inherit from. 85 | # 86 | # Type: str 87 | use: "critical" 88 | 89 | "TRAPPERKEEPER-MIB::testV2Trap": 90 | severity: warning 91 | 92 | "TRAPPERKEEPER-MIB::testV1Trap": 93 | severity: critical 94 | 95 | # Top level configuration for TrapperKeeper. 96 | config: 97 | # Takes a SqlAlchemy URL to the database to store traps. More details 98 | # can be found at the following URL: 99 | # http://docs.sqlalchemy.org/en/rel_0_9/core/engines.html#database-urls 100 | # 101 | # Type: str 102 | database: "sqlite:///trapperkeeper.sqlite" 103 | 104 | # The port to listen for traps on. 105 | # 106 | # Type: int 107 | trap_port: 162 108 | 109 | # The port to expose stats on. 110 | # 111 | # Type: int 112 | stats_port: 8889 113 | 114 | # Only accept traps with the following SNMP community. If community 115 | # is a falsey value, then all traps will be allowed through. 116 | # 117 | # Type: str 118 | community: null 119 | 120 | # Listen for traps on an IPv6 address 121 | # 122 | # Type: bool 123 | ipv6: false 124 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | TrapperKeeper 2 | ============= 3 | 4 | Description 5 | ----------- 6 | 7 | TrapperKeeper is a suite of tools for ingesting and displaying SNMP 8 | traps. This is designed as a replacement for snmptrapd and to supplement 9 | existing stateful monitoring solutions. 10 | 11 | Normally traps are stateless in nature which makes it difficult to 12 | monitor with a system like nagios which requires polling a source. 13 | TrapperKeeper will store traps in an active state for a configured 14 | amount of time before expiring. This makes it possible to poll the 15 | service for active traps and alert off of those traps. 16 | 17 | One example might be a humidity alert. If you cross over the humidity 18 | threshold and it clears immediately you might not want to be paged at 19 | 3am. But if it continues to send a trap every 5 minutes while it's over 20 | that threshold the combination of (host, oid, severity) will remain in 21 | an active state as long as that trap's expiration duration is longer 22 | than 5 minutes. This allows something like nagios to alarm when a single 23 | trap remains active for greater than some period of time. 24 | 25 | Another benefit is allowing aggregation of pages. Previously we'd just 26 | had an e-mail to a pager per trap but now we're only paged based on the 27 | alert interval regardless of how many traps we receive. This also allows 28 | us to schedule downtime for a device during scheduled maintenance to 29 | avoid trap storms. 30 | 31 | Requirements 32 | ------------ 33 | 34 | Ubuntu 35 | ~~~~~~ 36 | 37 | .. code:: bash 38 | 39 | $ sudo apt-get install libmysqlclient-dev libsnmp-dev 40 | 41 | Installation 42 | ------------ 43 | 44 | New versions will be updated to PyPI pretty regularly so it should be as 45 | easy as: 46 | 47 | .. code:: bash 48 | 49 | $ pip install trapperkeeper 50 | 51 | Once you've created a configuration file with your database information 52 | you can run the following to create the database schema. 53 | 54 | .. code:: bash 55 | 56 | $ python -m trapperkeeper.cmds.sync_db -c /path/to/trapperkeeper.yaml 57 | 58 | Tools 59 | ----- 60 | 61 | trapperkeeper 62 | ~~~~~~~~~~~~~ 63 | 64 | The trapperkeeper command receives SNMP traps and handles e-mailing and 65 | writing to the database. An example configuration file with 66 | documentation is available `here. `__ 67 | 68 | trapdoor 69 | ~~~~~~~~ 70 | 71 | trapdoor is a webserver that provides a view into the existing traps as 72 | well as an API for viewing the state of traps. An example configuration 73 | file with documentation is available `here. `__ 74 | 75 | .. figure:: https://raw.githubusercontent.com/dropbox/trapperkeeper/master/images/trapdoor.png 76 | :alt: Screenshot 77 | 78 | Screenshot 79 | API 80 | ^^^ 81 | 82 | /api/activetraps 83 | '''''''''''''''' 84 | 85 | **Optional Parameters:** \* host \* oid \* severity 86 | 87 | **Returns:** 88 | 89 | .. code:: javascript 90 | 91 | [ 92 | (, , ) 93 | ] 94 | 95 | /api/varbinds/ 96 | '''''''''''''' 97 | 98 | **Returns:** 99 | 100 | .. code:: javascript 101 | 102 | [ 103 | { 104 | "notification_id": , 105 | "name": , 106 | "pretty_value": , 107 | "oid": , 108 | "value": , 109 | "value_type": 110 | } 111 | ] 112 | 113 | MIB Configuration 114 | ----------------- 115 | 116 | ``trapperkeeper`` and ``trapdoor`` use the default mibs via netsnmp. You 117 | can see the default path for your system by running 118 | ``net-snmp-config --default-mibdirs``. You can use the following 119 | environment variables usually documented in the ``snmpcmd`` man page 120 | 121 | MIBS - The list of MIBs to load. Defaults to 122 | SNMPv2-TC:SNMPv2-MIB:IF-MIB:IP-MIB:TCP-MIB:UDP-MIB:SNMP-VACM-MIB. 123 | 124 | MIBDIRS - The list of directories to search for MIBs. Defaults to 125 | /usr/share/snmp/mibs. 126 | 127 | For example I run both the ``trapperkeeper`` and ``trapdoor`` commands 128 | with the following environment to add a directory to the path and load 129 | all mibs. 130 | 131 | ``MIBS=ALL MIBDIRS=+/usr/share/mibs/local/`` 132 | 133 | TODO 134 | ---- 135 | 136 | - Allow Custom E-mail templates for TrapperKeeper 137 | - cdnjs prefix for local cdnjs mirrors 138 | - User ACLs for resolution 139 | - Logging resolving user 140 | 141 | Known Issues 142 | ------------ 143 | 144 | - Doesn't currently support SNMPv3 145 | - Doesn't currently support inform 146 | - Certain devices have been known to send negative TimeTicks. pyasn1 147 | fails to handle this. 148 | 149 | -------------------------------------------------------------------------------- /trapperkeeper/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from jinja2 import Environment, PackageLoader 3 | import logging 4 | from oid_translate import ObjectId 5 | import pytz 6 | import re 7 | import struct 8 | import socket 9 | import smtplib 10 | import time 11 | 12 | from email.mime.multipart import MIMEMultipart 13 | from email.mime.text import MIMEText 14 | 15 | 16 | _TIME_STRING_RE = re.compile( 17 | r"(?:(?P\d+)d)?" 18 | r"(?:(?P\d+)h)?" 19 | r"(?:(?P\d+)m)?" 20 | r"(?:(?P\d+)s)?" 21 | ) 22 | 23 | DATEANDTIME_SLICES = ( 24 | (slice(1, None, -1), "h"), # year 25 | (2, "b"), # month 26 | (3, "b"), # day 27 | (4, "b"), # hour 28 | (5, "b"), # minutes 29 | (6, "b"), # seconds 30 | (7, "b"), # deci seconds 31 | (8, "c"), # direction from UTC 32 | (9, "b"), # hours from UTC 33 | (10, "b"), # minutes from UTC 34 | ) 35 | 36 | 37 | def parse_time_string(time_string): 38 | times = _TIME_STRING_RE.match(time_string).groupdict() 39 | for key, value in times.iteritems(): 40 | if value is None: 41 | times[key] = 0 42 | else: 43 | times[key] = int(value) 44 | 45 | return times 46 | 47 | 48 | def to_mibname(oid): 49 | return ObjectId(oid).name 50 | 51 | 52 | def varbind_pretty_value(varbind): 53 | output = varbind.value 54 | objid = ObjectId(varbind.oid) 55 | 56 | if varbind.value_type == "ipaddress": 57 | try: 58 | name = socket.gethostbyaddr(varbind.value)[0] 59 | output = "%s (%s)" % (name, output) 60 | except socket.error: 61 | pass 62 | elif varbind.value_type == "oid": 63 | output = to_mibname(varbind.value) 64 | elif varbind.value_type == "octet": 65 | if objid.textual == "DateAndTime": 66 | output = decode_date(varbind.value) 67 | 68 | if objid.enums and varbind.value.isdigit(): 69 | val = int(varbind.value) 70 | output = objid.enums.get(val, val) 71 | 72 | if objid.units: 73 | output = "%s %s" % (output, objid.units) 74 | 75 | return output 76 | 77 | 78 | def decode_date(hex_string): 79 | format_values = [ 80 | 0, 0, 0, 0, 81 | 0, 0, 0, "+", 82 | 0, 0 83 | ] 84 | 85 | if hex_string.startswith("0x"): 86 | hex_string = hex_string[2:].decode("hex") 87 | 88 | for idx, (_slice, s_type) in enumerate(DATEANDTIME_SLICES): 89 | try: 90 | value = hex_string[_slice] 91 | except IndexError: 92 | break 93 | format_values[idx] = struct.unpack(s_type, value)[0] 94 | 95 | return "%d-%d-%d,%d:%d:%d.%d,%s%d:%d" % tuple(format_values) 96 | 97 | 98 | def get_template_env(package="trapperkeeper", **kwargs): 99 | filters = { 100 | "to_mibname": to_mibname, 101 | "varbind_value": varbind_pretty_value, 102 | } 103 | filters.update(kwargs) 104 | env = Environment(loader=PackageLoader(package, "templates")) 105 | env.filters.update(filters) 106 | return env 107 | 108 | 109 | def send_trap_email(recipients, sender, subject, template_env, context): 110 | text_template = template_env.get_template("default_email_text.tmpl").render(**context) 111 | html_template = template_env.get_template("default_email_html.tmpl").render(**context) 112 | 113 | text = MIMEText(text_template, "plain") 114 | html = MIMEText(html_template, "html") 115 | 116 | if isinstance(recipients, basestring): 117 | recipients = recipients.split(",") 118 | 119 | msg = MIMEMultipart("alternative") 120 | msg["Subject"] = subject 121 | msg["From"] = sender 122 | msg["To"] = ", ".join(recipients) 123 | msg.attach(text) 124 | msg.attach(html) 125 | 126 | smtp = smtplib.SMTP("localhost") # TODO(gary): Allow config for this. 127 | smtp.sendmail(sender, recipients, msg.as_string()) 128 | smtp.quit() 129 | 130 | 131 | def get_loglevel(args): 132 | verbose = args.verbose * 10 133 | quiet = args.quiet * 10 134 | return logging.getLogger().level - verbose + quiet 135 | 136 | 137 | class CachingResolver(object): 138 | 139 | def __init__(self, timeout): 140 | self.timeout = timeout 141 | self._cache = {} 142 | 143 | def _hostname_or_ip(self, address): 144 | try: 145 | return socket.gethostbyaddr(address)[0] 146 | except socket.error: 147 | return address 148 | 149 | def hostname_or_ip(self, address): 150 | result = self._cache.get(address, None) 151 | now = time.time() 152 | if result is None or result[0] <= now: 153 | logging.debug("Cache miss for %s in hostname_or_ip", address) 154 | result = ( 155 | now + self.timeout, # Expiration 156 | self._hostname_or_ip(address), # Data 157 | ) 158 | self._cache[address] = result 159 | 160 | return result[1] 161 | 162 | 163 | def utcnow(): 164 | return datetime.datetime.utcnow().replace(tzinfo=pytz.UTC) 165 | -------------------------------------------------------------------------------- /trapdoor/handlers.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import json 3 | from sqlalchemy import desc, func, or_ 4 | 5 | from trapdoor.utils import TrapdoorHandler 6 | from trapperkeeper.models import Notification, VarBind 7 | 8 | def filter_query(query, host, oid, severity): 9 | if host is not None: 10 | query = query.filter(Notification.host == host) 11 | 12 | if oid is not None: 13 | query = query.filter(Notification.oid == oid) 14 | 15 | if severity is not None: 16 | query = query.filter(Notification.severity == severity) 17 | 18 | return query 19 | 20 | 21 | def _get_traps(db, offset=0, limit=50, host=None, oid=None, severity=None): 22 | now = datetime.utcnow() 23 | 24 | active_query = (db 25 | .query(Notification) 26 | .filter(or_( 27 | Notification.expires >= now, 28 | Notification.expires == None 29 | )) 30 | .order_by(desc(Notification.sent)) 31 | ) 32 | active_query = filter_query(active_query, host, oid, severity) 33 | 34 | total_active = active_query.count() 35 | traps = active_query.offset(offset).limit(limit).all() 36 | num_active = len(traps) 37 | 38 | if num_active: 39 | remaining_offset = 0 40 | else: 41 | remaining_offset = offset - total_active 42 | if remaining_offset < 0: 43 | remaining_offset = 0 44 | 45 | if num_active < limit: 46 | expired_query = (db 47 | .query(Notification) 48 | .filter(Notification.expires < now) 49 | .order_by(desc(Notification.sent)) 50 | ) 51 | expired_query = filter_query(expired_query, host, oid, severity) 52 | traps += expired_query.offset(remaining_offset).limit(limit - num_active).all() 53 | 54 | return traps, num_active 55 | 56 | 57 | class Index(TrapdoorHandler): 58 | def get(self): 59 | offset = int(self.get_argument("offset", 0)) 60 | limit = int(self.get_argument("limit", 50)) 61 | if limit > 100: 62 | limit = 100 63 | 64 | host = self.get_argument("host", None) 65 | if host is None: 66 | host = self.get_argument("hostname", None) 67 | oid = self.get_argument("oid", None) 68 | severity = self.get_argument("severity", None) 69 | 70 | now = datetime.utcnow() 71 | traps, num_active = _get_traps(self.db, offset, limit, host, oid, severity) 72 | 73 | return self.render( 74 | "index.html", traps=traps, now=now, num_active=num_active, 75 | host=host, oid=oid, severity=severity, offset=offset, limit=limit) 76 | 77 | class Resolve(TrapdoorHandler): 78 | def post(self): 79 | host = self.get_argument("host") 80 | oid = self.get_argument("oid") 81 | 82 | now = datetime.utcnow() 83 | 84 | traps = (self.db.query(Notification) 85 | .filter( 86 | Notification.host == host, 87 | Notification.oid == oid, 88 | or_( 89 | Notification.expires >= now, 90 | Notification.expires == None 91 | ) 92 | ) 93 | .all() 94 | ) 95 | 96 | for trap in traps: 97 | trap.expires = now 98 | self.db.commit() 99 | 100 | return self.redirect("/") 101 | 102 | class ResolveAll(TrapdoorHandler): 103 | def post(self): 104 | 105 | now = datetime.utcnow() 106 | traps = (self.db.query(Notification) 107 | .filter( 108 | or_( 109 | Notification.expires >= now, 110 | Notification.expires == None 111 | ) 112 | ) 113 | .all() 114 | ) 115 | 116 | for trap in traps: 117 | trap.expires = now 118 | self.db.commit() 119 | 120 | return self.redirect("/") 121 | 122 | class NotFound(TrapdoorHandler): 123 | def get(self): 124 | return self.notfound() 125 | 126 | 127 | class ApiVarBinds(TrapdoorHandler): 128 | def get(self, notification_id): 129 | varbinds = self.db.query(VarBind).filter(VarBind.notification_id == notification_id).all() 130 | varbinds = [varbind.to_dict(True) for varbind in varbinds] 131 | self.write(json.dumps(varbinds)) 132 | 133 | 134 | class ApiActiveTraps(TrapdoorHandler): 135 | def get(self): 136 | 137 | now = datetime.utcnow() 138 | host = self.get_argument("host", None) 139 | if host is None: 140 | host = self.get_argument("hostname", None) 141 | oid = self.get_argument("oid", None) 142 | severity = self.get_argument("severity", None) 143 | 144 | active_query = (self.db 145 | .query( 146 | Notification.host, 147 | Notification.oid, 148 | Notification.severity) 149 | .filter(or_( 150 | Notification.expires >= now, 151 | Notification.expires == None 152 | )) 153 | .group_by(Notification.host, Notification.oid) 154 | .order_by(desc(Notification.sent)) 155 | ) 156 | active_query = filter_query(active_query, host, oid, severity) 157 | 158 | traps = active_query.all() 159 | self.write(json.dumps(traps)) 160 | 161 | 162 | class ApiTraps(TrapdoorHandler): 163 | def get(self): 164 | offset = int(self.get_argument("offset", 0)) 165 | limit = int(self.get_argument("limit", 10)) 166 | if limit > 100: 167 | limit = 100 168 | 169 | host = self.get_argument("host", None) 170 | if host is None: 171 | host = self.get_argument("hostname", None) 172 | oid = self.get_argument("oid", None) 173 | severity = self.get_argument("severity", None) 174 | 175 | now = datetime.utcnow() 176 | traps, num_active = _get_traps(self.db, offset, limit, host, oid, severity) 177 | 178 | self.write(json.dumps([trap.to_dict() for trap in traps])) 179 | -------------------------------------------------------------------------------- /trapperkeeper/callbacks.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from expvar.stats import stats 3 | import logging 4 | from oid_translate import ObjectId 5 | 6 | from pyasn1.codec.ber import decoder 7 | from pyasn1.type.error import ValueConstraintError 8 | from pysnmp.proto import api 9 | from pysnmp.proto.error import ProtocolError 10 | 11 | import socket 12 | from sqlalchemy.exc import IntegrityError, InvalidRequestError, OperationalError 13 | 14 | from trapperkeeper.dde import DdeNotification 15 | from trapperkeeper.constants import SNMP_VERSIONS 16 | from trapperkeeper.models import Notification 17 | from trapperkeeper.utils import parse_time_string, send_trap_email 18 | 19 | 20 | try: 21 | from trapperkeeper_dde_plugin import run as dde_run 22 | except ImportError as err: 23 | # If no DDE plugin is found add noop to namespace. 24 | def dde_run(notification): 25 | pass 26 | 27 | 28 | class TrapperCallback(object): 29 | def __init__(self, conn, template_env, config, resolver, community): 30 | self.conn = conn 31 | self.template_env = template_env 32 | self.config = config 33 | self.hostname = socket.gethostname() 34 | self.resolver = resolver 35 | self.community = community 36 | 37 | def __call__(self, *args, **kwargs): 38 | try: 39 | self._call(*args, **kwargs) 40 | # Prevent the application from crashing when callback raises 41 | # an exception. 42 | except Exception as err: 43 | stats.incr("callback-failure", 1) 44 | logging.exception("Callback Failed: %s", err) 45 | 46 | def _send_mail(self, handler, trap, is_duplicate): 47 | if is_duplicate and not handler["mail_on_duplicate"]: 48 | return 49 | 50 | mail = handler["mail"] 51 | if not mail: 52 | return 53 | 54 | recipients = handler["mail"].get("recipients") 55 | if not recipients: 56 | return 57 | 58 | subject = handler["mail"]["subject"] % { 59 | "trap_oid": trap.oid, 60 | "trap_name": ObjectId(trap.oid).name, 61 | "ipaddress": trap.host, 62 | "hostname": self.resolver.hostname_or_ip(trap.host), 63 | } 64 | ctxt = dict(trap=trap, dest_host=self.hostname) 65 | try: 66 | stats.incr("mail_sent_attempted", 1) 67 | send_trap_email(recipients, "trapperkeeper", 68 | subject, self.template_env, ctxt) 69 | stats.incr("mail_sent_successful", 1) 70 | except socket.error as err: 71 | stats.incr("mail_sent_failed", 1) 72 | logging.warning("Failed to send e-mail for trap: %s", err) 73 | 74 | def _call(self, transport_dispatcher, transport_domain, transport_address, whole_msg): 75 | if not whole_msg: 76 | return 77 | 78 | msg_version = int(api.decodeMessageVersion(whole_msg)) 79 | 80 | if msg_version in api.protoModules: 81 | proto_module = api.protoModules[msg_version] 82 | else: 83 | stats.incr("unsupported-notification", 1) 84 | logging.error("Unsupported SNMP version %s", msg_version) 85 | return 86 | 87 | host = transport_address[0] 88 | version = SNMP_VERSIONS[msg_version] 89 | 90 | try: 91 | req_msg, whole_msg = decoder.decode(whole_msg, asn1Spec=proto_module.Message(),) 92 | except (ProtocolError, ValueConstraintError) as err: 93 | stats.incr("unsupported-notification", 1) 94 | logging.warning("Failed to receive trap (%s) from %s: %s", version, host, err) 95 | return 96 | req_pdu = proto_module.apiMessage.getPDU(req_msg) 97 | 98 | community = proto_module.apiMessage.getCommunity(req_msg) 99 | if self.community and community != self.community: 100 | stats.incr("unauthenticated-notification", 1) 101 | logging.warning("Received trap from %s with invalid community: %s... discarding", host, community) 102 | return 103 | 104 | if not req_pdu.isSameTypeWith(proto_module.TrapPDU()): 105 | stats.incr("unsupported-notification", 1) 106 | logging.warning("Received non-trap notification from %s", host) 107 | return 108 | 109 | if msg_version not in (api.protoVersion1, api.protoVersion2c): 110 | stats.incr("unsupported-notification", 1) 111 | logging.warning("Received trap not in v1 or v2c") 112 | return 113 | 114 | trap = Notification.from_pdu(host, proto_module, version, req_pdu) 115 | if trap is None: 116 | stats.incr("unsupported-notification", 1) 117 | logging.warning("Invalid trap from %s: %s", host, req_pdu) 118 | return 119 | 120 | dde = DdeNotification(trap, self.config.handlers[trap.oid]) 121 | dde_run(dde) 122 | handler = dde.handler 123 | 124 | trap.severity = handler["severity"] 125 | trap.manager = self.hostname 126 | 127 | if handler.get("expiration", None): 128 | expires = parse_time_string(handler["expiration"]) 129 | expires = timedelta(**expires) 130 | trap.expires = trap.sent + expires 131 | 132 | stats.incr("traps_received", 1) 133 | objid = ObjectId(trap.oid) 134 | if handler.get("blackhole", False): 135 | stats.incr("traps_blackholed", 1) 136 | logging.debug("Blackholed %s from %s", objid.name, host) 137 | return 138 | 139 | logging.info("Trap Received (%s) from %s", objid.name, host) 140 | stats.incr("traps_accepted", 1) 141 | 142 | 143 | duplicate = False 144 | try: 145 | stats.incr("db_write_attempted", 1) 146 | self.conn.add(trap) 147 | self.conn.commit() 148 | stats.incr("db_write_successful", 1) 149 | except OperationalError as err: 150 | self.conn.rollback() 151 | logging.warning("Failed to commit: %s", err) 152 | stats.incr("db_write_failed", 1) 153 | # TODO(gary) reread config and reconnect to database 154 | except InvalidRequestError as err: 155 | # If we get into this state we should rollback any pending changes. 156 | stats.incr("db_write_failed", 1) 157 | self.conn.rollback() 158 | logging.warning("Bad state, rolling back transaction: %s", err) 159 | except IntegrityError as err: 160 | stats.incr("db_write_duplicate", 1) 161 | duplicate = True 162 | self.conn.rollback() 163 | logging.info("Duplicate Trap (%s) from %s. Likely inserted by another manager.", objid.name, host) 164 | logging.debug(err) 165 | 166 | self._send_mail(handler, trap, duplicate) 167 | -------------------------------------------------------------------------------- /trapperkeeper/models.py: -------------------------------------------------------------------------------- 1 | from oid_translate import ObjectId 2 | import time 3 | import pytz 4 | 5 | from sqlalchemy import create_engine 6 | from sqlalchemy import ( 7 | Column, Integer, String, LargeBinary, 8 | ForeignKey, Enum, DateTime, BigInteger, 9 | Index 10 | ) 11 | from sqlalchemy.ext.declarative import declarative_base 12 | from sqlalchemy.orm import relationship, backref, sessionmaker 13 | 14 | from trapperkeeper.constants import NAME_TO_PY_MAP, SNMP_TRAP_OID, ASN_TO_NAME_MAP, SEVERITIES 15 | from trapperkeeper.utils import utcnow, varbind_pretty_value 16 | 17 | 18 | Session = sessionmaker() 19 | Model = declarative_base() 20 | 21 | def get_db_engine(url): 22 | return create_engine(url, pool_recycle=300) 23 | 24 | 25 | class Notification(Model): 26 | 27 | __tablename__ = "notifications" 28 | __table_args__ = ( 29 | Index("duplicate", "host", "request_id", "oid", "trunc_sent", unique=True, mysql_length={ 30 | "oid": 255, 31 | }), 32 | ) 33 | 34 | id = Column(Integer, primary_key=True) 35 | 36 | sent = Column(DateTime, default=utcnow, index=True) 37 | # This column is used to give a safer guarantee againt duplicate traps 38 | # coming into the system when running multiple trapperkeeper instances. 39 | # There is still a race condition where multiple traps could be on both 40 | # sides of the truncated time stamp. 41 | trunc_sent = Column(String(length=12)) 42 | expires = Column(DateTime, default=None, nullable=True, index=True) 43 | host = Column(String(length=255), index=True) 44 | manager = Column(String(length=255), index=True) 45 | trap_type = Column(Enum("trap", "trap2", "inform")) 46 | version = Column(Enum("v1", "v2c", "v3")) 47 | request_id = Column(BigInteger) 48 | oid = Column(String(length=1024), index=True) 49 | severity = Column(Enum(*SEVERITIES), default="warning", index=True) 50 | 51 | @property 52 | def sent_utc(self): 53 | return self.sent.replace(tzinfo=pytz.UTC) 54 | 55 | @property 56 | def expires_utc(self): 57 | return None if self.expires is None else self.expires.replace(tzinfo=pytz.UTC) 58 | 59 | def to_dict(self): 60 | return { 61 | "id": self.id, 62 | "host": self.host, 63 | "oid": self.oid, 64 | "severity": self.severity, 65 | "sent": time.mktime(self.sent.timetuple()), 66 | "expires": time.mktime(self.expires.timetuple()) if self.expires is not None else None, 67 | } 68 | 69 | def pprint(self): 70 | print "Host:", self.host 71 | print "Trap Type:", self.trap_type 72 | print "Request ID:", self.request_id 73 | print "Version:", self.version 74 | print "Trap OID:", self.oid 75 | print "VarBinds:" 76 | for varbind in self.varbinds: 77 | varbind.pprint() 78 | 79 | @staticmethod 80 | def _from_pdu_v1(host, proto_module, version, pdu): 81 | trapoid = str(proto_module.apiTrapPDU.getEnterprise(pdu)) 82 | 83 | generic = int(proto_module.apiTrapPDU.getGenericTrap(pdu)) 84 | specific = int(proto_module.apiTrapPDU.getSpecificTrap(pdu)) 85 | 86 | if generic == 6: # Enterprise Specific Traps 87 | trapoid = "%s.0.%s" % (trapoid, specific) 88 | else: 89 | trapoid = "%s.%s" % (trapoid, generic + 1) 90 | 91 | trap_type = "trap" 92 | # v1 doesn't have request_id. Use timestamp in it's place. 93 | request_id = int(proto_module.apiTrapPDU.getTimeStamp(pdu)) 94 | 95 | now = utcnow() 96 | trunc_now = now.replace(minute=now.minute / 10 * 10).strftime("%Y%m%d%H%M") 97 | trap = Notification( 98 | host=host, sent=now, trunc_sent=trunc_now, trap_type=trap_type, 99 | request_id=request_id, version=version, oid=trapoid) 100 | 101 | for oid, val in proto_module.apiTrapPDU.getVarBinds(pdu): 102 | oid = oid.prettyPrint() 103 | pval = val.prettyPrint() 104 | val_type = ASN_TO_NAME_MAP.get(val.__class__, "octet") 105 | trap.varbinds.append(VarBind(oid=oid, value_type=val_type, value=pval)) 106 | 107 | return trap 108 | 109 | @staticmethod 110 | def _from_pdu_v2c(host, proto_module, version, pdu): 111 | varbinds = [] 112 | trapoid = None 113 | trap_type = "trap2" 114 | request_id = proto_module.apiTrapPDU.getRequestID(pdu).prettyPrint() 115 | 116 | # Need to do initial loop to pull out trapoid 117 | for oid, val in proto_module.apiPDU.getVarBindList(pdu): 118 | oid = oid.prettyPrint() 119 | val = val.getComponentByName("value").getComponent().getComponent() 120 | pval = val.prettyPrint() 121 | 122 | if oid == SNMP_TRAP_OID: 123 | trapoid = pval 124 | continue 125 | 126 | varbinds.append((oid, ASN_TO_NAME_MAP.get(val.__class__, "octet"), pval)) 127 | 128 | if not trapoid: 129 | return 130 | 131 | now = utcnow() 132 | trunc_now = now.replace(minute=now.minute / 10 * 10).strftime("%Y%m%d%H%M") 133 | trap = Notification( 134 | host=host, sent=now, trunc_sent=trunc_now, trap_type=trap_type, 135 | request_id=request_id, version=version, oid=trapoid) 136 | for oid, val_type, val in varbinds: 137 | trap.varbinds.append(VarBind(oid=oid, value_type=val_type, value=val)) 138 | 139 | return trap 140 | 141 | @staticmethod 142 | def from_pdu(host, proto_module, version, pdu): 143 | if version == "v1": 144 | return Notification._from_pdu_v1(host, proto_module, version, pdu) 145 | elif version == "v2c": 146 | return Notification._from_pdu_v2c(host, proto_module, version, pdu) 147 | else: 148 | return None 149 | 150 | 151 | class VarBind(Model): 152 | 153 | __tablename__ = "varbinds" 154 | 155 | id = Column(Integer, primary_key=True) 156 | 157 | notification_id = Column(Integer, ForeignKey("notifications.id"), index=True) 158 | notification = relationship(Notification, backref=backref("varbinds")) 159 | oid = Column(String(length=1024)) 160 | value_type = Column(Enum(*NAME_TO_PY_MAP.keys())) 161 | value = Column(LargeBinary) 162 | 163 | def to_dict(self, pretty=False): 164 | out = { 165 | "notification_id": self.notification_id, 166 | "oid": self.oid, 167 | "value_type": self.value_type, 168 | "value": self.value, 169 | } 170 | 171 | if pretty: 172 | out["name"] = ObjectId(self.oid).name 173 | out["pretty_value"] = varbind_pretty_value(self) 174 | 175 | return out 176 | 177 | def pprint(self): 178 | print "\t", self.oid, "(%s)" % self.value_type, "=", self.value 179 | 180 | def __repr__(self): 181 | return "Varbind(oid=%s, value_type=%s, value=%s)" % ( 182 | repr(self.oid), repr(self.value_type), repr(self.value) 183 | ) 184 | -------------------------------------------------------------------------------- /trapdoor/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 | 7 |
8 | {% if host %} 9 |

10 | 11 | 12 | Hostname {{ host|hostname_or_ip }} 13 |

14 | {% endif %} 15 | {% if oid %} 16 |

17 | 18 | 19 | Trap {{ oid|to_mibname }} 20 |

21 | {% endif %} 22 | {% if severity %} 23 |

24 | 25 | 26 | Severity {{ severity }} 27 |

28 | {% endif %} 29 |
30 | 31 |
32 | {{ xsrf_form_html() }} 33 | 41 |
42 |
43 | 46 | 51 |
52 |
53 | 56 | 62 |
63 |
64 | {% if offset == 0 %} 65 | 66 | {% else %} 67 | 68 | {% endif %} 69 | 70 | {% if offset != 0 %} 71 | 72 | {% else %} 73 | 74 | {% endif %} 75 | 76 | Page {{ ((offset+limit)/limit)|int}} 77 | 78 | {% if traps|length < limit %} 79 | 80 | {% else %} 81 | 82 | {% endif %} 83 |
84 | 85 |
86 | 87 | 88 |
 
89 | 90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | {% if not traps %} 100 | 103 | {% endif %} 104 | {% for trap in traps %} 105 | {% set expired = trap.expires != None and now >= trap.expires %} 106 | {% if trap.severity == "critical"%} 107 | {% set severity_label = "danger" %} 108 | {% elif trap.severity == "informational"%} 109 | {% set severity_label = "success" %} 110 | {% else %} 111 | {% set severity_label = "warning" %} 112 | {% endif %} 113 | 114 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 146 | 147 | {% endfor %} 148 |
 HostnameTrapReceivedExpiration
101 |
No Active Traps.
102 |
115 | 121 | {% if not expired %} 122 |
123 |
124 | 125 | 126 | {{ xsrf_form_html() }} 127 | 130 |
131 |
132 | {% endif %} 133 |
{{trap.host|hostname_or_ip}}{{trap.oid|to_mibname}}{{trap.sent_utc|print_date}}{{trap.expires_utc|print_date}}
141 |
142 |
143 |
145 |
149 |
150 | 151 |
152 |
153 | {% if offset == 0 %} 154 | 155 | {% else %} 156 | 157 | {% endif %} 158 | 159 | {% if offset != 0 %} 160 | 161 | {% else %} 162 | 163 | {% endif %} 164 | 165 | Page {{ ((offset+limit)/limit)|int}} 166 | 167 | {% if traps|length < limit %} 168 | 169 | {% else %} 170 | 171 | {% endif %} 172 |
173 |
174 | 175 | 176 | {% endblock %} 177 | 178 | {% block script %} 179 | 180 | 204 | 205 | {% endblock %} 206 | --------------------------------------------------------------------------------