133 lines
5.1 KiB
Plaintext
133 lines
5.1 KiB
Plaintext
|
#!/usr/bin/env python
|
||
|
|
||
|
# Daemon that listens to updates about the source IP prefixes from which snmp access
|
||
|
# is allowed. In case of change, it will update the snmp configuration file accordingly.
|
||
|
# Also, after a change, it will notify snmpd to re-read its config file (service reload).
|
||
|
|
||
|
import os
|
||
|
import re
|
||
|
import sys
|
||
|
import time
|
||
|
import redis
|
||
|
|
||
|
service="snmpd"
|
||
|
config_file_path="/etc/snmp"
|
||
|
redis_key="SNMP_ALLOW_LIST" # the redis list we listen to
|
||
|
subscription='__keyspace@0__:%s' % redis_key
|
||
|
temporization_duration = 3 # how long we wait for changes to settle (ride out a bursts of changes in redis_key)
|
||
|
fake_infinite = 9999 # How often we wake up when nothing is going on --get_message()'s timeout has no 'infinite' value
|
||
|
# after these operations we may need to revisit existing ssh connections because they removed or modified existing entries
|
||
|
delete_operations = ["lrem", "lpop", "rpop", "blpop", "brpop", "brpoplpush", "rpoplpush", "ltrim", "del", "lset"]
|
||
|
|
||
|
r = redis.StrictRedis(host='localhost')
|
||
|
p = r.pubsub()
|
||
|
|
||
|
# If redis is not up yet, this can fail, so wait for redis to be available
|
||
|
while True:
|
||
|
try:
|
||
|
p.subscribe(subscription)
|
||
|
break
|
||
|
except redis.exceptions.ConnectionError:
|
||
|
time.sleep(3)
|
||
|
|
||
|
# We could loose contact with redis at a later stage, in which case we will exit with
|
||
|
# return code -2 and supervisor will restart us, at which point we are back in the
|
||
|
# while loop above waiting for redis to be ready.
|
||
|
try:
|
||
|
|
||
|
# By default redis does enable events, so enable them
|
||
|
r.config_set("notify-keyspace-events", "KAE")
|
||
|
|
||
|
|
||
|
# To update the configuration file
|
||
|
#
|
||
|
# Example config file for reference:
|
||
|
# root@sonic:/# cat /etc/snmp/snmpd.conf
|
||
|
# <...some snmp config, like udp port to use etc...>
|
||
|
# rocommunity public 172.20.61.0/24
|
||
|
# rocommunity public 172.20.60.0/24
|
||
|
# rocommunity public 127.00.00.0/8
|
||
|
# <...some more snmp config...>
|
||
|
# root@sonic:/#
|
||
|
#
|
||
|
# snmpd.conf supports include file, like so:
|
||
|
# includeFile /etc/snmp/community.conf
|
||
|
# includeDir /etc/snmp/config.d
|
||
|
# which could make file massaging simpler, but even then we still deal with lines
|
||
|
# that have shared "masters", since some other entity controls the community strings
|
||
|
# part of that line.
|
||
|
# If other database attributes need to be written to the snmp config file, then
|
||
|
# it should be done by this daemon as well (sure, we could inotify on the file
|
||
|
# and correct it back, but that's glitchy).
|
||
|
|
||
|
def write_configuration_file(v):
|
||
|
filename="%s/%s.conf" % (config_file_path, service)
|
||
|
filename_tmp = filename + ".tmp"
|
||
|
f=open(filename, "r")
|
||
|
snmpd_config = f.read()
|
||
|
f.close()
|
||
|
f=open(filename_tmp, "w")
|
||
|
this_community = "not_a_community"
|
||
|
for l in snmpd_config.split('\n'):
|
||
|
m = re.match("^(..)community (\S+)", l)
|
||
|
if not m:
|
||
|
f.write(l)
|
||
|
f.write("\n")
|
||
|
else:
|
||
|
if not l.startswith(this_community): # already handled community (each community is duplicated per allow entry)
|
||
|
this_community="%scommunity %s" % (m.group(1), m.group(2))
|
||
|
if len(v):
|
||
|
for value in v:
|
||
|
f.write("%s %s\n" % (this_community, value))
|
||
|
else:
|
||
|
f.write("%s\n" % this_community)
|
||
|
f.close()
|
||
|
os.rename(filename_tmp, filename)
|
||
|
os.system("kill -HUP $(pgrep snmpd) > /dev/null 2> /dev/null || :")
|
||
|
|
||
|
# write initial configuration
|
||
|
write_configuration_file(r.lrange(redis_key, 0, -1))
|
||
|
|
||
|
# listen for changes and rewrite configuration file if needed, after some temporization
|
||
|
#
|
||
|
# How those subscribed to messages look like, for reference:
|
||
|
# {'pattern': None, 'type': 'subscribe', 'channel': '__keyspace@0__:SNMP_PERMIT_LIST', 'data': 1L}
|
||
|
# {'pattern': None, 'type': 'message', 'channel': '__keyspace@0__:SNMP_PERMIT_LIST', 'data': 'rpush'}
|
||
|
# {'pattern': None, 'type': 'message', 'channel': '__keyspace@0__:SNMP_PERMIT_LIST', 'data': 'lpush'}
|
||
|
# {'pattern': None, 'type': 'message', 'channel': '__keyspace@0__:SNMP_PERMIT_LIST', 'data': 'lrem'}
|
||
|
# {'pattern': None, 'type': 'message', 'channel': '__keyspace@0__:SNMP_PERMIT_LIST', 'data': 'lset'}
|
||
|
# {'pattern': None, 'type': 'message', 'channel': '__keyspace@0__:SNMP_PERMIT_LIST', 'data': 'del'}
|
||
|
|
||
|
select_timeout = fake_infinite
|
||
|
config_changed = False
|
||
|
while True:
|
||
|
try:
|
||
|
m = p.get_message(timeout=select_timeout)
|
||
|
except Exception:
|
||
|
sys.exit(-2)
|
||
|
# temporization: no change after 'timeout' seconds -> commit any accumulated changes
|
||
|
if not m and config_changed:
|
||
|
write_configuration_file(r.lrange(redis_key, 0, -1))
|
||
|
config_changed = False
|
||
|
select_timeout = fake_infinite
|
||
|
if m and m['type'] == "message":
|
||
|
if m['channel'] != subscription:
|
||
|
print "WTF: unexpected case"
|
||
|
continue
|
||
|
config_changed = True
|
||
|
select_timeout = temporization_duration
|
||
|
# some debugs for now
|
||
|
print "-------------------- config change: ",
|
||
|
if m["data"] in delete_operations:
|
||
|
print "DELETE"
|
||
|
else:
|
||
|
print ""
|
||
|
v = r.lrange(redis_key, 0, -1)
|
||
|
for value in v:
|
||
|
print value
|
||
|
|
||
|
except redis.exceptions.ConnectionError as e:
|
||
|
sys.exit(-2)
|
||
|
|
||
|
|