Skip to content

Commit b4b4a7c

Browse files
author
Anze
committed
Fetch interface data from devices and save the interfaces as entities to Grafolean
1 parent 80b3cca commit b4b4a7c

File tree

1 file changed

+121
-24
lines changed

1 file changed

+121
-24
lines changed

snmpbot.py

+121-24
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ class InvalidOutputPath(Exception):
3737
r = redis.Redis(host=REDIS_HOST)
3838

3939

40+
OID_IF_DESCR = '1.3.6.1.2.1.2.2.1.2'
41+
OID_IF_SPEED = '1.3.6.1.2.1.2.2.1.5'
42+
43+
4044
def _get_previous_counter_value(counter_ident):
4145
prev_value = r.hgetall(counter_ident)
4246
if not prev_value: # empty dict
@@ -195,6 +199,33 @@ def send_results_to_grafolean(backend_url, bot_token, account_id, values):
195199

196200
class SNMPBot(Collector):
197201

202+
@staticmethod
203+
def _create_snmp_sesssion(job_info):
204+
# initialize SNMP session:
205+
session_kwargs = {
206+
"hostname": job_info["details"]["ipv4"],
207+
"use_numeric": True,
208+
}
209+
cred = job_info["credential_details"]
210+
snmp_version = int(cred["version"][5:6])
211+
session_kwargs["version"] = snmp_version
212+
if snmp_version in [1, 2]:
213+
session_kwargs["community"] = cred["snmpv12_community"]
214+
elif snmp_version == 3:
215+
session_kwargs = {
216+
**session_kwargs,
217+
"security_username": cred["snmpv3_securityName"],
218+
"security_level": cred["snmpv3_securityLevel"], # easysnmp supports camelCase level names too
219+
"privacy_protocol": cred.get("snmpv3_privProtocol", 'DEFAULT'),
220+
"privacy_password": cred.get("snmpv3_privKey", ''),
221+
"auth_protocol": cred.get("snmpv3_authProtocol", 'DEFAULT'),
222+
"auth_password": cred.get("snmpv3_authKey", ''),
223+
}
224+
else:
225+
raise Exception("Invalid SNMP version")
226+
session = Session(**session_kwargs)
227+
return session
228+
198229
@staticmethod
199230
def do_snmp(*args, **job_info):
200231
"""
@@ -249,29 +280,7 @@ def do_snmp(*args, **job_info):
249280
ipv4=job_info["details"]["ipv4"],
250281
))
251282

252-
# initialize SNMP session:
253-
session_kwargs = {
254-
"hostname": job_info["details"]["ipv4"],
255-
"use_numeric": True,
256-
}
257-
cred = job_info["credential_details"]
258-
snmp_version = int(cred["version"][5:6])
259-
session_kwargs["version"] = snmp_version
260-
if snmp_version in [1, 2]:
261-
session_kwargs["community"] = cred["snmpv12_community"]
262-
elif snmp_version == 3:
263-
session_kwargs = {
264-
**session_kwargs,
265-
"security_username": cred["snmpv3_securityName"],
266-
"security_level": cred["snmpv3_securityLevel"], # easysnmp supports camelCase level names too
267-
"privacy_protocol": cred.get("snmpv3_privProtocol", 'DEFAULT'),
268-
"privacy_password": cred.get("snmpv3_privKey", ''),
269-
"auth_protocol": cred.get("snmpv3_authProtocol", 'DEFAULT'),
270-
"auth_password": cred.get("snmpv3_authKey", ''),
271-
}
272-
else:
273-
raise Exception("Invalid SNMP version")
274-
session = Session(**session_kwargs)
283+
session = SNMPBot._create_snmp_sesssion(job_info)
275284

276285
# filter out only those sensors that are supposed to run at this interval:
277286
affecting_intervals, = args
@@ -308,6 +317,88 @@ def do_snmp(*args, **job_info):
308317
send_results_to_grafolean(job_info['backend_url'], job_info['bot_token'], job_info['account_id'], values)
309318

310319

320+
@staticmethod
321+
def update_if_entities(*args, **job_info):
322+
log.info("Running interfaces job for account [{account_id}], IP [{ipv4}]".format(
323+
account_id=job_info["account_id"],
324+
ipv4=job_info["details"]["ipv4"],
325+
))
326+
327+
session = SNMPBot._create_snmp_sesssion(job_info)
328+
329+
parent_entity_id = job_info["entity_id"]
330+
account_id = job_info["account_id"]
331+
backend_url = job_info['backend_url']
332+
bot_token = job_info['bot_token']
333+
# fetch interfaces and update the interface entities:
334+
result_descr = session.walk(OID_IF_DESCR)
335+
result_speed = session.walk(OID_IF_SPEED)
336+
337+
# make sure that indexes of results are aligned - we don't want to have incorrect data:
338+
if any([if_speed.oid_index != if_descr.oid_index for if_descr, if_speed in zip(result_descr, result_speed)]):
339+
log.warning(f"Out-of-order results for interface names on entity {parent_entity_id}, sorting not yet implemented, bailing out!")
340+
return
341+
342+
# - get those entities on this account, which have this entity as their parent and filter them by type ('interface')
343+
requests_session = requests.Session()
344+
url = f'{backend_url}/accounts/{account_id}/entities/?parent={parent_entity_id}&entity_type=interface&b={bot_token}'
345+
r = requests_session.get(url)
346+
r.raise_for_status()
347+
# existing_entities = {x['details']['snmp_index']: (x['name'], x['details']['speed_bps'], x['id'],) for x in r.json()['list']}
348+
# Temporary, until we implement filtering in API:
349+
existing_entities = {x['details']['snmp_index']: (x['name'], x['details']['speed_bps'], x['id'],) for x in r.json()['list'] if x["entity_type"] == 'interface' and x["parent"] == parent_entity_id}
350+
351+
for if_descr_snmpvalue, if_speed_snmpvalue in zip(result_descr, result_speed):
352+
oid_index = if_descr_snmpvalue.oid_index
353+
descr = if_descr_snmpvalue.value
354+
speed_bps = if_speed_snmpvalue.value
355+
# - for each new entity:
356+
# - make sure it exists (if not, create it - POST)
357+
if oid_index not in existing_entities:
358+
log.debug(f"Entity with OID index {oid_index} is new, inserting.")
359+
url = f'{backend_url}/accounts/{account_id}/entities/?b={bot_token}'
360+
payload = {
361+
"name": descr,
362+
"entity_type": "interface",
363+
"parent": parent_entity_id,
364+
"details":{
365+
"snmp_index": oid_index,
366+
"speed_bps": speed_bps,
367+
},
368+
}
369+
r = requests_session.post(url, json=payload)
370+
continue
371+
372+
# - make sure the description and speed are correct (if not, update them - PUT)
373+
existing_descr, existing_speed, existing_id = existing_entities[oid_index]
374+
if existing_descr != descr or existing_speed != speed_bps:
375+
log.debug(f"Entity with OID index {oid_index} changed data, updating.")
376+
url = f'{backend_url}/accounts/{account_id}/entities/{existing_id}/?b={bot_token}'
377+
payload = {
378+
"name": descr,
379+
"entity_type": "interface",
380+
# "parent": parent_entity_id, # changing entity parent is not possible
381+
"details":{
382+
"snmp_index": oid_index,
383+
"speed_bps": speed_bps,
384+
},
385+
}
386+
r = requests_session.put(url, json=payload)
387+
del existing_entities[oid_index]
388+
continue
389+
390+
# - mark it as processed
391+
log.debug(f"Entity with OID index {oid_index} didn't change.")
392+
del existing_entities[oid_index]
393+
394+
# - for every existing entity that is not among the new ones, remove it (no point in keeping it - we don't keep old versions of enities data either)
395+
for oid_index in existing_entities:
396+
_, _, existing_id = existing_entities[oid_index]
397+
log.debug(f"Entity with OID index {oid_index} no longer exists, removing.")
398+
url = f'{backend_url}/accounts/{account_id}/entities/{existing_id}/?b={bot_token}'
399+
r = requests_session.delete(url)
400+
401+
311402
def jobs(self):
312403
"""
313404
Each entity (device) is a single job, no matter how many sensors it has. The reason is
@@ -316,9 +407,15 @@ def jobs(self):
316407
for entity_info in self.fetch_job_configs('snmp'):
317408
intervals = list(set([sensor_info["interval"] for sensor_info in entity_info["sensors"]]))
318409
job_info = { **entity_info, "backend_url": self.backend_url, "bot_token": self.bot_token }
319-
job_id = str(entity_info["entity_id"])
410+
job_id = f'{entity_info["entity_id"]}'
320411
yield job_id, intervals, SNMPBot.do_snmp, job_info
321412

413+
# We also collect interface data from each entity; the assumption is that everyone who wants
414+
# to use SNMP also wants to know about network interfaces.
415+
# Since `job_info` has all the necessary data, we simply pass it along:
416+
job_id = f'{entity_info["entity_id"]}-interfaces'
417+
yield job_id, [5*60], SNMPBot.update_if_entities, job_info
418+
322419

323420
def wait_for_grafolean(backend_url):
324421
while True:

0 commit comments

Comments
 (0)