123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- #!/usr/bin/python
- #
- # Check state of BGP sessions in Bird Internet Routing Daemon
- #
- # Maximilian Wilhelm <max@rfc2324.org>
- # -- Thu 13 Apr 2017 12:04:13 PM CEST
- #
- import argparse
- import os
- import re
- import subprocess
- import sys
- def read_sessions_from_file (file_path, missing_ok):
- sessions = []
- # If we shouldn't care, we won't care if it's not there.
- if not os.path.isfile (file_path) and missing_ok:
- return sessions
- try:
- with open (args.sessions_down_ok_file, 'r') as ido_fh:
- for session in ido_fh.readlines ():
- if not session.startswith ('#'):
- sessions.append (session.strip ())
- except IOError as (errno, strerror):
- print "Failed to read sessions_down_ok from '%s': %s" % (args.sessions_down_ok_file, strerror)
- sys.exit (1)
- return sessions
- parser = argparse.ArgumentParser (description = 'check bird iBGP sessions')
- parser.add_argument ('--proto', '-p', help = 'IP protocol version to check', default = '4', choices = ['4', '6'])
- parser.add_argument ('--asn', '-A', help = "Local AS number", required = True)
- parser.add_argument ('--ibgp', '-i', help = "Check iBGP sessions", action = 'store_true')
- parser.add_argument ('--ibgp_w', help = "Warning interval for down iBGP sessions", default = "1:1", metavar = "RANGE")
- parser.add_argument ('--ibgp_c', help = "Critical interval for down iBGP sessions", default = "2:", metavar = "RANGE")
- parser.add_argument ('--ebgp', '-e', help = "Check eBGP sessions", action = 'store_true')
- parser.add_argument ('--ebgp_w', help = "Warning interval for down eBGP sessions", default = "1:1", metavar = "RANGE")
- parser.add_argument ('--ebgp_c', help = "Critical interval for down eBGP sessions", default = "2:", metavar = "RANGE")
- parser.add_argument ('--disabled_ok', help = "Treat sessions disabled in bird as OK.", action = 'store_true')
- parser.add_argument ('--sessions_down_ok', metavar = "LIST", help = "List of sessions which are OK to be down. Provide a space separated list.")
- parser.add_argument ('--sessions_down_ok_file', metavar = "FILENAME", help = "List of sessions which are OK to be down. Provide one interfaces per line.")
- parser.add_argument ('--ignore_missing_file', help = "Ignore a possible non-existent file given as --interfaces_down_ok_file", action = 'store_true')
- args = parser.parse_args ()
- if not args.ibgp and not args.ebgp:
- print >> sys.stderr, "Error: You have to enable at least one of iBGP and eBGP checking.\n"
- parser.print_help ()
- sys.exit (3)
- session_down_codes = {
- 'w' : 1,
- 'c' : 2,
- }
- # Are some sessions ok being down?
- sessions_down_ok = []
- if args.sessions_down_ok:
- sessions_down_ok = args.sessions_down_ok.split ()
- if args.sessions_down_ok_file:
- sessions_down_ok.extend (read_sessions_from_file (args.sessions_down_ok_file, args.ignore_missing_file))
- ################################################################################
- # Query BGP protocols from bird #
- ################################################################################
- cmds = {
- '4' : '/usr/sbin/birdc',
- '6' : '/usr/sbin/birdc6',
- }
- cmd = [ "/usr/bin/sudo", cmds[args.proto], "show protocols all" ]
- try:
- protocols = subprocess.Popen (cmd, bufsize = 4194304, stdout = subprocess.PIPE).stdout
- # cmd exited with non-zero code
- except subprocess.CalledProcessError as c:
- print "Failed to run %s: %s" % (" ".join (cmd), c.output)
- sys.exit (1)
- # This should not have happend.
- except Exception as e:
- print "Unknown error while running %s: %s" % (" ".join (cmd), str (e))
- sys.exit (3)
- # cr03_in_ffho_net BGP master up 2017-04-06 Established
- # Preference: 100
- # Input filter: ibgp_in
- # Output filter: ibgp_out
- # Routes: 38 imported, 3 exported, 1 preferred
- # Route change stats: received rejected filtered ignored accepted
- # Import updates: 16779 0 0 72 16707
- # Import withdraws: 18012 0 --- 1355 16657
- # Export updates: 55104 18903 24743 --- 11458
- # Export withdraws: 9789 --- --- --- 11455
- # BGP state: Established
- # Neighbor address: 10.132.255.3
- # Neighbor AS: 65132
- # Neighbor ID: 10.132.255.3
- # Neighbor caps: refresh enhanced-refresh restart-able AS4
- # Session: internal multihop AS4
- # Source address: 10.132.255.12
- # Hold timer: 198/240
- # Keepalive timer: 13/80
- ################################################################################
- # Parse all fields from bird output into bgp_sessions dict #
- ################################################################################
- bgp_sessions = {}
- # Simple fields with only one values
- simple_fields = [ 'Preference', 'Input filter', 'Output filter', 'BGP state', 'Neighbor address', 'Neighbor AS',
- 'Neighbor ID', 'Source address', 'Hold timer', 'Keepalive timer', 'Last error' ]
- # More "complex" fields
- fields = {
- 'Routes' : {
- 're' : re.compile (r'Routes:\s+(\d+) imported, (\d+) exported, (\d+) preferred'),
- 'groups' : [ 1, 2, 3 ],
- 'mangle_dict' : {
- 'Routes imported' : 1,
- 'Routes exported' : 2,
- 'Routes preferred' : 3,
- }
- },
- 'Neighbor caps' : {
- 're' : re.compile (r'Neighbor caps:\s+(.+)$'),
- 'groups' : [ 1 ],
- 'list' : True,
- 'split' : lambda x: x.split (),
- },
- 'Session' : {
- 're' : re.compile (r'Session:\s+(.+)$'),
- 'groups' : [ 1 ],
- 'list' : True,
- 'split' : lambda x: x.split (),
- },
- }
- # Generate entries for simple fields
- for field in simple_fields:
- fields[field] = {
- 're' : re.compile (r'^\s*%s:\s+(.+)$' % field),
- 'groups' : [ 1 ],
- }
- proto_re = re.compile (r'^([0-9a-zA-Z_.-]+)\s+BGP\s+') # XXX
- ignore_re = re.compile (r'^(BIRD [0-9.]+ ready.|name\s+proto\s+table\s+.*)?$')
- # Parse session list
- protocol = None
- proto_dict = None
- for line in protocols.readlines ():
- line = line.strip ()
- # Preamble or empty string
- if ignore_re.search (line):
- protocol = None
- proto_dict = None
- continue
- # Start of a new protocol
- match = proto_re.search (line)
- if match:
- protocol = match.group (1)
- bgp_sessions[protocol] = {}
- proto_dict = bgp_sessions[protocol]
- continue
- # Ignore any non-BGP protocols, empty lines, etc.
- if protocol == None:
- continue
- # Parse and store any interesting lines / fields
- for field, config in fields.items ():
- match = config['re'].search (line)
- if not match:
- continue
- # Get values from match
- values = []
- for group in config['groups']:
- values.append (match.group (group))
- # Store entries separately?
- mangle_dict = config.get ('mangle_dict', None)
- if mangle_dict:
- for entry, group in mangle_dict.items ():
- proto_dict[entry] = match.group (group)
- # Store as list?
- if config.get ('list', False) == True:
- proto_dict[field] = config['split'] (match.group (1))
- # Store as string
- else:
- proto_dict[field] = " ".join (values)
- ################################################################################
- # Check the status quo #
- ################################################################################
- up = []
- down = []
- ret_code = 0
- down_by_proto = {
- 'ibgp' : [],
- 'ebgp' : []
- }
- for protoname, config in sorted (bgp_sessions.items ()):
- # Skip iBGP/eBGP sessions when not asked to check them
- session_args = config.get ('Session', [])
- if (args.ibgp != True and (('internal' in session_args) or (config['Neighbor AS'] == args.asn))) or \
- (args.ebgp != True and (('external' in session_args) or (config['Neighbor AS'] != args.asn))):
- continue
- session_type = "ibgp"
- if ('external' in session_args) or (config['Neighbor AS'] != args.asn):
- session_type = "ebgp"
- remote_as = "I" if session_type == "ibgp" else config.get ('Neighbor AS')
- session_desc = "%s/%s" % (protoname, remote_as)
- bgp_state = config['BGP state']
- if bgp_state == 'Established':
- up.append (session_desc)
- # Session disable and we don't care
- elif bgp_state == 'Down' and args.disabled_ok:
- up.append (session_desc + " (Disabled)")
- # Session down but in session_down_ok* list
- elif protoname in sessions_down_ok:
- up.append (session_desc + " (Down/OK)")
- # Something's broken
- else:
- last_error = 'Disabled' if bgp_state == 'Down' else config.get ('Last error', 'unkown')
- session_desc += " (%s)" % last_error
- down.append (session_desc)
- down_by_proto[session_type].append (session_desc)
- for proto, sessions in down_by_proto.items ():
- down_sessions = len (sessions)
- if down_sessions == 0:
- continue
- for level in [ 'w', 'c' ]:
- limits = getattr (args, "%s_%s" % (proto, level)).split (":")
- code = session_down_codes[level]
- # Check if
- if (limits[0] == '' or down_sessions >= int (limits[0])) and \
- (limits[1] == '' or down_sessions <= int (limits[1])):
- if ret_code < code:
- ret_code = code
- if len (down) > 0:
- print "DOWN: %s" % ", ".join (down)
- if len (up) > 0:
- print "OK: %s" % ", ".join (up)
- sys.exit (ret_code)
|