diff --git a/brian_polling_manager/cli.py b/brian_polling_manager/cli.py index 0865ea7d4bc57cc6ad6ad479ce53c64040790d0e..e9015a357db3a3362763571069fef1850697b281 100644 --- a/brian_polling_manager/cli.py +++ b/brian_polling_manager/cli.py @@ -1,12 +1,11 @@ import json import logging import os -import re import click import jsonschema -from brian_polling_manager import sensu, inventory +from brian_polling_manager import inventory, interfaces logger = logging.getLogger(__name__) @@ -21,7 +20,17 @@ _DEFAULT_CONFIG = { 'https://test-poller-sensu-agent02.geant.org:8080', 'https://test-poller-sensu-agent03.geant.org:8080' ], - 'token': '696a815c-607e-4090-81de-58988c83033e' + 'token': '696a815c-607e-4090-81de-58988c83033e', + 'interface-check': { + 'script': '/var/lib/sensu/bin/counter2influx.sh', + 'measurement': 'counters', + 'interval': 300, + 'subscriptions': ['interfacecounters'], + 'output_metric_handlers': ['influx-db-handler'], + 'namespace': 'default', + 'round_robin': True, + 'command': '{script} {measurement} {community} {hostname} {interface} {ifIndex}', + } }, 'statedir': '/tmp/' } @@ -29,6 +38,29 @@ _DEFAULT_CONFIG = { CONFIG_SCHEMA = { '$schema': 'http://json-schema.org/draft-07/schema#', 'definitions': { + 'influx-check': { + 'type': 'object', + 'properties': { + 'script': {'type': 'string'}, + 'measurement': {'type': 'string'}, + 'interval': {'type': 'integer'}, + 'subscriptions': { + 'type': 'array', + 'items': {'type': 'string'} + }, + 'output_metric_handlers': { + 'type': 'array', + 'items': {'type': 'string'} + }, + 'namespace': {'type': 'string'}, + 'round_robin': {'type': 'boolean'}, + 'command': {'type': 'string'}, + }, + 'required': ['script', 'measurement', 'interval', + 'subscriptions', 'output_metric_handlers', + 'namespace', 'round_robin', 'command'], + 'additionalProperties': False + }, 'sensu': { 'type': 'object', 'properties': { @@ -37,8 +69,11 @@ CONFIG_SCHEMA = { 'items': {'type': 'string'}, 'minItems': 1 }, - 'token': {'type': 'string'} - } + 'token': {'type': 'string'}, + 'interface-check': {'$ref': '#/definitions/influx-check'} + }, + 'required': ['api-base', 'token', 'interface-check'], + 'additionalProperties': False } }, 'type': 'object', @@ -117,7 +152,6 @@ class State(object): f.write(json.dumps(new_interfaces)) - def _validate_config(ctx, param, value): """ loads, validates and returns configuration parameters @@ -144,12 +178,6 @@ def _validate_config(ctx, param, value): return config -def load_ifc_checks(sensu_params): - def _is_ifc_check(check): - name = check['metadata']['name'] - return re.match(r'^check-([^-]+\.geant\.net)-(.+)$', name) - ifc_checks = filter(_is_ifc_check, sensu.load_all_checks(sensu_params)) - return {c['metadata']['name']:c for c in ifc_checks} @click.command() @@ -164,21 +192,15 @@ def load_ifc_checks(sensu_params): default=False, help="update even if inventory hasn't been updated") def main(config, force): + state = State(config['statedir']) last = inventory.last_update_timestamp(config['inventory']) if force or last != state.last: - interfaces = inventory.load_interfaces(config['inventory']) state.last = last - state.interfaces = interfaces - - assert interfaces - - - ifc_checks = load_ifc_checks(config['sensu']) - print(ifc_checks.keys()) + state.interfaces = inventory.load_interfaces(config['inventory']) + interfaces.refresh(config['sensu'], state) - print('here') if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG) diff --git a/brian_polling_manager/interfaces.py b/brian_polling_manager/interfaces.py new file mode 100644 index 0000000000000000000000000000000000000000..f50b975cafd3a0fa87b8926505682d96c73bd1e7 --- /dev/null +++ b/brian_polling_manager/interfaces.py @@ -0,0 +1,85 @@ +import logging +import re + +from brian_polling_manager import sensu + +logger = logging.getLogger(__name__) + +def load_ifc_checks(sensu_params): + def _is_ifc_check(check): + name = check['metadata']['name'] + return re.match(r'^check-([^-]+\.geant\.net)-(.+)$', name) + ifc_checks = filter(_is_ifc_check, sensu.load_all_checks(sensu_params)) + return {c['metadata']['name']:c for c in ifc_checks} + + +def _check_name(interface): + ifc_name = interface['name'].replace('/', '-') + return f'check-{interface["router"]}-{ifc_name}' + + +def _make_check(check_params, interface): + command = check_params['command'].format( + script=check_params['script'], + measurement=check_params['measurement'], + community='0pBiFbD', # TODO: add this to /poller/interfaces response + hostname=interface['router'], + interface=interface['name'], + ifIndex=interface['snmp-index'] + ) + + return { + 'command': command, + 'interval': check_params['interval'], + 'subscriptions': sorted(check_params['subscriptions']), + 'proxy_entity_name': interface['router'], + 'round_robin': check_params['round_robin'], + 'output_metric_format': 'influxdb_line', + 'output_metric_handlers': sorted(check_params['output_metric_handlers']), + 'metadata': { + 'name': _check_name(interface), + 'namespace': check_params['namespace'] + } + } + + +def _checks_match(a, b) -> bool: + # if a['command'] != b['command']: + # return False + if a['interval'] != b['interval']: + return False + if a['proxy_entity_name'] != b['proxy_entity_name']: + return False + if a['round_robin'] != b['round_robin']: + return False + if a['output_metric_format'] != b['output_metric_format']: + return False + if sorted(a['subscriptions']) != sorted(b['subscriptions']): + return False + if sorted(a['output_metric_handlers']) != sorted(b['output_metric_handlers']): + return False + if a['metadata']['name'] != b['metadata']['name']: + return False + if a['metadata']['namespace'] != b['metadata']['namespace']: + return False + return True + + +def refresh(sensu_params, state): + + ifc_checks = load_ifc_checks(sensu_params) + + for interface in state.interfaces: + + expected_check = _make_check(sensu_params['interface-check'], interface) + + expected_name = _check_name(interface) + if expected_name not in ifc_checks: + sensu.create_check(sensu_params, expected_check) + elif not _checks_match(ifc_checks[expected_name], expected_check): + sensu.update_check(sensu_params, expected_check) + + wanted_checks = {_check_name(ifc) for ifc in state.interfaces} + extra_checks = set(ifc_checks.keys()) - wanted_checks + for name in extra_checks: + sensu.delete_check(sensu_params, name) diff --git a/brian_polling_manager/sensu.py b/brian_polling_manager/sensu.py index 55293f42c44e2da23822d5039d3e498b08091b4d..246e68996b96f6f8d19f3fa53e9bdc5d9448853d 100644 --- a/brian_polling_manager/sensu.py +++ b/brian_polling_manager/sensu.py @@ -1,15 +1,19 @@ """ Sensu api utilities """ +import logging import random import requests -def load_all_checks(params): +logger = logging.getLogger(__name__) + + +def load_all_checks(params, namespace='default'): url = random.choice(params['api-base']) # url = params['api-base'][0] r = requests.get( - f'{url}/api/core/v2/namespaces/default/checks', + f'{url}/api/core/v2/namespaces/{namespace}/checks', headers={ 'Authorization': f'Key {params["token"]}', 'Accepts': 'application/json', @@ -20,22 +24,41 @@ def load_all_checks(params): yield check +def create_check(params, check, namespace='default'): + logger.error(f'creating missing check: {check["metadata"]["name"]}') + # url = random.choice(params['api-base']) + # r = requests.post( + # f'{url}/api/core/v2/namespaces/{namespace}/checks', + # headers={ + # 'Authorization': f'Key {params["token"]}', + # 'Content-Type': 'application/json', + # }, + # json=check) + # r.raise_for_status() + + +def update_check(params, check, namespace='default'): + name = check["metadata"]["name"] + logger.error(f'updating existing check: {name}') + # url = random.choice(params['api-base']) + # r = requests.post( + # f'{url}/api/core/v2/namespaces/{namespace}/checks/{name}', + # headers={ + # 'Authorization': f'Key {params["token"]}', + # 'Content-Type': 'application/json', + # }, + # json=check) + # r.raise_for_status() + -# -# def snmp_counter_checks(all_checks): -# for c in all_checks: -# -# assert c['command'] -# items = c['command'].split() -# if len(items) < 6: -# continue -# if not items.pop(0).endswith('counter2influx.sh'): -# continue -# -# yield { -# 'hostname': items[3], -# 'interface_name': items[4], -# 'interval': c['interval'], -# 'name': c['metadata']['name'] -# } -# +def delete_check(params, check, namespace='default'): + if isinstance(check, str): + name = check + else: + name = check["metadata"]["name"] + logger.error(f'deleting unwanted check: {name}') + # url = random.choice(params['api-base']) + # r = requests.delete( + # f'{url}/api/core/v2/namespaces/{namespace}/checks', + # headers={'Authorization': f'Key {params["token"]}'}) + # r.raise_for_status()