class BirdAgent: def __init__(self, cfgfile, birdcli, netstatcmd="netstat -na"): self.cfgfile = cfgfile self.birdcli = birdcli self.netstatcmd = netstatcmd bgp_states = { "idle": 1, "connect": 2, "active": 3, "opensent": 4, "openconfirm": 5, "established": 6, } _re_config_include = re.compile("^include\s*\"(/[^\"]*)\".*$") _re_config_bgp_proto_begin = re.compile( "^protocol bgp ([a-zA-Z0-9_]+) .* \{$") _re_config_local_as = re.compile( "local ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) as ([0-9]+);") _re_config_bgp_holdtime = re.compile("hold time ([0-9]+);") _re_config_bgp_keepalive = re.compile("keepalive time ([0-9]+);") _re_config_remote_peer = re.compile( "neighbor ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) as ([0-9]+);") _re_config_timeformat = re.compile("\s*timeformat\s+protocol\s*\"%s\"\s*;") _re_config_proto_end = re.compile("^\}$") _re_birdcli_bgp_begin = re.compile( "([a-zA-Z0-9_]+) *BGP * [a-zA-Z0-9_]+ * [a-zA-Z0-9]+ * ([a-zA-Z0-9:]+) *" ) _re_birdcli_bgp_peer = { "bgpPeerIdentifier": re.compile("Neighbor ID:.* ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)"), "bgpPeerState": re.compile("BGP state:.* ([a-zA-Z]+)"), "bgpPeerLocalAddr": re.compile("Source address:.* ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)"), "bgpPeerRemoteAddr": re.compile("Neighbor address:.* ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)"), "bgpPeerRemoteAs": re.compile("Neighbor AS:.* ([0-9]+)"), "bgpPeerInUpdates": re.compile( "Import updates:\ +([0-9]+) .*[0-9\-]+.*[0-9\-]+.*[0-9\-]+.*[0-9\-]+" ), "bgpPeerOutUpdates": re.compile( "Export updates:\ +([0-9]+) .*[0-9\-]+.*[0-9\-]+.*[0-9\-]+.*[0-9\-]+" ), "bgpPeerHoldTime": re.compile("Hold timer:.* ([0-9]+)/[0-9]+"), "bgpPeerHoldTimeConfigured": re.compile("Hold timer:.* [0-9]+/([0-9]+)"), "bgpPeerKeepAlive": re.compile("Keepalive timer:.* ([0-9]+)/[0-9]+"), "bgpPeerKeepAliveConfigured": re.compile("Keepalive timer:.* [0-9]+/([0-9]+)"), "bgpPeerLastError": re.compile("Last error:\ +[a-zA-Z0-9-_\ ]+$") } _re_birdcli_bgp_end = re.compile("^$") _re_birdcli_ospf_neighbor = re.compile( "^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\s+([0-9]+)\s+(\S+)\s+(\S+)\s+(\S+)\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)" ) _re_netstat = re.compile( "^tcp\s+[0-9]+\s+[0-9]+\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):([0-9]+)\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):([0-9]+)\s+ESTABLISHED" ) bgp_keys = [ 'bgpPeerIdentifier', 'bgpPeerState', 'bgpPeerAdminStatus', 'bgpPeerNegotiatedVersion', 'bgpPeerLocalAddr', 'bgpPeerLocalPort', 'bgpPeerRemoteAddr', 'bgpPeerRemotePort', 'bgpPeerRemoteAs', 'bgpPeerInUpdates', 'bgpPeerOutUpdates', 'bgpPeerInTotalMessages', 'bgpPeerOutTotalMessages', 'bgpPeerLastError', 'bgpPeerFsmEstablishedTransitions', 'bgpPeerFsmEstablishedTime', 'bgpPeerConnectRetryInterval', 'bgpPeerHoldTime', 'bgpPeerKeepAlive', 'bgpPeerHoldTimeConfigured', 'bgpPeerKeepAliveConfigured', 'bgpPeerMinASOriginationInterval', 'bgpPeerMinRouteAdvertisementInterval', 'bgpPeerInUpdateElapsedTime', ] bgp_defaults = { 'bgpPeerIdentifier': SnmpIpAddress("0.0.0.0"), 'bgpPeerLocalAddr': SnmpIpAddress("0.0.0.0"), 'bgpPeerHoldTime': 0, 'bgpPeerHoldTimeConfigured': 0, 'bgpPeerKeepAlive': 0, 'bgpPeerKeepAliveConfigured': 0, 'bgpPeerState': 1, 'bgpPeerInUpdates': SnmpCounter32(0), 'bgpPeerOutUpdates': SnmpCounter32(0), 'bgpPeerAdminStatus': 2, 'bgpPeerConnectRetryInterval': 0, 'bgpPeerFsmEstablishedTime': SnmpGauge32(0), 'bgpPeerFsmEstablishedTransitions': SnmpCounter32(0), 'bgpPeerInTotalMessages': SnmpCounter32(0), 'bgpPeerInUpdateElapsedTime': SnmpGauge32(0), 'bgpPeerLastError': '0', 'bgpPeerMinASOriginationInterval': 15, 'bgpPeerMinRouteAdvertisementInterval': 30, 'bgpPeerNegotiatedVersion': 0, 'bgpPeerOutTotalMessages': SnmpCounter32(0), } @staticmethod def ipCompare(ip1, ip2): lst1 = "%3s.%3s.%3s.%3s" % tuple(ip1.split(".")) lst2 = "%3s.%3s.%3s.%3s" % tuple(ip2.split(".")) return cmp(lst1, lst2) @staticmethod def combinedConfigLines(filename): """ yield the whole bird configuration file line by line; all include-statements are resolved/unrolled """ with open(filename, "r") as bird_conf: for line in bird_conf: line = line.strip() match = BirdAgent._re_config_include.search(line) if not match: yield line else: for subconf in glob.glob(match.group(1)): yield "# subconf: %s (from %s)" % (subconf, line) for subline in BirdAgent.combinedConfigLines(subconf): yield subline @staticmethod def bgpKeys(): return BirdAgent.bgp_keys def getOSPFState(self, ospf_instance): """ fetch OSPF-related state from: * parsing `birdc show ospf neighbors $ospf` output """ # "with"-context-manager for Popen not available in python < 3.2 birdc = subprocess.Popen([self.birdcli, "show", "ospf", "neighbors", ospf_instance], \ stdout=subprocess.PIPE) output = birdc.communicate()[0] if birdc.returncode != 0: print("ERROR: bird-CLI (querying ospf neighbors) %s failed: %i" % (self.birdcli, birdc.returncode)) neighbors = {} for line in output.split("\n"): match = self._re_birdcli_ospf_neighbor.search(line) if match: rtrid, pri, state, deadtime, iface, rtrip = match.groups() neighbors[rtrid] = {} neighbors[rtrid]["pri"] = int(pri) neighbors[rtrid]["state"] = state neighbors[rtrid]["deadtime"] = deadtime neighbors[rtrid]["iface"] = iface neighbors[rtrid]["rtrip"] = rtrip return {"ospf-neighbors": neighbors} def getBGPState(self): """ fetch BGP-related state from: * parsing configuration file * parsing `birdc show protocols all` output * parsing `netstat` output """ current_time = int(time.time()) # fetch some data from the configuration: cfg = {} cfg["bgp-peers"] = {} proto = None for line in BirdAgent.combinedConfigLines(self.cfgfile): if self._re_config_timeformat: cfg["timeformat"] = True match = self._re_config_bgp_proto_begin.search(line) if match: proto = match.group(1) cfg["bgp-peers"][proto] = {} if proto: match = self._re_config_local_as.search(line) if match: cfg["bgp-peers"][proto][ "bgpPeerLocalAddr"] = SnmpIpAddress(match.group(1)) cfg["bgp-peers"][proto]["bgpPeerLocalAs"] = int( match.group(2)) if not cfg.has_key("bgpLocalAs"): cfg["bgpLocalAs"] = int(match.group(2)) elif cfg["bgpLocalAs"] != int(match.group(2)): print("WARNING: multiple local AS: %i/%i"% \ (cfg["bgpLocalAs"],int(match.group(2)))) match = self._re_config_remote_peer.search(line) if match: cfg["bgp-peers"][proto][ "bgpPeerRemoteAddr"] = SnmpIpAddress(match.group(1)) cfg["bgp-peers"][proto]["bgpPeerRemoteAs"] = int( match.group(2)) match = self._re_config_bgp_holdtime.search(line) if match: cfg["bgp-peers"][proto]["bgpPeerHoldTimeConfigured"] = int( match.group(1)) match = self._re_config_bgp_keepalive.search(line) if match: cfg["bgp-peers"][proto][ "bgpPeerKeepAliveConfigured"] = int(match.group(1)) if self._re_config_proto_end.search(line): proto = None if not cfg.has_key("timeformat"): print("WARNING: timeformat not configured for this agent's use.") state = cfg.copy() bgp_proto = None ospf_proto = None # "with"-context-manager for Popen not available in python < 3.2 birdc = subprocess.Popen([self.birdcli, "show", "protocols", "all"], \ stdout=subprocess.PIPE) output = birdc.communicate()[0] if birdc.returncode != 0: print("ERROR: bird-CLI %s failed: %i" % (self.birdcli, birdc.returncode)) for line in output.split("\n"): match = self._re_birdcli_bgp_begin.search(line) if match: bgp_proto = match.group(1) state["bgp-peers"][bgp_proto] = {} timestamp = int(match.group(2)) if not state["bgp-peers"].has_key(bgp_proto): print("WARNING: proto \"%s\" not in config, skipping" % bgp_proto) continue state["bgp-peers"][bgp_proto][ "bgpPeerFsmEstablishedTime"] = SnmpGauge32(current_time - timestamp) if bgp_proto: for peerprop_name, peerprop_re in self._re_birdcli_bgp_peer.items( ): match = peerprop_re.search(line) if match: if peerprop_name == 'bgpPeerState': state["bgp-peers"][bgp_proto][peerprop_name] = \ self.bgp_states[match.group(1).lower()] elif peerprop_name in [ 'bgpPeerIdentifier', 'bgpPeerLocalAddr', 'bgpPeerRemoteAddr' ]: state["bgp-peers"][bgp_proto][ peerprop_name] = SnmpIpAddress(match.group(1)) elif peerprop_name in [ 'bgpPeerInUpdates', 'bgpPeerOutUpdates' ]: state["bgp-peers"][bgp_proto][ peerprop_name] = SnmpCounter32(match.group(1)) else: state["bgp-peers"][bgp_proto][peerprop_name] = int( match.group(1)) if self._re_birdcli_bgp_end.search(line): bgp_proto = None # use netstat to query for tcp:179 connections bgp_sessions = {} netstat = subprocess.Popen( \ "%s | grep '^tcp.*:179.*ESTABLISHED'"%self.netstatcmd, shell=True, stdout=subprocess.PIPE) for line in netstat.communicate()[0].split("\n"): match = self._re_netstat.search(line) if not match: continue # key 4-tuples by remote ip: src-addr, src-port, dst-addr, dst-port bgp_sessions[match.group(3)] = match.groups() # now match the tcp:179 4-tuples with bgp-state, # and enrich state by local+remote ports for proto in state["bgp-peers"].keys(): state["bgp-peers"][proto]["bgpPeerLocalPort"] = 0 state["bgp-peers"][proto]["bgpPeerRemotePort"] = 0 if not bgp_sessions.has_key( state["bgp-peers"][proto]["bgpPeerRemoteAddr"]): # print("INFO: proto %s has no bgp session."%proto) continue srcip, srcport, dstip, dstport = bgp_sessions[ state["bgp-peers"][proto]["bgpPeerRemoteAddr"]] if srcip != state["bgp-peers"][proto]["bgpPeerLocalAddr"] or \ dstip != state["bgp-peers"][proto]["bgpPeerRemoteAddr"]: print("WARNING: proto %s has invalid BGP session (%s / %s)" % (proto, srcip, dstip)) continue state["bgp-peers"][proto]["bgpPeerLocalPort"] = int(srcport) state["bgp-peers"][proto]["bgpPeerRemotePort"] = int(dstport) return state
def getBGPState(self): """ fetch BGP-related state from: * parsing configuration file * parsing `birdc show protocols all` output * parsing `netstat` output """ current_time = int(time.time()) # fetch some data from the configuration: cfg = {} cfg["bgp-peers"] = {} proto = None for line in BirdAgent.combinedConfigLines(self.cfgfile): if self._re_config_timeformat: cfg["timeformat"] = True match = self._re_config_bgp_proto_begin.search(line) if match: proto = match.group(1) cfg["bgp-peers"][proto] = {} if proto: match = self._re_config_local_as.search(line) if match: cfg["bgp-peers"][proto][ "bgpPeerLocalAddr"] = SnmpIpAddress(match.group(1)) cfg["bgp-peers"][proto]["bgpPeerLocalAs"] = int( match.group(2)) if not cfg.has_key("bgpLocalAs"): cfg["bgpLocalAs"] = int(match.group(2)) elif cfg["bgpLocalAs"] != int(match.group(2)): print("WARNING: multiple local AS: %i/%i"% \ (cfg["bgpLocalAs"],int(match.group(2)))) match = self._re_config_remote_peer.search(line) if match: cfg["bgp-peers"][proto][ "bgpPeerRemoteAddr"] = SnmpIpAddress(match.group(1)) cfg["bgp-peers"][proto]["bgpPeerRemoteAs"] = int( match.group(2)) match = self._re_config_bgp_holdtime.search(line) if match: cfg["bgp-peers"][proto]["bgpPeerHoldTimeConfigured"] = int( match.group(1)) match = self._re_config_bgp_keepalive.search(line) if match: cfg["bgp-peers"][proto][ "bgpPeerKeepAliveConfigured"] = int(match.group(1)) if self._re_config_proto_end.search(line): proto = None if not cfg.has_key("timeformat"): print("WARNING: timeformat not configured for this agent's use.") state = cfg.copy() bgp_proto = None ospf_proto = None # "with"-context-manager for Popen not available in python < 3.2 birdc = subprocess.Popen([self.birdcli, "show", "protocols", "all"], \ stdout=subprocess.PIPE) output = birdc.communicate()[0] if birdc.returncode != 0: print("ERROR: bird-CLI %s failed: %i" % (self.birdcli, birdc.returncode)) for line in output.split("\n"): match = self._re_birdcli_bgp_begin.search(line) if match: bgp_proto = match.group(1) state["bgp-peers"][bgp_proto] = {} timestamp = int(match.group(2)) if not state["bgp-peers"].has_key(bgp_proto): print("WARNING: proto \"%s\" not in config, skipping" % bgp_proto) continue state["bgp-peers"][bgp_proto][ "bgpPeerFsmEstablishedTime"] = SnmpGauge32(current_time - timestamp) if bgp_proto: for peerprop_name, peerprop_re in self._re_birdcli_bgp_peer.items( ): match = peerprop_re.search(line) if match: if peerprop_name == 'bgpPeerState': state["bgp-peers"][bgp_proto][peerprop_name] = \ self.bgp_states[match.group(1).lower()] elif peerprop_name in [ 'bgpPeerIdentifier', 'bgpPeerLocalAddr', 'bgpPeerRemoteAddr' ]: state["bgp-peers"][bgp_proto][ peerprop_name] = SnmpIpAddress(match.group(1)) elif peerprop_name in [ 'bgpPeerInUpdates', 'bgpPeerOutUpdates' ]: state["bgp-peers"][bgp_proto][ peerprop_name] = SnmpCounter32(match.group(1)) else: state["bgp-peers"][bgp_proto][peerprop_name] = int( match.group(1)) if self._re_birdcli_bgp_end.search(line): bgp_proto = None # use netstat to query for tcp:179 connections bgp_sessions = {} netstat = subprocess.Popen( \ "%s | grep '^tcp.*:179.*ESTABLISHED'"%self.netstatcmd, shell=True, stdout=subprocess.PIPE) for line in netstat.communicate()[0].split("\n"): match = self._re_netstat.search(line) if not match: continue # key 4-tuples by remote ip: src-addr, src-port, dst-addr, dst-port bgp_sessions[match.group(3)] = match.groups() # now match the tcp:179 4-tuples with bgp-state, # and enrich state by local+remote ports for proto in state["bgp-peers"].keys(): state["bgp-peers"][proto]["bgpPeerLocalPort"] = 0 state["bgp-peers"][proto]["bgpPeerRemotePort"] = 0 if not bgp_sessions.has_key( state["bgp-peers"][proto]["bgpPeerRemoteAddr"]): # print("INFO: proto %s has no bgp session."%proto) continue srcip, srcport, dstip, dstport = bgp_sessions[ state["bgp-peers"][proto]["bgpPeerRemoteAddr"]] if srcip != state["bgp-peers"][proto]["bgpPeerLocalAddr"] or \ dstip != state["bgp-peers"][proto]["bgpPeerRemoteAddr"]: print("WARNING: proto %s has invalid BGP session (%s / %s)" % (proto, srcip, dstip)) continue state["bgp-peers"][proto]["bgpPeerLocalPort"] = int(srcport) state["bgp-peers"][proto]["bgpPeerRemotePort"] = int(dstport) return state
def getBGPState(self): """ fetch BGP-related state from: * parsing configuration file * parsing `birdc show protocols all` output * parsing `ss` output """ timezone = get_localzone() current_time = datetime.now(pytz.utc) # fetch some data from the configuration: cfg = {} cfg["bgp-peers"] = {} proto = None for line in BirdAgent.combinedConfigLines(self.cfgfile): if self._re_config_timeformat: cfg["timeformat"] = True match = self._re_config_bgp_proto_begin.search(line) if match: proto = match.group(1) cfg["bgp-peers"][proto] = {} if proto: match = self._re_config_local_as.search(line) if match: cfg["bgp-peers"][proto][ "bgpPeerLocalAddr"] = SnmpIpAddress(match.group(1)) cfg["bgp-peers"][proto]["bgpPeerLocalAs"] = int( match.group(2)) if "bgpLocalAs" not in cfg: cfg["bgpLocalAs"] = int(match.group(2)) elif cfg["bgpLocalAs"] != int(match.group(2)): print("WARNING: multiple local AS: %i/%i" % (cfg["bgpLocalAs"], int(match.group(2)))) match = self._re_config_remote_peer.search(line) if match: cfg["bgp-peers"][proto][ "bgpPeerRemoteAddr"] = SnmpIpAddress(match.group(1)) cfg["bgp-peers"][proto]["bgpPeerRemoteAs"] = int( match.group(2)) match = self._re_config_bgp_holdtime.search(line) if match: cfg["bgp-peers"][proto]["bgpPeerHoldTimeConfigured"] = int( match.group(1)) match = self._re_config_bgp_keepalive.search(line) if match: cfg["bgp-peers"][proto][ "bgpPeerKeepAliveConfigured"] = int(match.group(1)) if self._re_config_proto_end.search(line): proto = None if "timeformat" not in cfg: print( "ERROR: timeformat not configured for this agent's use, terminating..." ) sys.exit(1) # Validate protocol's config for proto in cfg["bgp-peers"]: if "bgpPeerLocalAddr" not in cfg["bgp-peers"][proto] and \ "bgpPeerLocalAs" not in cfg["bgp-peers"][proto]: print( "WARNING: Protocol \"%s\" does not have a properly formated 'local <ip> as <asn>;' line in the config" % proto) if "bgpPeerRemoteAddr" not in cfg["bgp-peers"][proto] and \ "bgpPeerRemoteAs" not in cfg["bgp-peers"][proto]: print( "WARNING: Protocol \"%s\" does not have a properly formated 'neighbor <ip> as <asn>;' line in the config" % proto) state = cfg.copy() bgp_proto = None # "with"-context-manager for Popen not available in python < 3.2 birdc = subprocess.Popen([self.birdcli, "show", "protocols", "all"], stdout=subprocess.PIPE) output = birdc.communicate()[0].decode('utf-8', 'ignore') if birdc.returncode != 0: print("ERROR: bird-CLI %s failed: %i" % (self.birdcli, birdc.returncode)) for line in output.split("\n"): match = self._re_birdcli_bgp_begin.search(line) if match: bgp_proto = match.group(1) timestamp = dateutil.parser.parse(match.group(2)) if not timestamp.tzinfo: timestamp = timezone.localize(timestamp) if bgp_proto not in state["bgp-peers"]: print("WARNING: proto \"%s\" not in config, skipping" % bgp_proto) continue state["bgp-peers"][bgp_proto][ "bgpPeerFsmEstablishedTime"] = SnmpGauge32( abs(current_time - timestamp).total_seconds()) if bgp_proto: try: for peerprop_name, peerprop_re in list( self._re_birdcli_bgp_peer.items()): match = peerprop_re.search(line) if match: if peerprop_name == 'bgpPeerState': if not match.group(1).lower() == 'down': state["bgp-peers"][bgp_proto][peerprop_name] = \ self.bgp_states[match.group(1).lower()] else: # handle disabled (down) protocols state["bgp-peers"][bgp_proto][ peerprop_name] = int(1) state["bgp-peers"][bgp_proto][ "bgpPeerAdminStatus"] = int(1) state["bgp-peers"][bgp_proto][ "bgpPeerFsmEstablishedTime"] = int(0) elif peerprop_name in [ 'bgpPeerIdentifier', 'bgpPeerLocalAddr', 'bgpPeerRemoteAddr' ]: state["bgp-peers"][bgp_proto][ peerprop_name] = SnmpIpAddress( match.group(1)) elif peerprop_name in [ 'bgpPeerInUpdates', 'bgpPeerOutUpdates' ]: state["bgp-peers"][bgp_proto][ peerprop_name] = SnmpCounter32( match.group(1)) else: state["bgp-peers"][bgp_proto][ peerprop_name] = int(match.group(1)) except: print( "WARNING: Unable to process \"%s\" as \"%s\" for protocol \"%s\"" % (match.group(1), peerprop_name, bgp_proto)) if self._re_birdcli_bgp_end.search(line): bgp_proto = None # use ss to query for source and destination ports of the bgp protocols bgp_sessions = {} try: ss = subprocess.Popen(self.sscmd, shell=True, stdout=subprocess.PIPE) for line in ss.communicate()[0].decode('utf-8', 'ignore').split("\n"): match = self._re_ss.search(line) if not match: continue # key 4-tuples by remote ip: src-addr, src-port, dst-addr, dst-port bgp_sessions[match.group(3)] = match.groups() except subprocess.CalledProcessError as e: print( "ERROR: Error executing \"ss\" command [%s], terminating..." % e) sys.exit(1) # match the connection 4-tuples with bgp-state for proto in list(state["bgp-peers"].keys()): # enrich the state by local+remote ports try: srcip, srcport, dstip, dstport = bgp_sessions[ state["bgp-peers"][proto]["bgpPeerRemoteAddr"]] except: print("INFO: Protocol \"%s\" has no active BGP session." % proto) try: state["bgp-peers"][proto]["bgpPeerRemoteAddr"] = \ cfg["bgp-peers"][proto]["bgpPeerRemoteAddr"] continue except: state["bgp-peers"][proto][ "bgpPeerRemoteAddr"] = SnmpIpAddress("0.0.0.0") continue # Check for mismatch between config and ss output if srcip != state["bgp-peers"][proto]["bgpPeerLocalAddr"] or \ dstip != state["bgp-peers"][proto]["bgpPeerRemoteAddr"]: print( "WARNING: Protocol \"%s\" has mismatch between the configuration file (local: %s, neighbor %s) and the active BGP session (local: %s, neighbor: %s)" % (proto, state["bgp-peers"][proto]["bgpPeerLocalAddr"], state["bgp-peers"][proto]["bgpPeerRemoteAddr"], srcip, dstip)) continue # populate the ports state["bgp-peers"][proto]["bgpPeerLocalPort"] = int(srcport) state["bgp-peers"][proto]["bgpPeerRemotePort"] = int(dstport) return state
class BirdAgent(object): def __init__(self, cfgfile, birdcli, sscmd): self.cfgfile = cfgfile self.birdcli = birdcli self.sscmd = sscmd bgp_states = { "idle": 1, "connect": 2, "active": 3, "opensent": 4, "openconfirm": 5, "established": 6, } _re_config_include = re.compile("^include\s*\"(/[^\"]*)\".*$") _re_config_bgp_proto_begin = re.compile( "^protocol bgp ([a-zA-Z0-9_]+).*\{$") _re_config_local_as = re.compile( "local ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) as ([0-9]+);") _re_config_bgp_holdtime = re.compile("hold time ([0-9]+);") _re_config_bgp_keepalive = re.compile("keepalive time ([0-9]+);") _re_config_remote_peer = re.compile( "neighbor ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) as ([0-9]+);") _re_config_timeformat = re.compile( "\s*timeformat\s+protocol\s+iso\s+long\s+;") _re_config_proto_end = re.compile("^\}$") _re_birdcli_bgp_begin = re.compile( "^([a-zA-Z0-9_]+)\s+BGP\s+[a-zA-Z0-9_]+\s+[a-zA-Z0-9]+\s+(\d\d\d\d-\d\d-\d\d\s\d\d:\d\d:\d\d).*$" ) _re_birdcli_bgp_peer = { "bgpPeerIdentifier": re.compile("^\s+Neighbor ID:\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$"), "bgpPeerState": re.compile("^\s+BGP state:\s+([a-zA-Z]+)$"), "bgpPeerLocalAddr": re.compile("^\s+Source address:\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$"), "bgpPeerRemoteAddr": re.compile( "^\s+Neighbor address:\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$"), "bgpPeerRemoteAs": re.compile("^\s+Neighbor AS:\s+([0-9]+)$"), "bgpPeerInUpdates": re.compile( "^\s+Import updates:\s+([0-9]+)\s+[0-9\-]+\s+[0-9\-]+\s+[0-9\-]+\s+[0-9\-]+$" ), "bgpPeerOutUpdates": re.compile( "^\s+Export updates:\s+([0-9]+)\s+[0-9\-]+\s+[0-9\-]+\s+[0-9\-]+\s+[0-9\-]+$" ), "bgpPeerHoldTime": re.compile("^\s+Hold timer:\s+([0-9]+)\/[0-9]+$"), "bgpPeerHoldTimeConfigured": re.compile("^\s+Hold timer:\s+[0-9]+\/([0-9]+)$"), "bgpPeerKeepAlive": re.compile("^\s+Keepalive timer:\s+([0-9]+)\/[0-9]+$"), "bgpPeerKeepAliveConfigured": re.compile("^\s+Keepalive timer:\s+[0-9]+\/([0-9]+)$"), "bgpPeerLastError": re.compile("^\s+Last error:\s+([a-zA-Z0-9-_\ ]+)$") } _re_birdcli_bgp_end = re.compile("^$") _re_birdcli_ospf_routerid = re.compile( "^Router ID is\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$") _re_birdcli_ospf_proto_status = re.compile( "^.+?\s+OSPF\s+.+?\s+(.+?)\s+(\d\d\d\d-\d\d-\d\d\s\d\d:\d\d:\d\d)\s+(.+?)$" ) _re_birdcli_ospf_area = re.compile( "^\s+Area:\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\s+\([0-9]+\)(.+?)?$") _re_birdcli_ospf_interfaces = re.compile("^Interface\s+(.+?)\s+\(.+?\)$") _re_birdcli_ospf_interface = { "state": re.compile("^\s+State:\s+([a-zA-Z]+)$"), "ipaddress": re.compile("^Interface\s+.+?\s+\((.+?)\)$"), "area": re.compile( "^\s+Area:\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\s+\([0-9]+\)(.+?)?$") } _re_birdcli_ospf_neighbor = re.compile( "^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\s+([0-9]+)\s+(\S+)\s+(\S+)\s+(\S+)\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)" ) _re_ss = re.compile( "^[0-9]+\s+[0-9]+\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)(?:%[a-z0-9-\.]+)?:([0-9]+)\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)(?:%[a-z0-9-\.]+?)?:([0-9]+)" ) bgp_keys = [ 'bgpPeerIdentifier', 'bgpPeerState', 'bgpPeerAdminStatus', 'bgpPeerNegotiatedVersion', 'bgpPeerLocalAddr', 'bgpPeerLocalPort', 'bgpPeerRemoteAddr', 'bgpPeerRemotePort', 'bgpPeerRemoteAs', 'bgpPeerInUpdates', 'bgpPeerOutUpdates', 'bgpPeerInTotalMessages', 'bgpPeerOutTotalMessages', 'bgpPeerLastError', 'bgpPeerFsmEstablishedTransitions', 'bgpPeerFsmEstablishedTime', 'bgpPeerConnectRetryInterval', 'bgpPeerHoldTime', 'bgpPeerKeepAlive', 'bgpPeerHoldTimeConfigured', 'bgpPeerKeepAliveConfigured', 'bgpPeerMinASOriginationInterval', 'bgpPeerMinRouteAdvertisementInterval', 'bgpPeerInUpdateElapsedTime', ] bgp_defaults = { 'bgpPeerIdentifier': SnmpIpAddress("0.0.0.0"), 'bgpPeerLocalAddr': SnmpIpAddress("0.0.0.0"), 'bgpPeerLocalPort': 0, 'bgpPeerRemoteAs': 0, 'bgpPeerRemotePort': 0, 'bgpPeerHoldTime': 0, 'bgpPeerHoldTimeConfigured': 0, 'bgpPeerKeepAlive': 0, 'bgpPeerKeepAliveConfigured': 0, 'bgpPeerState': 1, 'bgpPeerInUpdates': SnmpCounter32(0), 'bgpPeerOutUpdates': SnmpCounter32(0), 'bgpPeerAdminStatus': 2, 'bgpPeerConnectRetryInterval': 0, 'bgpPeerFsmEstablishedTime': SnmpGauge32(0), 'bgpPeerFsmEstablishedTransitions': SnmpCounter32(0), 'bgpPeerInTotalMessages': SnmpCounter32(0), 'bgpPeerInUpdateElapsedTime': SnmpGauge32(0), 'bgpPeerLastError': '0', 'bgpPeerMinASOriginationInterval': 15, 'bgpPeerMinRouteAdvertisementInterval': 30, 'bgpPeerNegotiatedVersion': 0, 'bgpPeerOutTotalMessages': SnmpCounter32(0), } @staticmethod def ipCompare(ip1, ip2): lst1 = "%3s.%3s.%3s.%3s" % tuple(ip1.split(".")) lst2 = "%3s.%3s.%3s.%3s" % tuple(ip2.split(".")) return cmp(lst1, lst2) @staticmethod def combinedConfigLines(filename): """ yield the whole bird configuration file line by line; all include-statements are resolved/unrolled """ try: with open(filename, "r") as bird_conf: for line in bird_conf: line = line.strip() match = BirdAgent._re_config_include.search(line) if not match: yield line else: for subconf in glob.glob(match.group(1)): yield "# subconf: %s (from %s)" % (subconf, line) for subline in BirdAgent.combinedConfigLines( subconf): yield subline except IOError: print("ERROR: Unable to open %s, terminating..." % filename) sys.exit(1) except Exception as e: print( "ERROR: Unexpected error in combinedConfigLines(): [%s], terminating" % e) sys.exit(1) @staticmethod def bgpKeys(): return BirdAgent.bgp_keys def getBGPState(self): """ fetch BGP-related state from: * parsing configuration file * parsing `birdc show protocols all` output * parsing `ss` output """ timezone = get_localzone() current_time = datetime.now(pytz.utc) # fetch some data from the configuration: cfg = {} cfg["bgp-peers"] = {} proto = None for line in BirdAgent.combinedConfigLines(self.cfgfile): if self._re_config_timeformat: cfg["timeformat"] = True match = self._re_config_bgp_proto_begin.search(line) if match: proto = match.group(1) cfg["bgp-peers"][proto] = {} if proto: match = self._re_config_local_as.search(line) if match: cfg["bgp-peers"][proto][ "bgpPeerLocalAddr"] = SnmpIpAddress(match.group(1)) cfg["bgp-peers"][proto]["bgpPeerLocalAs"] = int( match.group(2)) if "bgpLocalAs" not in cfg: cfg["bgpLocalAs"] = int(match.group(2)) elif cfg["bgpLocalAs"] != int(match.group(2)): print("WARNING: multiple local AS: %i/%i" % (cfg["bgpLocalAs"], int(match.group(2)))) match = self._re_config_remote_peer.search(line) if match: cfg["bgp-peers"][proto][ "bgpPeerRemoteAddr"] = SnmpIpAddress(match.group(1)) cfg["bgp-peers"][proto]["bgpPeerRemoteAs"] = int( match.group(2)) match = self._re_config_bgp_holdtime.search(line) if match: cfg["bgp-peers"][proto]["bgpPeerHoldTimeConfigured"] = int( match.group(1)) match = self._re_config_bgp_keepalive.search(line) if match: cfg["bgp-peers"][proto][ "bgpPeerKeepAliveConfigured"] = int(match.group(1)) if self._re_config_proto_end.search(line): proto = None if "timeformat" not in cfg: print( "ERROR: timeformat not configured for this agent's use, terminating..." ) sys.exit(1) # Validate protocol's config for proto in cfg["bgp-peers"]: if "bgpPeerLocalAddr" not in cfg["bgp-peers"][proto] and \ "bgpPeerLocalAs" not in cfg["bgp-peers"][proto]: print( "WARNING: Protocol \"%s\" does not have a properly formated 'local <ip> as <asn>;' line in the config" % proto) if "bgpPeerRemoteAddr" not in cfg["bgp-peers"][proto] and \ "bgpPeerRemoteAs" not in cfg["bgp-peers"][proto]: print( "WARNING: Protocol \"%s\" does not have a properly formated 'neighbor <ip> as <asn>;' line in the config" % proto) state = cfg.copy() bgp_proto = None # "with"-context-manager for Popen not available in python < 3.2 birdc = subprocess.Popen([self.birdcli, "show", "protocols", "all"], stdout=subprocess.PIPE) output = birdc.communicate()[0].decode('utf-8', 'ignore') if birdc.returncode != 0: print("ERROR: bird-CLI %s failed: %i" % (self.birdcli, birdc.returncode)) for line in output.split("\n"): match = self._re_birdcli_bgp_begin.search(line) if match: bgp_proto = match.group(1) timestamp = dateutil.parser.parse(match.group(2)) if not timestamp.tzinfo: timestamp = timezone.localize(timestamp) if bgp_proto not in state["bgp-peers"]: print("WARNING: proto \"%s\" not in config, skipping" % bgp_proto) continue state["bgp-peers"][bgp_proto][ "bgpPeerFsmEstablishedTime"] = SnmpGauge32( abs(current_time - timestamp).total_seconds()) if bgp_proto: try: for peerprop_name, peerprop_re in list( self._re_birdcli_bgp_peer.items()): match = peerprop_re.search(line) if match: if peerprop_name == 'bgpPeerState': if not match.group(1).lower() == 'down': state["bgp-peers"][bgp_proto][peerprop_name] = \ self.bgp_states[match.group(1).lower()] else: # handle disabled (down) protocols state["bgp-peers"][bgp_proto][ peerprop_name] = int(1) state["bgp-peers"][bgp_proto][ "bgpPeerAdminStatus"] = int(1) state["bgp-peers"][bgp_proto][ "bgpPeerFsmEstablishedTime"] = int(0) elif peerprop_name in [ 'bgpPeerIdentifier', 'bgpPeerLocalAddr', 'bgpPeerRemoteAddr' ]: state["bgp-peers"][bgp_proto][ peerprop_name] = SnmpIpAddress( match.group(1)) elif peerprop_name in [ 'bgpPeerInUpdates', 'bgpPeerOutUpdates' ]: state["bgp-peers"][bgp_proto][ peerprop_name] = SnmpCounter32( match.group(1)) else: state["bgp-peers"][bgp_proto][ peerprop_name] = int(match.group(1)) except: print( "WARNING: Unable to process \"%s\" as \"%s\" for protocol \"%s\"" % (match.group(1), peerprop_name, bgp_proto)) if self._re_birdcli_bgp_end.search(line): bgp_proto = None # use ss to query for source and destination ports of the bgp protocols bgp_sessions = {} try: ss = subprocess.Popen(self.sscmd, shell=True, stdout=subprocess.PIPE) for line in ss.communicate()[0].decode('utf-8', 'ignore').split("\n"): match = self._re_ss.search(line) if not match: continue # key 4-tuples by remote ip: src-addr, src-port, dst-addr, dst-port bgp_sessions[match.group(3)] = match.groups() except subprocess.CalledProcessError as e: print( "ERROR: Error executing \"ss\" command [%s], terminating..." % e) sys.exit(1) # match the connection 4-tuples with bgp-state for proto in list(state["bgp-peers"].keys()): # enrich the state by local+remote ports try: srcip, srcport, dstip, dstport = bgp_sessions[ state["bgp-peers"][proto]["bgpPeerRemoteAddr"]] except: print("INFO: Protocol \"%s\" has no active BGP session." % proto) try: state["bgp-peers"][proto]["bgpPeerRemoteAddr"] = \ cfg["bgp-peers"][proto]["bgpPeerRemoteAddr"] continue except: state["bgp-peers"][proto][ "bgpPeerRemoteAddr"] = SnmpIpAddress("0.0.0.0") continue # Check for mismatch between config and ss output if srcip != state["bgp-peers"][proto]["bgpPeerLocalAddr"] or \ dstip != state["bgp-peers"][proto]["bgpPeerRemoteAddr"]: print( "WARNING: Protocol \"%s\" has mismatch between the configuration file (local: %s, neighbor %s) and the active BGP session (local: %s, neighbor: %s)" % (proto, state["bgp-peers"][proto]["bgpPeerLocalAddr"], state["bgp-peers"][proto]["bgpPeerRemoteAddr"], srcip, dstip)) continue # populate the ports state["bgp-peers"][proto]["bgpPeerLocalPort"] = int(srcport) state["bgp-peers"][proto]["bgpPeerRemotePort"] = int(dstport) return state def ospfState2int(self, state): if state.lower().startswith("full"): return 8 elif state.lower().startswith("loading"): return 7 elif state.lower().startswith("exchange"): return 6 elif state.lower().startswith("exstart"): return 5 elif state.lower().startswith("2-way"): return 4 elif state.lower().startswith("init"): return 3 elif state.lower().startswith("attempt"): return 2 elif state.lower().startswith("down"): return 1 else: return 1 def getOSPFGeneralInfo(self, ospf_instance): """ fetch OSPF general information from: * parsing `birdc show status` output * parsing `birdc show proto $ospf_instance` output """ general_info = {} birdc = subprocess.Popen([self.birdcli, "show", "status"], stdout=subprocess.PIPE) output = birdc.communicate()[0].decode('utf-8', 'ignore') if birdc.returncode != 0: print("ERROR: bird-CLI (querying ospf) %s failed: %i" % (self.birdcli, birdc.returncode)) for line in output.split("\n"): match = self._re_birdcli_ospf_routerid.search(line) if match: general_info["router_id"] = match.groups()[0] # get ospf protocol info birdc = subprocess.Popen( [self.birdcli, "show", "proto", ospf_instance], stdout=subprocess.PIPE) output = birdc.communicate()[0].decode('utf-8', 'ignore') if birdc.returncode != 0: print("ERROR: bird-CLI (querying proto ospf) %s failed: %i" % (self.birdcli, birdc.returncode)) for line in output.split("\n"): match = self._re_birdcli_ospf_proto_status.search(line) if match: admin_state, since, state = match.groups() general_info[ "admin_state"] = "enabled" if admin_state == "up" else "disabled" general_info["up_since"] = since general_info["state"] = state return general_info def getOSPFAreas(self, ospf_instance): """ fetch OSPF general information from: * parsing `birdc show ospf $ospf_instance` output """ areas = [] birdc = subprocess.Popen([self.birdcli, "show", "ospf", ospf_instance], stdout=subprocess.PIPE) output = birdc.communicate()[0].decode('utf-8', 'ignore') if birdc.returncode != 0: print("ERROR: bird-CLI (querying ospf areas) %s failed: %i" % (self.birdcli, birdc.returncode)) for line in output.split("\n"): match = self._re_birdcli_ospf_area.search(line) if match: area = {} area["area_id"] = match.groups()[0] areas.append(area) return areas def getOSPFInterfaces(self, ospf_instance): """ fetch OSPF general information from: * parsing `birdc show ospf interface $ospf_instance` output """ interfaces = [] birdc = subprocess.Popen( [self.birdcli, "show", "ospf", "interface", ospf_instance], stdout=subprocess.PIPE) output = birdc.communicate()[0].decode('utf-8', 'ignore') if birdc.returncode != 0: print("ERROR: bird-CLI (querying ospf interfaces) %s failed: %i" % (self.birdcli, birdc.returncode)) # create a list of all interfaces for line in output.split("\n"): match = self._re_birdcli_ospf_interfaces.search(line) if match: interface = {} interface["interface_name"] = match.groups()[0] interfaces.append(interface) # get detail information for interface in interfaces: birdc = subprocess.Popen([ self.birdcli, "show", "ospf", "interface", ospf_instance, "\"%s\"" % interface["interface_name"] ], stdout=subprocess.PIPE) output = birdc.communicate()[0].decode('utf-8', 'ignore') if birdc.returncode != 0: print( "ERROR: bird-CLI (querying ospf interfaces) %s failed: %i" % (self.birdcli, birdc.returncode)) for line in output.split("\n"): matchipaddress = self._re_birdcli_ospf_interface[ "ipaddress"].search(line) if matchipaddress: interface["ipaddress"] = matchipaddress.groups()[0].split( '/', 1)[0] matcharea = self._re_birdcli_ospf_interface["area"].search( line) if matcharea: interface["area"] = matcharea.groups()[0] matchstate = self._re_birdcli_ospf_interface["state"].search( line) if matchstate: interface["state"] = matchstate.groups()[0] return interfaces def getOSPFNeighbors(self, ospf_instance): """ fetch OSPF-neighbors from: * parsing `birdc show ospf neighbors $ospf_instance` output """ # "with"-context-manager for Popen not available in python < 3.2 birdc = subprocess.Popen( [self.birdcli, "show", "ospf", "neighbors", ospf_instance], stdout=subprocess.PIPE) output = birdc.communicate()[0].decode('utf-8', 'ignore') if birdc.returncode != 0: print("ERROR: bird-CLI (querying ospf neighbors) %s failed: %i" % (self.birdcli, birdc.returncode)) neighbors = {} for line in output.split("\n"): match = self._re_birdcli_ospf_neighbor.search(line) if match: rtrid, pri, state, deadtime, iface, rtrip = match.groups() neighbors[rtrid] = {} neighbors[rtrid]["pri"] = int(pri) neighbors[rtrid]["state"] = self.ospfState2int(state) neighbors[rtrid]["deadtime"] = deadtime neighbors[rtrid]["iface"] = iface neighbors[rtrid]["rtrip"] = rtrip # ip-sorted neighbors neighbors_sorted = [] for nbrid in sorted(neighbors.keys(), key=functools.cmp_to_key(self.ipCompare)): neighbors_sorted.append((nbrid, neighbors[nbrid])) return neighbors_sorted def getOSPFState(self, ospf_instance): """ fetch OSPF state information """ ospf_state = {} ospf_state["general_info"] = self.getOSPFGeneralInfo(ospf_instance) ospf_state["areas"] = self.getOSPFAreas(ospf_instance) ospf_state["interfaces"] = self.getOSPFInterfaces(ospf_instance) ospf_state["neighbors"] = self.getOSPFNeighbors(ospf_instance) return ospf_state
class BirdAgent(object): def __init__(self, cfgfile, birdcli, sscmd): self.cfgfile = cfgfile self.birdcli = birdcli self.sscmd = sscmd bgp_states = { "idle": 1, "connect": 2, "active": 3, "opensent": 4, "openconfirm": 5, "established": 6, } _re_config_include = re.compile("^include\s*\"(/[^\"]*)\".*$") _re_config_bgp_proto_begin = re.compile( "^protocol bgp ([a-zA-Z0-9_]+).*\{$") _re_config_local_as = re.compile( "local ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) as ([0-9]+);") _re_config_bgp_holdtime = re.compile("hold time ([0-9]+);") _re_config_bgp_keepalive = re.compile("keepalive time ([0-9]+);") _re_config_remote_peer = re.compile( "neighbor ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) as ([0-9]+);") _re_config_timeformat = re.compile( "\s*timeformat\s+protocol\s+iso\s+long\s+;") _re_config_proto_end = re.compile("^\}$") _re_birdcli_bgp_begin = re.compile( "^([a-zA-Z0-9_]+)\s+BGP\s+[a-zA-Z0-9-_]+\s+[a-zA-Z0-9]+\s+(\d\d\d\d-\d\d-\d\d\s\d\d:\d\d:\d\d).*$" ) _re_birdcli_bgp_peer = { "bgpPeerIdentifier": re.compile("^\s+Neighbor ID:\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$"), "bgpPeerState": re.compile("^\s+BGP state:\s+([a-zA-Z]+)$"), "bgpPeerLocalAddr": re.compile("^\s+Source address:\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$"), "bgpPeerRemoteAddr": re.compile( "^\s+Neighbor address:\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$"), "bgpPeerRemoteAs": re.compile("^\s+Neighbor AS:\s+([0-9]+)$"), "bgpPeerInUpdates": re.compile( "^\s+Import updates:\s+([0-9]+)\s+[0-9\-]+\s+[0-9\-]+\s+[0-9\-]+\s+[0-9\-]+$" ), "bgpPeerOutUpdates": re.compile( "^\s+Export updates:\s+([0-9]+)\s+[0-9\-]+\s+[0-9\-]+\s+[0-9\-]+\s+[0-9\-]+$" ), "bgpPeerHoldTime": re.compile("^\s+Hold timer:\s+([0-9]+)\/[0-9]+$"), "bgpPeerHoldTimeConfigured": re.compile("^\s+Hold timer:\s+[0-9]+\/([0-9]+)$"), "bgpPeerKeepAlive": re.compile("^\s+Keepalive timer:\s+([0-9]+)\/[0-9]+$"), "bgpPeerKeepAliveConfigured": re.compile("^\s+Keepalive timer:\s+[0-9]+\/([0-9]+)$"), "bgpPeerLastError": re.compile("^\s+Last error:\s+([a-zA-Z0-9-_\ ]+)$") } _re_birdcli_bgp_end = re.compile("^$") _re_ss = re.compile( "^[0-9]+\s+[0-9]+\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)(?:%[a-z0-9-\.]+)?:([0-9]+)\s+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)(?:%[a-z0-9-\.]+?)?:([0-9]+)" ) bgp_keys = [ 'bgpPeerIdentifier', 'bgpPeerState', 'bgpPeerAdminStatus', 'bgpPeerNegotiatedVersion', 'bgpPeerLocalAddr', 'bgpPeerLocalPort', 'bgpPeerRemoteAddr', 'bgpPeerRemotePort', 'bgpPeerRemoteAs', 'bgpPeerInUpdates', 'bgpPeerOutUpdates', 'bgpPeerInTotalMessages', 'bgpPeerOutTotalMessages', 'bgpPeerLastError', 'bgpPeerFsmEstablishedTransitions', 'bgpPeerFsmEstablishedTime', 'bgpPeerConnectRetryInterval', 'bgpPeerHoldTime', 'bgpPeerKeepAlive', 'bgpPeerHoldTimeConfigured', 'bgpPeerKeepAliveConfigured', 'bgpPeerMinASOriginationInterval', 'bgpPeerMinRouteAdvertisementInterval', 'bgpPeerInUpdateElapsedTime', ] bgp_defaults = { 'bgpPeerIdentifier': SnmpIpAddress("0.0.0.0"), 'bgpPeerLocalAddr': SnmpIpAddress("0.0.0.0"), 'bgpPeerLocalPort': 0, 'bgpPeerRemoteAs': 0, 'bgpPeerRemotePort': 0, 'bgpPeerHoldTime': 0, 'bgpPeerHoldTimeConfigured': 0, 'bgpPeerKeepAlive': 0, 'bgpPeerKeepAliveConfigured': 0, 'bgpPeerState': 1, 'bgpPeerInUpdates': SnmpCounter32(0), 'bgpPeerOutUpdates': SnmpCounter32(0), 'bgpPeerAdminStatus': 2, 'bgpPeerConnectRetryInterval': 0, 'bgpPeerFsmEstablishedTime': SnmpGauge32(0), 'bgpPeerFsmEstablishedTransitions': SnmpCounter32(0), 'bgpPeerInTotalMessages': SnmpCounter32(0), 'bgpPeerInUpdateElapsedTime': SnmpGauge32(0), 'bgpPeerLastError': '0', 'bgpPeerMinASOriginationInterval': 15, 'bgpPeerMinRouteAdvertisementInterval': 30, 'bgpPeerNegotiatedVersion': 0, 'bgpPeerOutTotalMessages': SnmpCounter32(0), } @staticmethod def ipCompare(ip1, ip2): lst1 = "%3s.%3s.%3s.%3s" % tuple(ip1.split(".")) lst2 = "%3s.%3s.%3s.%3s" % tuple(ip2.split(".")) return cmp(lst1, lst2) @staticmethod def combinedConfigLines(filename): """ yield the whole bird configuration file line by line; all include-statements are resolved/unrolled """ try: with open(filename, "r") as bird_conf: for line in bird_conf: line = line.strip() match = BirdAgent._re_config_include.search(line) if not match: yield line else: for subconf in glob.glob(match.group(1)): yield "# subconf: %s (from %s)" % (subconf, line) for subline in BirdAgent.combinedConfigLines( subconf): yield subline except IOError: print("ERROR: Unable to open %s, terminating..." % filename) sys.exit(1) except Exception as e: print( "ERROR: Unexpected error in combinedConfigLines(): [%s], terminating" % e) sys.exit(1) @staticmethod def bgpKeys(): return BirdAgent.bgp_keys def getBGPState(self): """ fetch BGP-related state from: * parsing configuration file * parsing `birdc show protocols all` output * parsing `ss` output """ timezone = get_localzone() current_time = datetime.now(pytz.utc) # fetch some data from the configuration: cfg = {} cfg["bgp-peers"] = {} proto = None for line in BirdAgent.combinedConfigLines(self.cfgfile): if self._re_config_timeformat: cfg["timeformat"] = True match = self._re_config_bgp_proto_begin.search(line) if match: proto = match.group(1) cfg["bgp-peers"][proto] = {} if proto: match = self._re_config_local_as.search(line) if match: cfg["bgp-peers"][proto][ "bgpPeerLocalAddr"] = SnmpIpAddress(match.group(1)) cfg["bgp-peers"][proto]["bgpPeerLocalAs"] = int( match.group(2)) if "bgpLocalAs" not in cfg: cfg["bgpLocalAs"] = int(match.group(2)) elif cfg["bgpLocalAs"] != int(match.group(2)): print("WARNING: multiple local AS: %i/%i" % (cfg["bgpLocalAs"], int(match.group(2)))) match = self._re_config_remote_peer.search(line) if match: cfg["bgp-peers"][proto][ "bgpPeerRemoteAddr"] = SnmpIpAddress(match.group(1)) cfg["bgp-peers"][proto]["bgpPeerRemoteAs"] = int( match.group(2)) match = self._re_config_bgp_holdtime.search(line) if match: cfg["bgp-peers"][proto]["bgpPeerHoldTimeConfigured"] = int( match.group(1)) match = self._re_config_bgp_keepalive.search(line) if match: cfg["bgp-peers"][proto][ "bgpPeerKeepAliveConfigured"] = int(match.group(1)) if self._re_config_proto_end.search(line): proto = None if "timeformat" not in cfg: print( "ERROR: timeformat not configured for this agent's use, terminating..." ) sys.exit(1) # Validate protocol's config for proto in cfg["bgp-peers"]: if not cfg["bgp-peers"][proto]: continue if "bgpPeerLocalAddr" not in cfg["bgp-peers"][proto] and \ "bgpPeerLocalAs" not in cfg["bgp-peers"][proto]: print( "WARNING: Protocol \"%s\" does not have a properly formated 'local <ip> as <asn>;' line in the config" % proto) if "bgpPeerRemoteAddr" not in cfg["bgp-peers"][proto] and \ "bgpPeerRemoteAs" not in cfg["bgp-peers"][proto]: print( "WARNING: Protocol \"%s\" does not have a properly formated 'neighbor <ip> as <asn>;' line in the config" % proto) state = cfg.copy() bgp_proto = None # "with"-context-manager for Popen not available in python < 3.2 birdc = subprocess.Popen([self.birdcli, "show", "protocols", "all"], stdout=subprocess.PIPE) output = birdc.communicate()[0].decode('utf-8', 'ignore') if birdc.returncode != 0: print("ERROR: bird-CLI %s failed: %i" % (self.birdcli, birdc.returncode)) for line in output.split("\n"): match = self._re_birdcli_bgp_begin.search(line) if match: bgp_proto = match.group(1) timestamp = dateutil.parser.parse(match.group(2)) if not timestamp.tzinfo: timestamp = timezone.localize(timestamp) if bgp_proto not in state["bgp-peers"]: print("WARNING: proto \"%s\" not in config, skipping" % bgp_proto) continue state["bgp-peers"][bgp_proto][ "bgpPeerFsmEstablishedTime"] = SnmpGauge32( abs(current_time - timestamp).total_seconds()) if bgp_proto: try: for peerprop_name, peerprop_re in list( self._re_birdcli_bgp_peer.items()): match = peerprop_re.search(line) if match: if peerprop_name == 'bgpPeerState': if not match.group(1).lower() == 'down': state["bgp-peers"][bgp_proto][peerprop_name] = \ self.bgp_states[match.group(1).lower()] else: # handle disabled (down) protocols state["bgp-peers"][bgp_proto][ peerprop_name] = int(1) state["bgp-peers"][bgp_proto][ "bgpPeerAdminStatus"] = int(1) state["bgp-peers"][bgp_proto][ "bgpPeerFsmEstablishedTime"] = int(0) elif peerprop_name in [ 'bgpPeerIdentifier', 'bgpPeerLocalAddr', 'bgpPeerRemoteAddr' ]: state["bgp-peers"][bgp_proto][ peerprop_name] = SnmpIpAddress( match.group(1)) elif peerprop_name in [ 'bgpPeerInUpdates', 'bgpPeerOutUpdates' ]: state["bgp-peers"][bgp_proto][ peerprop_name] = SnmpCounter32( match.group(1)) else: state["bgp-peers"][bgp_proto][ peerprop_name] = int(match.group(1)) except: print( "WARNING: Unable to process \"%s\" as \"%s\" for protocol \"%s\"" % (match.group(1), peerprop_name, bgp_proto)) if self._re_birdcli_bgp_end.search(line): bgp_proto = None # use ss to query for source and destination ports of the bgp protocols bgp_sessions = {} try: ss = subprocess.Popen(self.sscmd, shell=True, stdout=subprocess.PIPE) for line in ss.communicate()[0].decode('utf-8', 'ignore').split("\n"): match = self._re_ss.search(line) if not match: continue # key 4-tuples by remote ip: src-addr, src-port, dst-addr, dst-port bgp_sessions[match.group(3)] = match.groups() except subprocess.CalledProcessError as e: print( "ERROR: Error executing \"ss\" command [%s], terminating..." % e) sys.exit(1) # match the connection 4-tuples with bgp-state for proto in list(state["bgp-peers"].keys()): if not state["bgp-peers"][proto]: continue # enrich the state by local+remote ports try: srcip, srcport, dstip, dstport = bgp_sessions[ state["bgp-peers"][proto]["bgpPeerRemoteAddr"]] except: print("INFO: Protocol \"%s\" has no active BGP session." % proto) try: state["bgp-peers"][proto]["bgpPeerRemoteAddr"] = \ cfg["bgp-peers"][proto]["bgpPeerRemoteAddr"] continue except: state["bgp-peers"][proto][ "bgpPeerRemoteAddr"] = SnmpIpAddress("0.0.0.0") continue # Check for mismatch between config and ss output if srcip != state["bgp-peers"][proto]["bgpPeerLocalAddr"] or \ dstip != state["bgp-peers"][proto]["bgpPeerRemoteAddr"]: print( "WARNING: Protocol \"%s\" has mismatch between the configuration file (local: %s, neighbor %s) and the active BGP session (local: %s, neighbor: %s)" % (proto, state["bgp-peers"][proto]["bgpPeerLocalAddr"], state["bgp-peers"][proto]["bgpPeerRemoteAddr"], srcip, dstip)) continue # populate the ports state["bgp-peers"][proto]["bgpPeerLocalPort"] = int(srcport) state["bgp-peers"][proto]["bgpPeerRemotePort"] = int(dstport) return state