Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Snmp checks #983

Merged
merged 35 commits into from
Jun 18, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
29ed5c2
SNMP| First commit for SNMP Agent checks
Apr 29, 2014
3a251aa
SNMP| Add interface monitoring
Apr 29, 2014
f9a0041
SNMP| Fallback for interface identification in case of not indexed
Apr 29, 2014
fce8ab0
SNMP| Add support for Custom tags
Apr 29, 2014
a18e457
SNMP| Allow use of snmp v3 authentication
Apr 29, 2014
b613276
SNMP| Allows to add more metrics
Apr 30, 2014
620853b
SNMP| Fix:Don't use device as a tag
Apr 30, 2014
eeac4c6
SNMP| Takes all the metrics definition to conf.d
Apr 30, 2014
0224970
SNMP| Raise Exception on failed calls remove debug logging
Apr 30, 2014
b534528
SNMP| Allow for partial configuration
Apr 30, 2014
8e6f03c
SNMP| First pass at testing
Apr 30, 2014
c44409a
Merge branch 'master' into SNMP_checks
Apr 30, 2014
998269b
SNMP| Fix test
May 1, 2014
35c0015
SNMP| Add a test performing the checks
May 1, 2014
ccfa0f9
Cln| Remove spurious print
May 1, 2014
ab05bc4
SNMP| Better tests for the check
May 1, 2014
869c63c
SNMP| Metrics defined at the instance level
May 5, 2014
0827c32
Merge branch 'master' into SNMP_checks
May 5, 2014
80ddd73
SNMP| Added docstring
May 5, 2014
51df079
Merge branch 'master' into SNMP_checks
May 19, 2014
c77d416
SNMP| Use the agent's aggregator's rate for SNMP counters
May 19, 2014
85774eb
SNMP Checks| Use single way to determine interface and improve logging
May 22, 2014
544883a
SNMP Checks| Allows to specify metric by oid + name
May 28, 2014
5ad472b
SNMP Checks| Allow to use custom mibs if they are in the pysnmp format
May 28, 2014
52efcb6
SNMP Checks | Test for custom mib folder
May 28, 2014
d8fda49
SNMP Checks| Handle no such object cases
May 30, 2014
d513744
SNMP Checks| Support type extension and add debug logging
May 30, 2014
43e3da0
Merge branch 'master' into SNMP_checks
May 30, 2014
b1f5437
SNMP Checks| Extended type fix
May 30, 2014
4259c5e
SNMP checks| Remove bare except
Jun 13, 2014
e9675d6
SNMP Checks | Better interface discovery
Jun 13, 2014
b240ee8
SNMP Checks| Reorganized imports
Jun 13, 2014
1c09b09
SNMP Checks| Add documentation
Jun 13, 2014
19be52b
SNMP Checks| Camel case and spacing
Jun 17, 2014
a6978ea
Merge branch 'omnibus_bundling' into SNMP_checks
Jun 17, 2014
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ before_install:
- sudo apt-get install couchdb
- sudo apt-get install lighttpd
- sudo apt-get install gearman
- sudo apt-get install snmpd
install:
- pip install -r requirements.txt --use-mirrors
- pip install . --use-mirrors
Expand All @@ -51,11 +52,14 @@ before_script:
- sudo mkdir -p /etc/dd-agent/
- sudo install -d -o "$(id -u)" /var/log/datadog
- sudo bash -c "curl -L https://raw.github.com/DataDog/dd-agent/master/datadog.conf.example > /etc/dd-agent/datadog.conf"
- sudo service snmpd stop
- sudo bash -c "curl -L https://raw.github.com/DataDog/dd-agent/SNMP_checks/tests/snmp/snmpd.conf > /etc/snmp/snmpd.conf" # referring wrong branch during development
- sudo service apache2 start
- sudo service nginx start
- sudo /etc/init.d/lighttpd start
- sudo service tomcat6 restart
- sudo service couchdb start
- sudo service snmpd start
env:
- DB=redis
script:
Expand Down
257 changes: 257 additions & 0 deletions checks.d/snmp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
# std
from collections import defaultdict

# project
from checks import AgentCheck

# 3rd party
from pysnmp.entity.rfc3413.oneliner import cmdgen
from pysnmp.smi.exval import noSuchInstance, noSuchObject
from pysnmp.smi import builder
import pysnmp.proto.rfc1902 as snmp_type

# Additional types that are not part of the SNMP protocol. cf RFC 2856
(CounterBasedGauge64, ZeroBasedCounter64) = builder.MibBuilder().importSymbols("HCNUM-TC","CounterBasedGauge64", "ZeroBasedCounter64")

# Metric type that we support
SNMP_COUNTERS = [snmp_type.Counter32.__name__, snmp_type.Counter64.__name__, ZeroBasedCounter64.__name__]
SNMP_GAUGES = [snmp_type.Gauge32.__name__, CounterBasedGauge64.__name__]

