Radius External Monitor (Python)

Problem this snippet solves:

Note: This script works and is usable however currently the only issue is that for some unknown reason it fails authentication on valid credentials, but since it still gets a valid response from the Radius server it will still mark the pool member as up. This only occurs via a configured monitor using either variable or arguments, running the script via command line with the same arguments works.

This python script is an external monitor for radius that checks for any radius response in order to mark a pool member as up. If the connection times out the script does not output any response and as such will result in the member being marked as down.

The script strips of routing domain tags and the IPv6 padding of the Node IPs.

How to use this snippet:

Installation:

  • Import as an External Monitor Program File and name it however you want.
  • Get and copy python modules Six (six py) and Py-Radius (radius py) then copy them to a directory with suitable permissions such as the /config/eav directory.
  • If you're running python 2.6, which you probably will, then the radius module will need to be modified as per Code Mods section in the code below.

Monitor Configuration:

Set up a monitor with at the very least a valid SECRET, if you're looking to simply test whether the Radius server is alive and responding then the user account does not need to be valid. Valid Environment Variables are as follows:

  • DEBUG = 0, 1, 2 (Default: 0)
  • MOD_PATH = Directory containing the Python modules (Default: /config/eav)
  • SECRET = Radius server secret (Default: NoSecret)
  • USER = User account (Default: radius_monitor)
  • PASSWD = Account Password (Default: NoPassword)

Notes:

  • The configured environment variables can overridden using Arguments with TESTME as the first argument followed by the variables you want to override in the same format as above, eg. KEY=Value. This is usedful for troubleshooting and can be used to override the F5 supplied NODE_IP and NODE_PORT variables.
  • Log file location: /var/log/monitors
  • Debug level 2 outputs environment variables, command line arguments and radius module into the log file.

Code :

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Filename  : radius_mon.py
# Author    : ATennent
# Version   : 1.1
# Date      : 2018/09/14
# Python ver: 2.6+
# F5 version: 12.0+

# ========== Installation
# Import this script via GUI:
# System > File Management > External Monitor Program File List > Import...
# Name it however you want.
# Get, modify and copy the following modules:
# ========== Required modules
# -- six --
# https://pypi.org/project/six/
# Copy six.py into /config/eav
#
# -- py-radius --
# https://pypi.org/project/py-radius/
# Copy radius.py into /config/eav
# If running python2.6 modify radius.py as per "Code mods -- python.py --" section below

# ========== Notes
# NB: Any and all outputs from EAV scripts equate to a POSITIVE result by the F5 monitor
# so to make the result of the script to be a NEGATIVE result, ie. a DOWN state, we need
# to ensure that the script does not output anything when the attempt results in a time out.

# ========== Environment Variables
# NODE_IP   -   Supplied by F5 monitor
# NODE_PORT -   Supplied by F5 monitor
# MOD_PATH  -   Path to location of Python modules six.py and radius.py, default: /config/eav
# SECRET    -   Radius server secret
# USER      -   Username for test account
# PASSWD    -   Password for user account
# DEBUG     -   Enable/Disable Debugging
# ========== Additional Environment Variables (supplied at execution)
# MON_TMPL_NAME                 -   Monitor Name, eg. '/Common/radius_monitor'
# RUN_I                         -   Run file, eg. '/Common/radius_mon.py'
# SECURITY_FIPS140_COMPLIANCE   -   'true'/'false'
# PATH                          -   Colon delimited and not editable.
# ARGS_I                        -   Copy of command line Arguments
# NODE_NAME                     -   Node Name, eg, '/Common/SNGPCTXWEB01'
# MON_INST_LOG_NAME             -   Pool member's monitor log, eg. '/var/log/monitors/Common_radius_monitor-Common_MyNode-1812.log'
# SECURITY_COMMONCRITERIA       -   'true'/'false'

# ========== Code mods -- python.py --
# Dictionary comprehension fixes:
# https://stackoverflow.com/questions/1747817/create-a-dictionary-with-list-comprehension-in-python#1747827
# LINE:193; replace with
#ATTR_NAMES = dict((v.lower(), k) for (k, v) in ATTRS.items())
#
# LINE:100; replace with
#CODE_NAMES = dict((v.lower(), k) for (k, v) in CODES.items())
#
# Logger.NullHandler fix:
# https://stackoverflow.com/questions/33175763/how-to-use-logging-nullhandler-in-python-2-6#34939479
# LINE:64-65; replace with
#LOGGER = logging.getLogger(__name__)
#try:  # Python 2.7+
#    LOGGER.addHandler(logging.NullHandler())
#except AttributeError:
#    class NullHandler(logging.Handler):
#        def emit(self, record):
#            pass
#    LOGGER.addHandler(NullHandler())

# ========== Imports/Modules
from sys import path
from sys import argv
from sys import stdout
from os import environ
import logging.handlers
import re

if environ.get('MOD_PATH'):
    path.append(environ.get('MOD_PATH'))
else:
    path.append('/config/eav')

import radius

# ========== Dictionary Defaults
opts_dict = {'NODE_IP': '', 'NODE_PORT': '',
             'SECRET': 'NoSecret',
             'USER':   'radius_monitor',
             'PASSWD': 'NoPassword',
             'DEBUG':  '0',
             }

