#!/usr/bin/python
#
# Check state of OSPF sessions in Bird Internet Routing Daemon
#
# Maximilian Wilhelm <max@rfc2324.org>
#  --  Wed 26 Apr 2017 07:26:48 PM CEST
#

import argparse
import os.path
import re
import subprocess
import sys


def read_interfaces_from_file (file_path, missing_ok):
	interfaces = []

	# 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 interfaces

	try:
		with open (args.interfaces_down_ok_file, 'r') as ido_fh:
			for iface in ido_fh.readlines ():
				if not iface.startswith ('#'):
					interfaces.append (iface.strip ())

	except IOError as (errno, strerror):
		print "Failed to read interfaces_down_ok from '%s': %s" % (args.interfaces_down_ok_file, strerror)
		sys.exit (1)

	return interfaces


def format_peer (ifname, iface_config, peer):
	global args

	if args.no_ptp_ip and iface_config['Type'] == 'ptp':
		return ifname

	return "%s/%s" % (ifname, peer)


parser = argparse.ArgumentParser (description = 'check bird OSPF sessions')

parser.add_argument ('--proto', '-p', help = 'IP protocol version to check', default = '4', choices = ['4', '6'])
parser.add_argument ('--protocol', '-P', help = 'Bird OSPF protocol instance name to check', default = "")
parser.add_argument ('--interfaces_down_ok', metavar = "LIST", help = "List of interfaces which are OK to have no OSPF neighbor. Provide a space separated list.")
parser.add_argument ('--interfaces_down_ok_file', metavar = "FILENAME", help = "List of interfaces which are OK to have no OSPF neighbor. 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')
parser.add_argument ('--no_ptp_ip', help = "Do not print router ID/IP on ptp interfaces", action = 'store_true')

args = parser.parse_args ()


# Are some interfaces ok being down?
interfaces_down_ok = []
if args.interfaces_down_ok:
	interfaces_down_ok = args.interfaces_down_ok.split ()

if args.interfaces_down_ok_file:
	interfaces_down_ok.extend (read_interfaces_from_file (args.interfaces_down_ok_file, args.ignore_missing_file))


################################################################################
#                   Query OSPF protocl information from bird                   #
################################################################################
cmds = {
	'4' : '/usr/sbin/birdc',
	'6' : '/usr/sbin/birdc6',
}

cmd_interfaces = [ "/usr/bin/sudo", cmds[args.proto], "show ospf interface %s" % args.protocol ]
cmd_neighbors  = [ "/usr/bin/sudo", cmds[args.proto], "show ospf neighbors %s" % args.protocol ]

try:
	interfaces_fh = subprocess.Popen (cmd_interfaces, bufsize = 4194304, stdout = subprocess.PIPE)
	if interfaces_fh.returncode:
		print "Failed to get OSPF interfaces from bird: %s" % str (" ".join ([line.strip () for line in interfaces_fh.stdout.readlines ()]))
		sys.exit (1)

	neighbors_fh  = subprocess.Popen (cmd_neighbors,  bufsize = 4194304, stdout = subprocess.PIPE)
	if neighbors_fh.returncode:
		print "Failed to get OSPF neighbors from bird: %s" % str (" ".join ([line.strip () for line in neighbors_fh.stdout.readlines ()]))
		sys.exit (1)

# cmd exited with non-zero code
except subprocess.CalledProcessError as c:
	print "Failed to get OSPF information from bird: %s" % c.output
	sys.exit (1)

# This should not have happend.
except Exception as e:
	print "Unknown error while getting OSPF information from bird: %s" % str (e)
	sys.exit (3)


################################################################################
#                        Parse interfaces and neighbors                        #
################################################################################

interfaces = {}

interface_re = re.compile (r'^Interface (.+) \(')
state_re = re.compile (r'(Type|State): (.+)$')
stub_re = re.compile (r'\(stub\)')

# Parse interfaces
interface = None
for line in interfaces_fh.stdout.readlines ():
	line = line.strip ()

	# Create empty interface hash
	match = interface_re.search (line)
	if match:
		interface = match.group (1)
		interfaces[interface] = {}
		continue

	# Store Type and State attributes
	match = state_re.search (line)
	if match:
		interfaces[interface][match.group (1)] = match.group (2)


# Delete any stub interfaces from our list
for iface in interfaces.keys ():
	if stub_re.search (interfaces[iface]['State']):
		del interfaces[iface]


ok = []
broken = []
down = []

neighbor_re = re.compile (r'^([0-9a-fA-F.:]+)\s+(\d+)\s+([\w/-]+)\s+([0-9:]+)\s+([\w.-]+)\s+([\w.:]+)')

# Read and check all neighbor states
for line in neighbors_fh.stdout.readlines ():
	line = line.strip ()

	match = neighbor_re.search (line)
	if match:
		peer = match.group (1)
		state = match.group (3)
		ifname = match.group (5)

		interface = interfaces[ifname]

		# Mark interfaces as "up" in bird
		interface['up'] = 1

		# State FULL is awesome.
		if 'Full' in state:
			ok.append (format_peer (ifname, interface, peer))

		# In broadcast areas there are only two FULL sessions (to the DR and BDR)
		# all other sessions will be 2-Way/Other which is perfectly fine.
		elif state == "2-Way/Other" and interface['Type'] == "broadcast":
			ok.append (format_peer (ifname, interface, peer))

		# Everything else is considered broken.
		# Likely some ExStart/* etc. pointing to possible MTU troubles.
		else:
			broken.append ("%s:%s" % (format_peer (ifname, interface, peer), state))


# Check for any interfaces which should have (at least) an OSPF peer
# but don't appear in the neighbors list
for iface in interfaces.keys ():
	if iface in interfaces_down_ok:
		ok.append ("%s (Down/OK)" % iface)

	elif "up" not in interfaces[iface]:
		down.append (iface)


################################################################################
#                                Prepare output                                #
################################################################################

ret_code = 0

# Any down interfaces?
if len (down) > 0:
	ret_code = 2
	print "DOWN: %s" % ", ".join (sorted (down))

# Any broken sessions?
if len (broken) > 0:
	# Issue a warning when there are issues..
	if ret_code < 2:
		ret_code = 1

	print "BROKEN: %s" % ", ".join (sorted (broken))

# And the good ones
if len (ok) > 0:
	print "OK: %s" % ", ".join (sorted (ok))

sys.exit (ret_code)
