diff --git a/README.md b/README.md index 6f8835bba4fdec5992f8a7a36e306bc89379c2dc..e5f9ebc4f9e281f9de67327a48fa1983c95a839e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[PACKAGE]: ../../raw/master/mkp/check_radius-0.0.1-20240421.mkp "check_radius-0.0.1-20240421.mkp" +[PACKAGE]: ../../raw/master/mkp/check_radius-0.1.0-20240428.mkp "check_radius-0.1.0-20240428.mkp" # Check RADIUS This is a (very) basic active RADIUS check. Tests if a RADIUS server is responsive (accept/reject/timeout). There is (limited) support to add AV-pairs to the RADIUS request. diff --git a/mkp/check_radius-0.1.0-20240428.mkp b/mkp/check_radius-0.1.0-20240428.mkp new file mode 100644 index 0000000000000000000000000000000000000000..c895811b07fc9b346597ffd6feffbc1f8170408e Binary files /dev/null and b/mkp/check_radius-0.1.0-20240428.mkp differ diff --git a/source/checks/check_radius b/source/checks/check_radius index eaa316ccf62ee5d8b362af8f23c6e1108a317835..352190350c977cc390bdc4c6f3459ba87b02d9f6 100644 --- a/source/checks/check_radius +++ b/source/checks/check_radius @@ -14,32 +14,53 @@ def check_radius_arguments(params): args = [] - if server := params.get('server'): + if (server := params.get('server')) is not None: args.extend(['-H', server]) else: args.append('-H $HOSTADDRESS$') - if auth_port := params.get("auth_port"): - args.extend(['--authport', auth_port]) + if (auth_port := params.get("auth_port")) is not None: + args.extend(['--auth-port', auth_port]) - if secret := params.get("secret"): + if (secret := params.get("secret")) is not None: args.extend(["--secret", passwordstore_get_cmdline("%s", secret)]) - if user_name := params.get("user_name"): + if (user_name := params.get("user_name")) is not None: args.extend([f'--username', user_name]) - if user_password := params.get("user_password"): + if (user_password := params.get("user_password")) is not None: args.extend(["--password", passwordstore_get_cmdline("%s", user_password)]) - if timeout := params.get('timeout'): - args.extend(['-timeout', timeout]) + if (timeout := params.get('timeout')) is not None: + args.extend(['--timeout', timeout]) + + if (request_attributes := params.get('request_params').get('request_attributes')) is not None: + for av_name, av_value in request_attributes: + args.extend(['--request-attribute', f'{av_name}:{av_value}']) + + if (expected_response := params.get('response_params').get('expected_response')) is not None: + args.extend(['--expected-response', expected_response]) + + if (state_not_expected_response := params.get('response_params').get('state_not_expected_response')) is not None: + args.extend(['--state-not-expected-response', state_not_expected_response]) + + if (num_resp_attributes := params.get('response_params').get('num_resp_attributes')) is not None: + args.extend(['--num-resp-attributes', num_resp_attributes]) + + if (state_wrong_number_of_response_attributes := params.get('response_params').get( + 'state_wrong_number_of_response_attributes')) is not None: + args.extend(['--state-wrong-num-resp-attributes', state_wrong_number_of_response_attributes]) + + if (level_upper_response_time := params.get('response_params').get('level_upper_response_time')) is not None: + warn, crit = level_upper_response_time + args.extend(['--max-response-time', f'{warn}, {crit}']) return args def _check_description(params): if 'description' in params: - return f'RADIUS server {params["description"]}' + return f'RADIUS {params["description"]}' return 'RADIUS server' diff --git a/source/gui/metrics/check_radius.py b/source/gui/metrics/check_radius.py index 5c8cff2d37eb286eab8a4daaa3e67c47783a8052..67790d936ba06f7cb8f4428512b353c4ffd0da2b 100644 --- a/source/gui/metrics/check_radius.py +++ b/source/gui/metrics/check_radius.py @@ -18,8 +18,8 @@ from cmk.gui.plugins.metrics.utils import ( perfometer_info ) -metric_info['radius_request_time'] = { - 'title': _('Request time'), +metric_info['radius_response_time'] = { + 'title': _('Response time'), 'unit': 's', 'color': '#9a52bf', } @@ -27,17 +27,17 @@ metric_info['radius_request_time'] = { graph_info['check_radius_time'] = { 'title': _('RADIUS request time'), 'metrics': [ - ('radius_request_time', 'area'), + ('radius_response_time', 'area'), ], 'scalars': [ - ('radius_request_time:crit', _('Crit')), - ('radius_request_time:warn', _('Warn')), + ('radius_response_time:crit', _('Crit')), + ('radius_response_time:warn', _('Warn')), ], } perfometer_info.append({ 'type': 'logarithmic', - 'metric': 'radius_request_time', + 'metric': 'radius_response_time', 'half_value': 1.0, 'exponent': 10.0, }) diff --git a/source/gui/wato/check_parameters/check_radius.py b/source/gui/wato/check_parameters/check_radius.py index d38398d35ae7d0a57649ebea1663a574716f9ca7..71dd1d8c8eb0d373222188b480412f94e70da35e 100644 --- a/source/gui/wato/check_parameters/check_radius.py +++ b/source/gui/wato/check_parameters/check_radius.py @@ -12,15 +12,153 @@ # 2024-01-01: modified for cmk 2.2.x from cmk.gui.i18n import _ +from cmk.gui.plugins.wato.active_checks.common import RulespecGroupActiveChecks +from cmk.gui.plugins.wato.utils import HostRulespec, IndividualOrStoredPassword, rulespec_registry from cmk.gui.valuespec import ( + Alternative, Dictionary, + DropdownChoice, + FixedValue, + Foldable, + IPv4Address, Integer, - TextAscii, + ListOf, + MonitoringState, + TextInput, Transform, + Tuple, ) -from cmk.gui.plugins.wato.active_checks.common import RulespecGroupActiveChecks -from cmk.gui.plugins.wato.utils import HostRulespec, rulespec_registry, IndividualOrStoredPassword +_called_station_id = Tuple( + title='Called-Station-Id', + orientation='horizontal', + elements=[ + FixedValue('Called-Station-Id'), # , totext='' # add empty totext to remove "duplicate" attribute name + TextInput( + size=20, + placeholder='AA-BB-CC-DD-EE-FF', + allow_empty=False, + ), + ], +) +_calling_station_id = Tuple( + title='Calling-Station-Id', + orientation='horizontal', + elements=[ + FixedValue('Calling-Station-Id'), + TextInput( + size=20, + placeholder='AA-BB-CC-DD-EE-FF', + allow_empty=False, + ), + ], +) +_framed_mtu = Tuple( + title='Framed-MTU', + orientation='horizontal', + elements=[ + FixedValue('Framed-MTU'), + Integer( + size=5, + default_value=1500 + ), + ], +) +_nas_identifier = Tuple( + title='NAS-Identifier', + orientation='horizontal', + elements=[ + FixedValue('NAS-Identifier'), + TextInput( + size=20, + placeholder='NAS001', + allow_empty=False, + ), + ], +) +_nas_ip_address = Tuple( + title='NAS-IP-Address', + orientation='horizontal', + elements=[ + FixedValue('NAS-IP-Address'), + IPv4Address(), + ], +) +_nas_port_id = Tuple( + title='NAS-Port-Id', + orientation='horizontal', + elements=[ + FixedValue('NAS-Port-Id'), + TextInput( + size=20, + placeholder='GigabitEthernet0/8', + allow_empty=False, + ), + ], +) +_nas_port_type = Tuple( + title='NAS-Port-Type', + orientation='horizontal', + elements=[ + FixedValue('NAS-Port-Type'), + DropdownChoice( + choices=[ + ('0', 'Async'), + ('1', 'Sync'), + ('2', 'ISDN'), + ('3', 'ISDN-V120'), + ('4', 'ISDN-V110'), + ('5', 'Virtual'), + ('6', 'PIAFS'), + ('7', 'HDLC-Clear-Channel'), + ('8', 'X.25'), + ('9', 'X.75'), + ('10', 'G.3-Fax'), + ('11', 'SDSL'), + ('12', 'ADSL-CAP'), + ('13', 'ADSL-DMT'), + ('14', 'IDSL'), + ('15', 'Ethernet'), + ('16', 'xDSL'), + ('17', 'Cable'), + ('18', 'Wireless-Other'), + ('19', 'Wireless-802.11'), + ] + ), + ], +) +_nas_port = Tuple( + title='NAS-Port', + orientation='horizontal', + elements=[ + FixedValue('NAS-Port'), + Integer( + size=7, + ), + ], +) +_service_type = Tuple( + title='Service-Type', + orientation='horizontal', + elements=[ + FixedValue('Service-Type'), + DropdownChoice( + choices=[ + ('1', 'Login-User'), + ('2', 'Framed-User'), + ('3', 'Callback-Login-User'), + ('4', 'Callback-Framed-User'), + ('5', 'Outbound-User'), + ('6', 'Administrative-User'), + ('7', 'NAS-Prompt-User'), + ('8', 'Authenticate-Only'), + ('9', 'Callback-NAS-Prompt'), + ('10', 'Call-Check'), + ('11', 'Callback-Administrative'), + ] + ), + ], +) def _valuespec_active_checks_radius(): @@ -30,49 +168,51 @@ def _valuespec_active_checks_radius(): help=_(''), elements=[ ('description', - TextAscii( + TextInput( title=_('Service description'), help=_( - 'Must be unique for every host. The service description starts always with \"RADIUS server\".'), + 'Must be unique for every host. The service description starts always with \"RADIUS server\".' + ), size=50, placeholder='Item name for the service', allow_empty=False, )), ('server', - TextAscii( + TextInput( title=_('Server IP-address or name'), help=_( 'Hostname or IP-address to monitor. Default is the host name/IP-Address of the monitored host.' ), size=50, allow_empty=False, + placeholder='i.e 192.168.10.10 or srvrad01.company.intern' )), ('auth_port', Integer( - title=_('RADIUS authentication port'), + title=_('Authentication port'), help=_('The RADIUS port to use for authentication. Default is 1812.'), - # size=5, + size=5, default_value=1812, minvalue=1, maxvalue=65535, )), ('secret', IndividualOrStoredPassword( - title=_('Server secret'), - help=_('The RADIUS secret.'), - # size=50, + title=_('Shared secret'), + help=_('The shared secret.'), allow_empty=False, )), ('timeout', Integer( - title=_('Server timeout'), - help=_('The user password.'), + title=_('Request timeout'), + help=_('The timeout for the RADIUS request.'), default_value=2, minvalue=1, maxvalue=30, + unit='s', )), ('user_name', - TextAscii( + TextInput( title=_('User name'), help=_('The user name to use in the request.'), size=50, @@ -83,11 +223,85 @@ def _valuespec_active_checks_radius(): IndividualOrStoredPassword( title=_('User password'), help=_('The user password.'), - # size=50, allow_empty=False )), + ('request_params', + Foldable( + Dictionary( + title="Hide/Show Request Parameters", + elements=[ + ('request_attributes', + ListOf( + Alternative( + orientation='horizontal', + elements=[ + _called_station_id, + _calling_station_id, + _framed_mtu, + _nas_identifier, + _nas_ip_address, + _nas_port, + _nas_port_id, + _nas_port_type, + _service_type, + ], + ), + title=_('Request Attributes'), + add_label=_('add attribute'), + )), + ], + ), + title='Request paranmeters', + )), + ('response_params', + Foldable( + Dictionary( + title="Hide/Show Response Parameters", + elements=[ + ('expected_response', + DropdownChoice( + choices=( + (2, 'Accepted'), + (3, 'Rejected'), + ), + title=_('Expected response'), + help=_('Expected response from the RADIUS server.'), + )), + ('state_not_expected_response', + MonitoringState( + title=_('Monitoring state not expected response'), + default_value=2, + )), + ('level_upper_response_time', + Tuple( + elements=[ + Integer(title=_('Warning at'), unit='ms', minvalue=0, maxvalue=10000), + Integer(title=_('Critical at'), unit='ms', minvalue=0, maxvalue=10000), + ], + title=_('Max. response time'), + )), + ('num_resp_attributes', + Integer( + title=_('# of expected attribues in response'), + help=_('The expected number of RADIUS attibutes in the response.'), + minvalue=0, + maxvalue=65535, + )), + ('state_wrong_number_of_response_attributes', + MonitoringState( + title=_('Monitoring state on wrong # of response attributes'), + default_value=1, + )), + ], + ), + title='Response paranmeters', + )) ], - required_keys=['secret'] + required_keys=[ + 'secret', + 'request_params', + 'response_params', + ] ), ) diff --git a/source/lib/nagios/plugins/check_radius b/source/lib/nagios/plugins/check_radius index 4680980d163fb9c979abae40296bfc20a073f5d7..2147c1fb272c5e898485f36d218cd8947869ed58 100755 --- a/source/lib/nagios/plugins/check_radius +++ b/source/lib/nagios/plugins/check_radius @@ -13,48 +13,146 @@ # # https://github.com/pyradius/pyrad # -import socket -from typing import Sequence -import sys -from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter, Namespace, ArgumentTypeError -from time import time_ns +from argparse import ( + ArgumentDefaultsHelpFormatter, + ArgumentParser, + ArgumentTypeError, + Namespace, +) from os import environ +from socket import error as socket_error +from sys import ( + argv as sys_argv, + exit as sys_exit, + stdout as sys_stdout, +) +from time import time_ns +from typing import Sequence, Tuple import cmk.utils.password_store no_radiuslib = False try: - from pyrad.client import Client as rad_client - from pyrad.dictionary import Dictionary as rad_dictionary - import pyrad.packet + from pyrad.client import Client as radClient + from pyrad.dictionary import Dictionary as radDictionary + from pyrad.packet import AccessAccept, AccessReject, AccessRequest + from pyrad.client import Timeout as pyTimeout except ModuleNotFoundError: no_radiuslib = True -def parse_arguments(argv: Sequence[str]) -> Namespace: +class Args(Namespace): + host: str + auth_port: int + secret: str + timeout: int + username: str + password: str + num_resp_attributes: int + state_wrong_num_resp_attributes: int + request_attribute: Tuple[str, str] + max_response_time: Tuple[int, int] + expected_response: int + state_not_expected_response: int + + +VERSION = '0.1.0-20240428' + +cmk_state = { + 0: '', + 1: '(!)', + 2: '(!!)', + 3: '(?)', +} +response_str = { + 2: 'accept', + 3: 'reject', +} + + +def parse_arguments(argv: Sequence[str]) -> Args: + def _av_pair(s): + try: + name, value = s.split(':') + return name, value + except ValueError: + raise ArgumentTypeError("AV-Pairs must be in the form of name:vale") + + def _levels(s) -> Tuple[int, int]: + try: + warn, crit = s.split(',') + warn = int(warn) + crit = int(crit) + return warn, crit + except ValueError: + raise ArgumentTypeError("Levels must be in the form 'warn,crit' value") + parser = ArgumentParser( + description='This is a (very) basic active RADIUS check for Check_mk. Tests if a RADIUS server is responsive ' + '(accept/reject/timeout). There is (limited) support to add AV-pairs to the RADIUS request.', formatter_class=ArgumentDefaultsHelpFormatter, - epilog='' + epilog=f'(c) thl-cmk[at]outlook[dot], Version: {VERSION}, For more information see: https://thl-cmk.hopto.org' ) + # + # required request parameters + # parser.add_argument( '-H', '--host', required=True, - help='Host/IP-Address of RADIUS server to query (required)') + help='Host/IP-Address of RADIUS server to query (required)', + ) parser.add_argument( '--secret', required=True, - help='secret RADIUS key') + help='secret RADIUS key', + ) parser.add_argument( - '--username', default='dummyuser', - help='user name to test with') + '--username', default='dummyuser', required=True, + help='user name to test with', + ) parser.add_argument( - '--password', default='dummyuser', - help='user password to test with') + '--password', default='dummypassword', required=True, + help='user password to test with', + ) + # + # optional request parameters + # parser.add_argument( - '--authport', type=int, default=1812, - help='RADIUS authentication port to use.') + '--auth_port', type=int, default=1812, + help='RADIUS authentication port to use.', + ) parser.add_argument( '--timeout', type=int, default=1, - help='RADIUS server timeout.') + help='RADIUS server timeout', + ) + parser.add_argument( + '--request-attribute', nargs='*', type=_av_pair, action='append', default=[], + help='add request attribute in the form of "attribute-name:attribute-value" ' + 'ie: "Called-Station-Id:AA-BB-CC-DD-EE-FF". Repeat to add more attributes. ' + 'For valid attributes the dictionary file.', + ) + # + # response parameters + # + parser.add_argument( + '--expected-response', type=int, choices=[2, 3], + help=' 2 -> Accepted, 3 -> Rejected', + ) + parser.add_argument( + '--state-not-expected-response', type=int, choices=[0, 1, 2, 3], default=1, + help='Monitoring state: 0 -> OK, 1 -> WARN, 2 -> CRIT, 3 -> UNKNOWN', + ) + parser.add_argument( + '--num-resp-attributes', type=int, + help='Expected number of response attributes', + ) + parser.add_argument( + '--state-wrong-num-resp-attributes', type=int, choices=[0, 1, 2, 3], default=1, + help='Monitoring state: 0 -> OK, 1 -> WARN, 2 -> CRIT, 3 -> UNKNOWN', + ) + parser.add_argument( + '--max-response-time', type=_levels, + help='Upper levels for response time in ms in the format WARN,CRIT time. ie: 10,50' + ) args = parser.parse_args(argv) args.host = args.host.strip(' ') @@ -63,80 +161,130 @@ def parse_arguments(argv: Sequence[str]) -> Namespace: def main(args=None): if args is None: - args = sys.argv[1:] # without the path/plugin itself + args = sys_argv[1:] # without the path/plugin itself args = parse_arguments(args) if no_radiuslib: - sys.stdout.write( - 'To use this check plugin you need to install the python pyrad lib in your CMK python environment.\n' + sys_stdout.write( + 'To use this check plugin you need to install the python pyrad lib in your CMK python environment.(!!!)\n' ) - sys.exit(3) + sys_exit(3) omd_root = environ["OMD_ROOT"] - info_text = '' - long_output = '' - perf_data = '' + info_text = [] + long_output = [] + perf_data = [] status = 0 - rad_server = rad_client( + rad_server = radClient( server=args.host, - authport=args.authport, + authport=args.auth_port, secret=args.secret.encode('utf-8'), - dict=rad_dictionary(f'{omd_root}/local/lib/nagios/plugins/dictionary'), + # freeradius dictionaries are under /usr/share/freeradius/ + dict=radDictionary(f'{omd_root}/local/lib/nagios/plugins/dictionary'), timeout=args.timeout, ) rad_req = rad_server.CreateAuthPacket( - code=pyrad.packet.AccessRequest, + code=AccessRequest, User_Name=args.username, NAS_Identifier=args.host, ) rad_req["User-Password"] = rad_req.PwCrypt(args.password) + + # add optional request attributes + for av_pair in args.request_attribute: + name, value = av_pair[0] + try: + rad_req[name] = value + except TypeError: + sys_stdout.write( + f'WARNING: attribute value must be the real value not the name of the ' + f'value: {value}{cmk_state[1]}{cmk_state[1]}' + ) + status = max(status, 1) + continue + before_request_time = time_ns() try: response = rad_server.SendPacket(rad_req) - except pyrad.client.Timeout as e: - status = 2 - info_text = 'Radius request timeout' - long_output += f'\nRadius request timeout.\n{e}' - except socket.error as e: - status = 2 - info_text = 'Network error' - long_output += f'\nNetwork error\n{e}' + except pyTimeout as e: + status = max(status, 2) + message = f'Radius request timeout{cmk_state[2]}' + info_text.append(message) + long_output.append(f'{message}\n{e}') + except socket_error as e: + status = max(status, 2) + message = f'Network error{cmk_state[2]}' + info_text.append(message) + long_output.append(f'{message}\n{e}') else: - request_time = (time_ns() - before_request_time) / 1000 / 1000 / 1000 # -> ns to seconds - match response.code: - case pyrad.packet.AccessAccept: - info_text += 'Response: access accept' - long_output += '\nResponse: access accept' - long_output += f'\nResponse code: {response.code}' - if response.has_key: - long_output += f'\nNumber of attributes in response: {len(response.keys())}' - long_output += f'\n\nResponse attributes:' - for key in response.keys(): - long_output += f'\n{key}: {response.get(key)}' + # first: calculate response time + response_time = (time_ns() - before_request_time) / 1000 / 1000 / 1000 # -> ns to seconds + + # + # second: check response code + message = f'Response: access {response_str.get(response.code, f"unknown ({response.code})")}' + if args.expected_response and response.code != args.expected_response: + message += f' (expected: {response_str[args.expected_response]}{cmk_state[args.state_not_expected_response]})' + status = max(status, args.state_not_expected_response) + info_text.append(message) + long_output.append(message) + + # third: check response time + message = f'Response time {response_time * 1000:.0f}ms' + if args.max_response_time: + warn, crit = args.max_response_time + warn = warn + crit = crit + if response_time >= warn / 1000: + message += f' (WARN/CRIT at {warn}/{crit}' + if response_time >= crit / 1000: + message += cmk_state[2] + status = max(status, 2) else: - long_output += f'\nNo attributes in response: {len(response.keys())}' - case pyrad.packet.AccessReject: - info_text += 'Response: access reject' - long_output += '\nResponse: access reject' - long_output += f'\nResponse code: {response.code}' - case _: - info_text += f'Response: code unknown' - long_output += f'\nResponse: code unknown' - long_output += f'\nResponse code: {response.code}' - status = 3 - - perf_data += f'radius_request_time={request_time}' - - info_text = info_text.strip(',').strip(' ') - sys.stdout.write(f'{info_text}\n{long_output} | {perf_data}\n') + message += cmk_state[1] + status = max(status, 1) + perf_data.append(f'radius_response_time={response_time};{warn};{crit};;') + else: + perf_data.append(f'radius_response_time={response_time}') + info_text.append(message) + long_output.append(message) + + if response.code == AccessAccept: + # + # forth: check return attributes + if response.has_key: + message = f'Number of attributes in response: {len(response.keys())}' + else: + message = long_output.append('No return attributes in response') + if args.num_resp_attributes and len(response.keys()) != args.num_resp_attributes: + message += f' (expected {args.num_resp_attributes}{cmk_state[args.state_wrong_num_resp_attributes]})' + status = max(status, args.state_wrong_num_resp_attributes) + info_text.append(message) + + long_output.append(message) + + if response.has_key: + long_output.append('\nResponse attributes:') + for key in response.keys(): + long_output.append(f'{key}: {response.get(key)}') + + # + # format output data + info_text = ', '.join(info_text) + long_output = '\n'.join(long_output) + perf_data = '|'.join(perf_data) + if perf_data: + sys_stdout.write(f'{info_text}\n{long_output}|{perf_data}\n') + else: + sys_stdout.write(f'{info_text}\n{long_output}') return status if __name__ == '__main__': cmk.utils.password_store.replace_passwords() exitcode = main() - sys.exit(exitcode) + sys_exit(exitcode) diff --git a/source/lib/nagios/plugins/dictionary b/source/lib/nagios/plugins/dictionary old mode 100644 new mode 100755 index 30fb5281c4db8ff587c5f44350b61b171baae5c7..125de2fc6581f18a66137b2e1ebddc3d3a05c292 --- a/source/lib/nagios/plugins/dictionary +++ b/source/lib/nagios/plugins/dictionary @@ -230,7 +230,7 @@ ATTRIBUTE Autz-Type 1011 integer # User Types VALUE Service-Type Login-User 1 -VALUE Service-Type Framed-User 2 +VALUE Service-Type pdate 2 VALUE Service-Type Callback-Login-User 3 VALUE Service-Type Callback-Framed-User 4 VALUE Service-Type Outbound-User 5 diff --git a/source/lib/nagios/plugins/dictionary.freeradius b/source/lib/nagios/plugins/dictionary.freeradius old mode 100644 new mode 100755 diff --git a/source/packages/check_radius b/source/packages/check_radius index b14b394a21c71e70af36010c262794dcad5dbe2c..bff1f3c988c75b7826db24dbb66a2de962974128 100644 --- a/source/packages/check_radius +++ b/source/packages/check_radius @@ -1,5 +1,12 @@ {'author': 'Th.L. (thl-cmk[at]outlook[dot]com)', - 'description': 'active RADIUS check\n', + 'description': 'This is a (very) basic active RADIUS check:\n' + '\n' + ' - tests if a RADIUS server is responsive ' + '(accept/reject/timeout).\n' + ' - (limited) support to add AV-pairs to the RADIUS request\n' + ' - checks response code, response time and number of response ' + 'attributes\n' + '\n', 'download_url': 'https://thl-cmk.hopto.org', 'files': {'checks': ['check_radius'], 'gui': ['metrics/check_radius.py', @@ -9,7 +16,7 @@ 'nagios/plugins/dictionary.freeradius']}, 'name': 'check_radius', 'title': 'Check RADIUS', - 'version': '0.0.1-20240421', + 'version': '0.1.0-20240428', 'version.min_required': '2.2.0b1', 'version.packaged': '2.2.0p24', 'version.usable_until': None}