diff --git a/docs/source/protocol/poller.rst b/docs/source/protocol/poller.rst index 64628fc062a190b28a0c4c27a55ac6db2b87b760..7b9040ad8e720f1e4f431652df76642a25520a65 100644 --- a/docs/source/protocol/poller.rst +++ b/docs/source/protocol/poller.rst @@ -18,3 +18,9 @@ These endpoints are intended for use by BRIAN. --------------------------------- .. autofunction:: inventory_provider.routes.poller.interface_speeds + + +/poller/eumetsat-multicast +--------------------------------- + +.. autofunction:: inventory_provider.routes.poller.eumetsat_multicast diff --git a/inventory_provider/routes/poller.py b/inventory_provider/routes/poller.py index 79c3ec88ce0976f61c3ee1f5c901bbe07e88d671..4ce45cd9524b0ef8fa09149e147058dce4551ea9 100644 --- a/inventory_provider/routes/poller.py +++ b/inventory_provider/routes/poller.py @@ -3,6 +3,8 @@ import logging import re from flask import Blueprint, Response, current_app +from lxml import etree + from inventory_provider import juniper from inventory_provider.routes import common @@ -61,6 +63,7 @@ INTERFACE_LIST_SCHEMA = { 'items': {'$ref': '#/definitions/interface'} } + INTERFACE_SPEED_LIST_SCHEMA = { '$schema': 'http://json-schema.org/draft-07/schema#', @@ -81,6 +84,36 @@ INTERFACE_SPEED_LIST_SCHEMA = { 'items': {'$ref': '#/definitions/interface'} } +MULTICAST_SUBSCRIPTION_LIST_SCHEMA = { + '$schema': 'http://json-schema.org/draft-07/schema#', + + 'definitions': { + 'ipv4-address': { + 'type': 'string', + 'pattern': r'^(\d+\.){3}\d+$' + }, + 'subscription': { + 'type': 'object', + 'properties': { + 'router': {'type': 'string'}, + 'subscription': {'$ref': '#/definitions/ipv4-address'}, + 'endpoint': {'$ref': '#/definitions/ipv4-address'}, + 'oid': { + 'type': 'string', + 'pattern': r'^(\d+\.)*\d+$' + }, + 'community': {'type': 'string'} + }, + 'required': [ + 'router', 'subscription', 'endpoint', 'oid', 'community'], + 'additionalProperties': False + }, + }, + + 'type': 'array', + 'items': {'$ref': '#/definitions/subscription'} +} + @routes.after_request def after_request(resp): @@ -387,3 +420,77 @@ def interface_speeds(hostname=None): r.set(cache_key, result.encode('utf-8')) return Response(result, mimetype="application/json") + + +@routes.route("/eumetsat-multicast", methods=['GET', 'POST']) +@common.require_accepts_json +def eumetsat_multicast(hostname=None): + """ + Handler for `/poller/eumetsat-multicast` which returns information about + multicast subscriptions on mx1.fra.de.geant.net. + + The response is a list of oid/router/community structures that all + all subscription octet counters to be polled. + + .. asjson:: + inventory_provider.routes.poller.MULTICAST_SUBSCRIPTION_LIST_SCHEMA + + This method returns essentially hard-coded data, + based on the information in POL1-395. + + :return: + """ + + MX1_FRA = 'mx1.fra.de.geant.net' + SUBSCRIPTIONS = [{ + 'subscription': f'232.223.223.{idx}', + 'endpoint': '193.17.9.3', + } for idx in range(1, 73)] + + SUBSCRIPTIONS.append( + {'subscription': '232.223.223.1', 'endpoint': '193.17.9.7'}) + SUBSCRIPTIONS.append( + {'subscription': '232.223.223.22', 'endpoint': '193.17.9.7'}) + + def _oid(sub): + return ('1.3.6.1.2.1.83.1.1.2.1.16' + f'.{sub["subscription"]}.{sub["endpoint"]}' + '.255.255.255.255') + + cache_key = 'classifier-cache:poller-eumetsat-multicast' + + r = common.get_current_redis() + + result = r.get(cache_key) + if result: + result = result.decode('utf-8') + else: + netconf = r.get(f'netconf:{MX1_FRA}') + if not netconf: + return Response( + status=503, + response=f'error loading netconf for {MX1_FRA}') + + netconf_doc = etree.fromstring(netconf.decode('utf-8')) + community = juniper.snmp_community_string(netconf_doc) + if not community: + return Response( + status=503, + response=f'error extracting community string for {MX1_FRA}') + + def _rsp_element(sub): + result = { + 'router': MX1_FRA, + 'oid': _oid(sub), + 'community': community + } + result.update(sub) + return result + + result = [_rsp_element(sub) for sub in SUBSCRIPTIONS] + + result = json.dumps(result) + # cache this data for the next call + r.set(cache_key, result.encode('utf-8')) + + return Response(result, mimetype="application/json") diff --git a/test/test_general_poller_routes.py b/test/test_general_poller_routes.py index f3974eec19594c51da31a76fbece6b279e79959b..756446610cfcf062031dab534a0a84900ddb8563 100644 --- a/test/test_general_poller_routes.py +++ b/test/test_general_poller_routes.py @@ -1,6 +1,6 @@ import json import jsonschema -from inventory_provider.routes.poller import INTERFACE_LIST_SCHEMA +from inventory_provider.routes import poller DEFAULT_REQUEST_HEADERS = { "Content-type": "application/json", @@ -15,7 +15,38 @@ def test_get_all_interfaces(client): assert rv.status_code == 200 assert rv.is_json response_data = json.loads(rv.data.decode('utf-8')) - jsonschema.validate(response_data, INTERFACE_LIST_SCHEMA) + jsonschema.validate(response_data, poller.INTERFACE_LIST_SCHEMA) response_routers = {ifc['router'] for ifc in response_data} assert len(response_routers) > 1, \ 'there should data from be lots of routers' + + +def test_all_router_interface_speeds(client): + rv = client.post( + '/poller/speeds', + headers=DEFAULT_REQUEST_HEADERS) + + assert rv.status_code == 200 + response = json.loads(rv.data.decode("utf-8")) + jsonschema.validate(response, poller.INTERFACE_SPEED_LIST_SCHEMA) + assert response # at least shouldn't be empty + response_routers = {ifc['router'] for ifc in response} + assert len(response_routers) > 1, \ + 'there should data from be lots of routers' + + +def test_eumetsat_multicast(mocker, client): + + # routers don't have snmp acl's for us + mocker.patch('inventory_provider.juniper.snmp_community_string') \ + .return_value = 'blah' + + rv = client.get( + '/poller/eumetsat-multicast', + headers=DEFAULT_REQUEST_HEADERS) + assert rv.status_code == 200 + assert rv.is_json + response_data = json.loads(rv.data.decode('utf-8')) + jsonschema.validate( + response_data, poller.MULTICAST_SUBSCRIPTION_LIST_SCHEMA) + assert response_data, "the subscription list shouldn't be empty"