# IF-MIB magic values
IF_TABLE_OID = '.1.3.6.1.2.1.2.2.1.'
IF_TABLE_TYPE_POS = 9
IF_TABLE_INDEX_POS = 10
IF_DESCR = 2
IF_TYPE = 3
LOCALHOST_INTERFACE = 24

def reply_invalid(oid):
return noSuchInstance.isSameTypeWith(oid) or \
noSuchObject.isSameTypeWith(oid)

class SnmpCheck(AgentCheck):

cmd_generator = None

def __init__(self, name, init_config, agentConfig, instances=None):
AgentCheck.__init__(self, name, init_config, agentConfig, instances)

self.interface_list = {}

# Load Custom MIB directory
mibs_path = None
if init_config is not None:
mibs_path = init_config.get("mibs_folder")
SnmpCheck.create_command_generator(mibs_path)

# Detect the interfaces of the instance and retain their indexes
if instances is not None:
for instance in instances:
if 'ip_address' in instance:
ip_address = instance["ip_address"]
self.interface_list[ip_address] = self.get_interfaces(instance)

@classmethod
def create_command_generator(cls, mibs_path=None):
'''
Create a command generator to perform all the snmp query
If mibs_path is not None, load the mibs present in the custom mibs
folder (Need to be in pysnmp format)
'''
cls.cmd_generator = cmdgen.CommandGenerator()
if mibs_path is not None:
mib_builder = cls.cmd_generator.snmpEngine.msgAndPduDsp.\
mibInstrumController.mibBuilder
mib_sources = mib_builder.getMibSources() + (
builder.DirMibSource(mibs_path),
)
mib_builder.setMibSources(*mib_sources)

def get_interfaces(self, instance):
'''
Query the IF-MIB table to get the list of interfaces and their description
Ignore the interfaces that are loopback (localhost)
The nextCmd will return all the value in a format like this:

[(1.3.6.1.2.1.2.2.1.{what type of information}.{index of the interface}, information)]

For now we only care about the index, the description(eg. interface name eth0) and the type
to ignore the loopback interface
See http://www.alvestrand.no/objectid/1.3.6.1.2.1.2.2.1.html for more info about the table
'''
interface_list = {}
transport_target = self.get_transport_target(instance)
auth_data = self.get_auth_data(instance)

snmp_command = self.cmd_generator.nextCmd
error_indication, error_status, error_index, var_binds = snmp_command(
auth_data,
transport_target,
IF_TABLE_OID,
lookupValues = True
)

if_table = defaultdict(dict)
if error_indication:
raise Exception("{0} for instance {1}".format(error_indication, instance["ip_address"]))
else:
if error_status:
raise Exception("{0} for instance {1}".format(error_status.prettyPrint(), instance["ip_address"]))
else:
for table_row in var_binds:
for name, val in table_row:
if_table[name.asTuple()[IF_TABLE_INDEX_POS]][name.asTuple()[IF_TABLE_TYPE_POS]] = val

self.log.debug("Interface Table discovered %s" % if_table)
for index in if_table:
type = if_table[index].get(IF_TYPE)
descr = if_table[index].get(IF_DESCR)
if not reply_invalid(type):
if int(type) != LOCALHOST_INTERFACE and not reply_invalid(descr):
interface_list[index] = str(descr)
self.log.info("Discovered interface %s" % str(descr))

return interface_list

@classmethod
def get_auth_data(cls, instance):
'''
Generate a Security Parameters object based on the configuration of the instance
See http://pysnmp.sourceforge.net/docs/current/security-configuration.html
'''
if "community_string" in instance:
# SNMP v1 - SNMP v2
return cmdgen.CommunityData(instance['community_string'])
elif "user" in instance:
# SNMP v3
user = instance["user"]
auth_key = None
priv_key = None
auth_protocol = None
priv_protocol = None
if "authKey" in instance:
auth_key = instance["authKey"]
auth_protocol = cmdgen.usmHMACMD5AuthProtocol
if "privKey" in instance:
priv_key = instance["privKey"]
auth_protocol = cmdgen.usmHMACMD5AuthProtocol
priv_protocol = cmdgen.usmDESPrivProtocol
if "authProtocol" in instance:
auth_protocol = getattr(cmdgen, instance["authProtocol"])
if "privProtocol" in instance:
priv_protocol = getattr(cmdgen, instance["privProtocol"])
return cmdgen.UsmUserData(user, auth_key, priv_key, auth_protocol, priv_protocol)
else:
raise Exception("An authentication method needs to be provided")

@classmethod
def get_transport_target(cls, instance):
'''
Generate a Transport target object based on the configuration of the instance
'''
if "ip_address" not in instance:
raise Exception("An IP address needs to be specified")
ip_address = instance["ip_address"]
port = instance.get("port", 161) # Default SNMP port
return cmdgen.UdpTransportTarget((ip_address, port))

def check(self, instance):
tags = instance.get("tags",[])
ip_address = instance["ip_address"]
device_oids = []
oid_names ={}