# ========== TEST w/ command line
try:
    if argv[3] == 'TESTME':
        environ['NODE_IP'] = argv[1]
        environ['NODE_PORT'] = argv[2]
        for cmd_opt in argv[3:]:
            if 'DEBUG' in cmd_opt:
                environ['DEBUG'] = cmd_opt.split('=')[1]
            elif 'SECRET' in cmd_opt:
                environ['SECRET'] = cmd_opt.split('=')[1]
            elif 'USER' in cmd_opt:
                environ['USER'] = cmd_opt.split('=')[1]
            elif 'PASSWD' in cmd_opt:
                environ['PASSWD'] = cmd_opt.split('=')[1]
            else:
                continue
except:
    pass

# ========== Logging Config
try:
    if int(environ.get('DEBUG')) == 0:
        log_file = '/dev/null'
    elif int(environ.get('DEBUG')) <=2:
        log_file = environ.get('MON_INST_LOG_NAME')
    else:
        log_file = '/dev/null'
except:
    # DEBUG not supplied as ENV variable
    environ['DEBUG'] = opts_dict['DEBUG']
    log_file = '/dev/null'

if int(environ.get('DEBUG')) == 2:
    logging.basicConfig(
        filename=log_file,
        level=logging.DEBUG,
        format='%(asctime)s %(name)s:%(levelname)s %(message)s')
else:
    logging.basicConfig(
        filename=log_file,
        level=logging.INFO,
        format='%(asctime)s %(name)s:%(levelname)s %(message)s')
log = logging.getLogger('radius_mon')
syslog_handler = logging.handlers.SysLogHandler(address='/dev/log', facility=0)
log.addHandler(syslog_handler)
if int(environ.get('DEBUG')) == 0:
    log.setLevel(logging.INFO)
    opts_dict['DEBUG'] = environ.get('DEBUG')
elif int(environ.get('DEBUG')) == 1:
    log.setLevel(logging.DEBUG)
    opts_dict['DEBUG'] = environ.get('DEBUG')
elif int(environ.get('DEBUG')) == 2:
    log.setLevel(logging.DEBUG)
    opts_dict['DEBUG'] = environ.get('DEBUG')
    log.debug('VERBOSE Debug Enabled, CMD line and ENV variables will be dumped to file.')
else:
    log.error('Bad DEBUG value!')

# ========== CMD Line and ENV Dump
if int(opts_dict['DEBUG']) == 2:
    log.debug('=========== CMD ARGS')
    log.debug(argv[0:])
    log.debug('=========== ENV VARS')
    log.debug(environ)

# ========== Update Dictionary
if environ.get('NODE_IP'):
    opts_dict['NODE_IP'] = environ.get('NODE_IP')
    log.debug('Set NODE_IP, %s', opts_dict['NODE_IP'])
else:
    log.error('NODE_IP not supplied as ENV variable')
    log.error('Exiting..')
    exit(0)
if environ.get('NODE_PORT'):
    opts_dict['NODE_PORT'] = environ.get('NODE_PORT')
    log.debug('Set NODE_PORT, %s', opts_dict['NODE_PORT'])
else:
    log.error('NODE_PORT not supplied as ENV variable')
    log.error('Exiting..')
    exit(0)
if environ.get('SECRET'):
    opts_dict['SECRET'] = environ.get('SECRET')
    log.debug('Set SECRET, %s', opts_dict['SECRET'])
else:
    log.debug('SECRET not supplied as ENV variable')
if environ.get('USER'):
    opts_dict['USER'] = environ.get('USER')
    log.debug('Set USER, %s', opts_dict['USER'])
else:
    log.debug('USER not supplied as ENV variable')
if environ.get('PASSWD'):
    opts_dict['PASSWD'] = environ.get('PASSWD')
    log.debug('Set PASSWD, %s', opts_dict['PASSWD'])
else:
    log.debug('PASSWD not supplied as ENV variable')

# ========== Clean up NODE_IP for IPv4
if '::ffff:' in opts_dict['NODE_IP']:
    opts_dict['NODE_IP'] = re.sub(r'::ffff:','', opts_dict['NODE_IP'])
    log.debug('Stripped IPv6 notation from IPv4 address')
# ========== Strip routing domain from NODE_IP
if '%' in opts_dict['NODE_IP']:
    opts_dict['NODE_IP'] = re.sub(r'%\d{1,4}$', '', opts_dict['NODE_IP'])
    log.debug('Stripped routing domain from IP address')
# ========== Dictionary debug
if int(opts_dict['DEBUG']) == 2:
    log.debug('----- KEY, VALUE pairs:')
    for key in opts_dict.keys():
        log.debug('%s, %s', key, opts_dict[key])
    log.debug('-----')

# ========== Main
r = radius.Radius(opts_dict['SECRET'], host=opts_dict['NODE_IP'], port=int(opts_dict['NODE_PORT']))
try:
    if r.authenticate(opts_dict['USER'], opts_dict['PASSWD']):
        log.debug('Service UP, Authentication attempt Successful')
        stdout.write('UP')
    else:
        log.debug('Service UP, Authentication attempt Failure')
        stdout.write('UP')
except radius.ChallengeResponse:
    log.debug('Service UP, Authentication attempt Challenge Response')
    stdout.write('UP')
except (radius.NoResponse, radius.SocketError):
    # This includes bad SECRET keys, the radius RFC states that Invalid responses from the radius
    # server are to be silently dropped which, from a client perspective, will result ina  timeout.
    # This cannot be changed without modifying radius.py to instead raise an Exception.
    log.debug('No Response from %s:%s', opts_dict['NODE_IP'], int(opts_dict['NODE_PORT']))
    exit(0)
except Exception as msg:
    log.error('Exception due to %s, marking as DOWN', msg)
    exit(0)

Tested this on version:

12.0
Published Sep 19, 2018
Version 1.0

Was this article helpful?

No CommentsBe the first to comment