diff --git a/brian_polling_manager/configuration.py b/brian_polling_manager/configuration.py index 699633a04bf94e468488f8eeaea150b47a8a44e6..160b07dcbe8870351dc5a27b418ca4dc91a108d8 100644 --- a/brian_polling_manager/configuration.py +++ b/brian_polling_manager/configuration.py @@ -31,6 +31,10 @@ _DEFAULT_CONFIG = { 'config': '/var/lib/sensu/conf/get-interface-stats.config.json', 'command': '{script} --config {config} {args}', }, + 'snmp-interface-check': { + 'script': '/var/lib/sensu/bin/counter2influx-v6.sh', + 'measurement': 'counters', + }, 'gws-direct-interface-check': { 'script': '/var/lib/sensu/bin/poll-gws-direct.sh', 'measurement': 'gwsd_counters', @@ -81,6 +85,15 @@ CONFIG_SCHEMA = { 'required': ['script', 'config', 'command'], 'additionalProperties': False }, + 'snmp-interface-check': { + 'type': 'object', + 'properties': { + 'script': {'type': 'string'}, + 'measurement': {'type': 'string'}, + }, + 'required': ['script', 'measurement'], + 'additionalProperties': False + }, 'sensu': { 'type': 'object', 'properties': { @@ -92,6 +105,8 @@ CONFIG_SCHEMA = { 'api-key': {'type': 'string'}, 'interface-check': {'$ref': '#/definitions/router-check'}, + 'snmp-interface-check': + {'$ref': '#/definitions/snmp-interface-check'}, 'gws-direct-interface-check': {'$ref': '#/definitions/influx-check'}, 'dscp32-service-check': diff --git a/brian_polling_manager/interface_stats/cli.py b/brian_polling_manager/interface_stats/cli.py index 6b32276458f2f661a8737eaa9588c98754339e4f..44f09cf5178e17a0f3d75da726b6e2ca320a4400 100644 --- a/brian_polling_manager/interface_stats/cli.py +++ b/brian_polling_manager/interface_stats/cli.py @@ -329,8 +329,11 @@ def cli( logger.exception( f"Error while processing {str(vendor).capitalize()} router {router_fqdn}" ) + # Exit code 2 indicates CRITICAL in Sensu + raise click.exceptions.Exit(2) if error_counter.count: + # Exit code 1 indicates WARNING in Sensu raise click.ClickException( "Errors were encountered while processing interface stats" ) diff --git a/brian_polling_manager/interface_stats/vendors/common.py b/brian_polling_manager/interface_stats/vendors/common.py index be465560fb801bcb4b4525eff1ef62e1a96da446..6ee44e4f6aabf4bc87a5e7cf72a6334180638ba2 100644 --- a/brian_polling_manager/interface_stats/vendors/common.py +++ b/brian_polling_manager/interface_stats/vendors/common.py @@ -14,17 +14,17 @@ BRIAN_POINT_FIELDS_SCHEMA = { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { - "egressOctets": {"type": "integer"}, - "egressPackets": {"type": "integer"}, - "ingressOctets": {"type": "integer"}, - "ingressPackets": {"type": "integer"}, - "egressOctetsv6": {"type": "integer"}, - "egressPacketsv6": {"type": "integer"}, - "ingressOctetsv6": {"type": "integer"}, - "ingressPacketsv6": {"type": "integer"}, - "egressErrors": {"type": "integer"}, - "ingressDiscards": {"type": "integer"}, - "ingressErrors": {"type": "integer"}, + "egressOctets": {"type": "number"}, + "egressPackets": {"type": "number"}, + "ingressOctets": {"type": "number"}, + "ingressPackets": {"type": "number"}, + "egressOctetsv6": {"type": "number"}, + "egressPacketsv6": {"type": "number"}, + "ingressOctetsv6": {"type": "number"}, + "ingressPacketsv6": {"type": "number"}, + "egressErrors": {"type": "number"}, + "ingressDiscards": {"type": "number"}, + "ingressErrors": {"type": "number"}, }, "required": [ "egressOctets", diff --git a/brian_polling_manager/interface_stats/vendors/juniper.py b/brian_polling_manager/interface_stats/vendors/juniper.py index acd7a819c3d720e7c61d9969473811e7ebd40002..eaf55c8ede9eb52e72fba032841ec6b3fd80a26d 100644 --- a/brian_polling_manager/interface_stats/vendors/juniper.py +++ b/brian_polling_manager/interface_stats/vendors/juniper.py @@ -17,7 +17,9 @@ NCCLIENT_PARAMS = { } PHYSICAL_INTERFACE_COUNTERS = { - "__defaults__": {"transform": int, "required": False}, + # Counters must be floats to be compatible with influx writing to the same + # measurement. cf https://github.com/sensu/sensu-go/issues/2213 + "__defaults__": {"transform": float, "required": False}, "name": {"path": "./name", "transform": lambda v: str(v).strip()}, "brian": { "ingressOctets": {"path": "./traffic-statistics/input-bytes", "required": True}, @@ -59,6 +61,7 @@ PHYSICAL_INTERFACE_COUNTERS = { # "l2_output_unicast": {"path": "./ethernet-mac-statistics/output-unicasts"}, }, "errors": { + "__defaults__": {"transform": int}, "input_fifo_errors": {"path": "./input-error-list/input-fifo-errors"}, "input_discards": {"path": "./input-error-list/input-discards"}, "input_drops": {"path": "./input-error-list/input-drops"}, @@ -90,7 +93,9 @@ PHYSICAL_INTERFACE_COUNTERS = { LOGICAL_INTERFACE_COUNTERS = { - "__defaults__": {"transform": int, "required": False}, + # Counters must be floats to be compatible with influx writing to the same + # measurement. cf https://github.com/sensu/sensu-go/issues/2213 + "__defaults__": {"transform": float, "required": False}, "name": {"path": "./name", "transform": lambda v: str(v).strip()}, "brian": { "ingressOctets": { diff --git a/brian_polling_manager/interface_stats/vendors/nokia.py b/brian_polling_manager/interface_stats/vendors/nokia.py index 8025652a0bb5cdc8aae40b4eeb0d47f10f6d7242..9d54bbd93258c739768ca962fc72cc03c357abe7 100644 --- a/brian_polling_manager/interface_stats/vendors/nokia.py +++ b/brian_polling_manager/interface_stats/vendors/nokia.py @@ -19,7 +19,9 @@ NCCLIENT_PARAMS = { } INTERFACE_COUNTERS = { - "__defaults__": {"transform": int, "required": False}, + # Counters must be floats to be compatible with influx writing to the same + # measurement. cf https://github.com/sensu/sensu-go/issues/2213 + "__defaults__": {"transform": float, "required": False}, "name": { "path": ["./lag-name", "./port-id"], "transform": lambda v: str(v).strip(), @@ -37,6 +39,7 @@ INTERFACE_COUNTERS = { "ingressDiscards": {"path": "./statistics/in-discards"}, }, "errors": { + "__defaults__": {"transform": int}, "output_total_errors": {"path": "./statistics/out-errors"}, "input_total_errors": {"path": "./statistics/in-errors"}, "input_discards": {"path": "./statistics/in-discards"}, @@ -45,7 +48,9 @@ INTERFACE_COUNTERS = { } INTERFACE_COUNTERS_ALT = { - "__defaults__": {"transform": int, "required": False}, + # Counters must be floats to be compatible with influx writing to the same + # measurement. cf https://github.com/sensu/sensu-go/issues/2213 + "__defaults__": {"transform": float, "required": False}, "name": { "path": "./interface-name", "transform": lambda v: str(v).strip(), @@ -63,7 +68,10 @@ INTERFACE_COUNTERS_ALT = { "egressOctetsv6": {"path": "./ipv6/statistics/out-octets"}, "egressPacketsv6": {"path": "./ipv6/statistics/out-packets"}, }, - "errors": {"output_discards": {"path": "./statistics/ip/out-discard-packets"}}, + "errors": { + "__defaults__": {"transform": int}, + "output_discards": {"path": "./statistics/ip/out-discard-packets"}, + }, } diff --git a/brian_polling_manager/interfaces.py b/brian_polling_manager/interfaces.py index bdcfbbec2b030115e286a3eebf19bdccf7db7876..4aaf2a7e940e7fde0cebb5619a32ac3ff869b199 100644 --- a/brian_polling_manager/interfaces.py +++ b/brian_polling_manager/interfaces.py @@ -9,8 +9,6 @@ logger = logging.getLogger(__name__) def _is_rtr_check(check): name = check["metadata"]["name"] - # check-* is the old-style name (add to the returned - # data so it can be deleted) return re.match(r"^(rtr)-[^-]+\.geant\.net$", name) @@ -26,8 +24,7 @@ def load_checks(sensu_params, filter_by): return {c["metadata"]["name"]: c for c in checks} -# TODO [POL1-797] rm after POL1-773 runs in production -def load_deprecated_ifc_checks(sensu_params): +def load_ifc_checks(sensu_params): return load_checks(sensu_params, _is_ifc_check) @@ -35,6 +32,45 @@ def load_router_checks(sensu_params): return load_checks(sensu_params, _is_rtr_check) +class SNMPInterfaceCheck(sensu.AbstractCheck): + COMMAND_STR = "{script} {measurement} {community} {hostname} {interface} {if_index}" + + def __init__(self, ifc_check_params, interface): + super().__init__() + self.ifc_check_params = ifc_check_params + self.interface = interface + + @sensu.AbstractCheck.name.getter + def name(self): + # fix POL1-386 - replace : in interface name with . + # https://docs.sensu.io/sensu-go/latest/observability-pipeline/ + # observe-schedule/checks/#metadata-attributes + ifc_name = self.interface["name"].replace("/", "-").replace(":", ".") + return f'ifc-{self.interface["router"]}-{ifc_name}' + + @sensu.AbstractCheck.command.getter + def command(self): + return self.COMMAND_STR.format( + script=self.ifc_check_params["script"], + measurement=self.ifc_check_params["measurement"], + # TODO: add community string to /poller/interfaces response + # (cf. POL1-339) + community="0pBiFbD", + hostname=self.interface["router"], + interface=self.interface["name"], + if_index=self.interface["snmp-index"], + ) + + @sensu.AbstractCheck.proxy_entity_name.getter + def proxy_entity_name(self): + return self.interface["router"] + + @staticmethod + def requires_snmp_check(interface): + # only juniper ae subinterfaces for now + return re.match(r"^ae\d+\.\d+", interface["name"]) + + class NetconfRouterCheck(sensu.AbstractCheck): METRIC_FORMAT = "" METRIC_HANDLERS = None @@ -68,7 +104,7 @@ class NetconfRouterCheck(sensu.AbstractCheck): return self.router -def refresh(sensu_params, inventory_interfaces): +def netconf_router_refresh(sensu_params, inventory_interfaces): routers_w_vendor = { (ifc["router"], ifc.get("vendor")) for ifc in inventory_interfaces } @@ -78,12 +114,16 @@ def refresh(sensu_params, inventory_interfaces): for router, vendor in routers_w_vendor ] - r1 = sensu.refresh(sensu_params, [], load_deprecated_ifc_checks(sensu_params)) - r2 = sensu.refresh(sensu_params, required_checks, load_router_checks(sensu_params)) - return { - "checks": r1["checks"] + r2["checks"], - "input": r1["input"] + r2["input"], - "created": r1["created"] + r2["created"], - "updated": r1["updated"] + r2["updated"], - "deleted": r1["deleted"] + r2["deleted"], - } + return sensu.refresh( + sensu_params, required_checks, load_router_checks(sensu_params) + ) + + +def snmp_interfaces_refresh(sensu_params, inventory_interfaces): + required_checks = [ + SNMPInterfaceCheck(sensu_params["snmp-interface-check"], ifc) + for ifc in inventory_interfaces + if SNMPInterfaceCheck.requires_snmp_check(ifc) + ] + + return sensu.refresh(sensu_params, required_checks, load_ifc_checks(sensu_params)) diff --git a/brian_polling_manager/main.py b/brian_polling_manager/main.py index 1228b238a0b512b172b8cc631e8e646ca81e4797..6e7cd985f8778012c53a8db4b3875e32e5e46204 100644 --- a/brian_polling_manager/main.py +++ b/brian_polling_manager/main.py @@ -52,12 +52,13 @@ REFRESH_RESULT_SCHEMA = { }, 'type': 'object', 'properties': { - 'interfaces': {'$ref': '#/definitions/refresh-result'}, + 'netconf': {'$ref': '#/definitions/refresh-result'}, + 'snmp': {'$ref': '#/definitions/refresh-result'}, 'gws_direct': {'$ref': '#/definitions/refresh-result'}, 'gws_indirect': {'$ref': '#/definitions/refresh-result'}, 'eumetsat_multicast': {'$ref': '#/definitions/refresh-result'}, }, - 'required': ['interfaces'], + 'required': ['netconf'], 'additionalProperties': False } @@ -90,7 +91,8 @@ def refresh(config, force=False): = inventory.load_eumetsat_multicast_subscriptions( config['inventory']) result = { - 'interfaces': interfaces.refresh(config['sensu'], state.interfaces), + 'netconf': interfaces.netconf_router_refresh(config['sensu'], state.interfaces), + 'snmp': interfaces.snmp_interfaces_refresh(config['sensu'], state.interfaces), 'gws_direct': gws_direct.refresh(config['sensu'], state.gws_direct), 'gws_indirect': gws_indirect.refresh( config['sensu'], state.gws_indirect), diff --git a/setup.py b/setup.py index 7d889e797d086c33ec0957431e3db92a6b782759..3326323a7b2195de287ab00393b27b357535e7b3 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name='brian-polling-manager', - version="0.10", + version="0.11", author='GEANT', author_email='swd@geant.org', description='service for managing BRIAN polling checks', diff --git a/test/conftest.py b/test/conftest.py index e2bd49b1f5ce643b6aefb26d57925b9947fb3360..e8850df8b5ba6f2a828cd8936366de43bf20f717 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -44,6 +44,10 @@ def config(): "config": "/var/lib/sensu/conf/get-interface-stats.config.json", "command": "{script} --config {config} {args}", }, + "snmp-interface-check": { + "script": "/var/lib/sensu/bin/counter2influx-v6.sh", + "measurement": "counters", + }, "gws-direct-interface-check": { "script": "/var/lib/sensu/bin/poll-gws-direct.sh", "measurement": "gwsd_counters", diff --git a/test/data/interfaces.json b/test/data/interfaces.json index 04a1eeda7b4c115e6950e92ecac6d25f420c0a9e..86d8a747847f6e020b4f170b98aba6995b62ba24 100644 --- a/test/data/interfaces.json +++ b/test/data/interfaces.json @@ -55,5 +55,21 @@ "description": "blah blah nokia router", "circuits": [], "snmp-index": 9999 + }, + { + "router": "mx1.fra.de.geant.net", + "name": "ae10.1", + "bundle": [], + "bundle-parents": [], + "description": "blah blah", + "circuits": [ + { + "id": 50028, + "name": "something", + "type": "SERVICE", + "status": "operational" + } + ], + "snmp-index": 9999 } ] diff --git a/test/interface_stats/test_juniper.py b/test/interface_stats/test_juniper.py index 8f8096fca94a8739dee99888e3de7c1074073561..f6ecca551b53c6d7c18eeead34e1ad8774f158dc 100644 --- a/test/interface_stats/test_juniper.py +++ b/test/interface_stats/test_juniper.py @@ -200,7 +200,8 @@ def test_validate_interface_counters_and_influx_points_for_all_juniper_routers( for point in bpoints: jsonschema.validate(point, influx.INFLUX_POINT) jsonschema.validate(point["fields"], common.BRIAN_POINT_FIELDS_SCHEMA) - + for value in point['fields'].values(): + assert isinstance(value, float) epoints = list( common.error_points( juniper_router_fqdn, @@ -213,6 +214,8 @@ def test_validate_interface_counters_and_influx_points_for_all_juniper_routers( for point in epoints: jsonschema.validate(point, influx.INFLUX_POINT) jsonschema.validate(point["fields"], common.ERROR_POINT_FIELDS_SCHEMA) + for value in point['fields'].values(): + assert isinstance(value, int) class TestGetJuniperNetConf: diff --git a/test/test_sensu_checks.py b/test/test_sensu_checks.py index fd03063c802896b28b1c9310afffc16541619536..f331c4bbc74468615d958411640b48ab5ccf69d0 100644 --- a/test/test_sensu_checks.py +++ b/test/test_sensu_checks.py @@ -53,6 +53,30 @@ def test_router_check(config): assert check["output_metric_handlers"] == [] +def test_snmp_interface_check(config): + check = interfaces.SNMPInterfaceCheck( + config["sensu"]["snmp-interface-check"], + {"router": "bogus.router", "name": "bogus/1/1", "snmp-index": 123}, + ).to_dict() + assert check["command"] == ( + "/var/lib/sensu/bin/counter2influx-v6.sh counters 0pBiFbD bogus.router " + "bogus/1/1 123" + ) + assert check["proxy_entity_name"] == "bogus.router" + assert check["metadata"]["name"] == "ifc-bogus.router-bogus-1-1" + assert check["output_metric_format"] == "influxdb_line" + assert check["output_metric_handlers"] == ["influx-db-handler"] + + +@responses.activate +def test_snmp_interface_refresh(config, mocked_sensu, mocked_inventory): + routers = inventory.load_interfaces("http://inventory1") + result = interfaces.snmp_interfaces_refresh( + config["sensu"], inventory_interfaces=routers + ) + assert result == {"checks": 3, "input": 1, "created": 1, "updated": 0, "deleted": 3} + + def test_nokia_router_check(config): check = interfaces.NetconfRouterCheck( config["sensu"]["interface-check"], "xyz", vendor="nokia" @@ -68,22 +92,14 @@ def test_nokia_router_check(config): assert check["output_metric_handlers"] == [] -@responses.activate -def test_cleans_up_old_interface_checks(config, mocked_sensu, mocked_inventory): - routers = inventory.load_interfaces("http://inventory1") - result = interfaces.refresh(config["sensu"], inventory_interfaces=routers) - assert result == {"checks": 3, "input": 3, "created": 3, "updated": 0, "deleted": 3} - sensu.clear_cached_values() - result = interfaces.refresh(config["sensu"], inventory_interfaces=routers) - assert result == {"checks": 3, "input": 3, "created": 0, "updated": 0, "deleted": 0} - - @responses.activate def test_runs_idempotent(config, mocked_sensu, mocked_inventory): routers = inventory.load_interfaces("http://inventory1") - interfaces.refresh(config["sensu"], inventory_interfaces=routers) + interfaces.netconf_router_refresh(config["sensu"], inventory_interfaces=routers) sensu.clear_cached_values() - result = interfaces.refresh(config["sensu"], inventory_interfaces=routers) + result = interfaces.netconf_router_refresh( + config["sensu"], inventory_interfaces=routers + ) assert result == {"checks": 3, "input": 3, "created": 0, "updated": 0, "deleted": 0}