# Check the metrics completely defined
for metric in instance.get('metrics', []):
if 'MIB' in metric:
device_oids.append(((metric["MIB"], metric["symbol"]), metric["index"]))
elif 'OID' in metric:
device_oids.append(metric['OID'])
# Associate the name to the OID so that we can perform the matching
oid_names[metric['OID']] = metric['name']
else:
raise Exception('Unsupported metrics format in config file')
self.log.debug("Querying device %s for %s oids", ip_address, len(device_oids))
results = SnmpCheck.snmp_get(instance, device_oids)
for oid, value in results:
self.submit_metric(instance, oid, value, tags=tags + ["snmp_device:" + ip_address],
oid_names = oid_names)

# Check the metrics defined per interface by appending the index
# of the table to create a fully defined metric
interface_oids = []
for metric in instance.get('interface_metrics', []):
interface_oids.append((metric["MIB"], metric["symbol"]))
for interface, descr in self.interface_list[instance['ip_address']].items():
oids = [(oid, interface) for oid in interface_oids]
self.log.debug("Querying device %s for %s oids", ip_address, len(oids))
interface_results = SnmpCheck.snmp_get(instance, oids)
for oid, value in interface_results:
self.submit_metric(instance, oid, value, tags = tags + ["snmp_device:" + ip_address,
"interface:"+descr])

@classmethod
def snmp_get(cls, instance, oids):
"""
Perform a snmp get command to the device that instance
describe and return a list of tuble (name, values)
corresponding to the elements in oids.
"""
transport_target = cls.get_transport_target(instance)
auth_data = cls.get_auth_data(instance)


snmp_command = cls.cmd_generator.getCmd
error_indication, error_status, error_index, var_binds = snmp_command(
auth_data,
transport_target,
*oids,
lookupNames=True,
lookupValues=True
)

if error_indication:
raise Exception("{0} for instance {1}".format(error_indication, instance["ip_address"]))
else:
if error_status:
raise Exception("{0} for instance {1}".format(error_status.prettyPrint(), instance["ip_address"]))
else:
return var_binds

def submit_metric(self, instance, oid, snmp_value, tags=[], oid_names={}):
'''
Convert the values reported as pysnmp-Managed Objects to values and
report them to the aggregator
'''
if reply_invalid(snmp_value):
# Metrics not present in the queried object
self.log.warning("No such Mib available: %s" %oid.getMibSymbol()[1])
return

# Get the name for the OID either from the name that we can decode
# or from the name that was specified for it
if str(oid.getOid()) in oid_names:
name = "snmp."+ oid_names[str(oid.getOid())]
else:
try:
name = "snmp." + oid.getMibSymbol()[1]
except Exception:
self.log.warning("Couldn't find a name for oid {0}".format(oid))
return

# Ugly hack but couldn't find a cleaner way
# Proper way would be to use the ASN1 method isSameTypeWith but this
# returns True in the case of CounterBasedGauge64 and Counter64 for example
snmp_class = snmp_value.__class__.__name__
for counter_class in SNMP_COUNTERS:
if snmp_class==counter_class:
value = int(snmp_value)
self.rate(name, value, tags)
return
for gauge_class in SNMP_GAUGES:
if snmp_class==gauge_class:
value = int(snmp_value)
self.gauge(name, value, tags)
return
self.log.warning("Unsupported metric type %s", snmp_class)

59 changes: 59 additions & 0 deletions conf.d/snmp.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
init_config:
# #You can specify an additional folder for your custom mib files (python format)
# mibs_folder: /path/to/your/mibs/folder

instances:

# SNMP v1-v2 configuration

# - ip_address: localhost
# port: 161
# community_string: public
# tags:
# - optional_tag_1
# - optional_tag_2
#
# # Device metrics to monitor
# metrics:
# # You can specify metrics using MIBS
# - MIB: UDP-MIB
# symbol: udpInDatagrams
# index: 0
# - MIB: TCP-MIB
# symbol: tcpActiveOpens
# index: 0
# - MIB: TCP-MIB
# symbol: tcpPassiveOpens
# index: 0
# # You can also specify an oid and the name under which it should appear
# # in Datadog
# - OID: 1.3.6.1.2.1.6.9.0
# name: tcpConnections
#
# # Metrics to monitor on each interface
# interface_metrics:
# - MIB: IF-MIB
# symbol: ifInOctets
# - MIB: IF-MIB
# symbol: ifOutOctets

# SNMP v3 configuration
# check http://pysnmp.sourceforge.net/docs/current/security-configuration.html

# - ip_address: localhost
# port: 161 # default value
# user: user
# authKey: password
# privKey: private_key
# authProtocol: authProtocol
# privProtocol: privProtocol
# tags:
# - optional_tag_1
# - optional_tag_2
#
# # Metrics to monitor on each interface
# interface_metrics:
# - MIB: IF-MIB
# symbol: ifInOctets
# - MIB: IF-MIB
# symbol: ifOutOctets
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ psutil
gearman
pylint
boto
pysnmp
pysnmp-mibs
PyMySQL
pg8000
ntplib
Expand Down
1 change: 1 addition & 0 deletions tests/snmp/snmpd.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rocommunity public
Loading