diff --git a/docs/source/protocol/poller.rst b/docs/source/protocol/poller.rst index 8bfa6ddc4bb39ece105e3ba0adf93072a22e8355..64628fc062a190b28a0c4c27a55ac6db2b87b760 100644 --- a/docs/source/protocol/poller.rst +++ b/docs/source/protocol/poller.rst @@ -6,7 +6,15 @@ BRIAN support Endpoints These endpoints are intended for use by BRIAN. -/poller/interfaces +.. contents:: :local: + +/poller/interfaces</hostname> --------------------------------- .. autofunction:: inventory_provider.routes.poller.interfaces + + +/poller/speeds</hostname> +--------------------------------- + +.. autofunction:: inventory_provider.routes.poller.interface_speeds diff --git a/inventory_provider/routes/poller.py b/inventory_provider/routes/poller.py index f606759e3d0bd09128270ba756c27946a8683b63..4bd254f76c827777abb661d51b7342dc48fa3031 100644 --- a/inventory_provider/routes/poller.py +++ b/inventory_provider/routes/poller.py @@ -9,6 +9,8 @@ from inventory_provider.routes import common logger = logging.getLogger(__name__) routes = Blueprint('poller-support-routes', __name__) +Gb = 1 << 30 + INTERFACE_LIST_SCHEMA = { '$schema': 'http://json-schema.org/draft-07/schema#', @@ -59,6 +61,26 @@ INTERFACE_LIST_SCHEMA = { 'items': {'$ref': '#/definitions/interface'} } +INTERFACE_SPEED_LIST_SCHEMA = { + '$schema': 'http://json-schema.org/draft-07/schema#', + + 'definitions': { + 'interface': { + 'type': 'object', + 'properties': { + 'router': {'type': 'string'}, + 'name': {'type': 'string'}, + 'speed': {'type': 'integer'} + }, + 'required': ['router', 'name', 'speed'], + 'additionalProperties': False + }, + }, + + 'type': 'array', + 'items': {'$ref': '#/definitions/interface'} +} + @routes.after_request def after_request(resp): @@ -150,42 +172,83 @@ def _load_interfaces(hostname): 'name': ifc['name'], 'bundle': ifc['bundle'], 'bundle-parents': [], - 'snmp-index': -1, 'description': ifc['description'], 'circuits': [] } -def _load_poller_interfaces(hostname=None): +def _add_bundle_parents(interfaces, hostname=None): + """ + generator that adds bundle-parents info to each interface. - snmp_indexes = _load_snmp_indexes(hostname) + :param interfaces: result of _load_interfaces + :param hostname: hostname or None for all + :return: generator with bundle-parents populated in each element + """ bundles = _load_interface_bundles(hostname) - services = _load_services(hostname) - - for ifc in _load_interfaces(hostname): - - router_snmp = snmp_indexes.get(ifc['router'], None) - if not router_snmp or ifc['name'] not in router_snmp: - # there's no way to poll this interface - continue - ifc['snmp-index'] = router_snmp[ifc['name']]['index'] - # TODO: uncomment this when it won't break poller-admin-service - # not urgent ... it looks empirically like all logical-system - # interfaces are repeated for both communities - # ifc['snmp-community'] = router_snmp[ifc['name']]['community'] - + for ifc in interfaces: router_bundle = bundles.get(ifc['router'], None) if router_bundle: base_ifc = ifc['name'].split('.')[0] ifc['bundle-parents'] = router_bundle.get(base_ifc, []) + yield ifc + + +def _add_circuits(interfaces, hostname=None): + """ + generator that adds service info to each interface. + :param interfaces: result of _load_interfaces + :param hostname: hostname or None for all + :return: generator with 'circuits' populated in each element, if present + """ + services = _load_services(hostname) + for ifc in interfaces: router_services = services.get(ifc['router'], None) if router_services: ifc['circuits'] = router_services.get(ifc['name'], []) + yield ifc + + +def _add_snmp_indexes(interfaces, hostname=None): + """ + generator that adds snmp info to each interface, if available + :param interfaces: result of _load_interfaces + :param hostname: hostname or None for all + :return: generator with 'snmp-index' optionally added to each element + """ + snmp_indexes = _load_snmp_indexes(hostname) + for ifc in interfaces: + router_snmp = snmp_indexes.get(ifc['router'], None) + if router_snmp and ifc['name'] in router_snmp: + ifc['snmp-index'] = router_snmp[ifc['name']]['index'] + # TODO: uncomment this when it won't break poller-admin-service + # not urgent ... it looks empirically like all logical-system + # interfaces are repeated for both communities + # ifc['snmp-community'] = router_snmp[ifc['name']]['community'] yield ifc +def _load_interfaces_to_poll(hostname=None): + """ + prepares the result of a call to /interfaces + + :param hostname: hostname or None for all + :return: generator yielding interface elements + """ + basic_interfaces = _load_interfaces(hostname) + with_bundles = _add_bundle_parents(basic_interfaces, hostname) + with_circuits = _add_circuits(with_bundles, hostname) + with_snmp = _add_snmp_indexes(with_circuits, hostname) + + def _has_snmp_index(ifc): + return 'snmp-index' in ifc + + # only return interfaces that can be polled + return filter(_has_snmp_index, with_snmp) + + @routes.route("/interfaces", methods=['GET', 'POST']) @routes.route('/interfaces/<hostname>', methods=['GET', 'POST']) @common.require_accepts_json @@ -216,7 +279,104 @@ def interfaces(hostname=None): if result: result = result.decode('utf-8') else: - result = list(_load_poller_interfaces(hostname)) + result = list(_load_interfaces_to_poll(hostname)) + if not result: + return Response( + response='no interfaces found', + status=404, + mimetype='text/html') + + 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") + + +def interface_speed(ifc): + """ + Return the maximum bits per second expected for the given interface. + + cf. https://www.juniper.net/documentation/us/en/software/ + vmx/vmx-getting-started/topics/task/ + vmx-chassis-interface-type-configuring.html + + :param ifc: + :return: an integer bits per second + """ + + def _name_to_speed(ifc_name): + if ifc_name.startswith('ge'): + return Gb + if ifc_name.startswith('xe'): + return 10 * Gb + if ifc_name.startswith('et'): + return 100 * Gb + logger.warning(f'unrecognized interface name: {ifc_name}') + return -1 + + if ifc['bundle-parents']: + if not ifc['name'].startswith('ae'): + logger.warning( + f'ifc has bundle-parents, but name is {ifc["name"]}') + return sum(_name_to_speed(name) for name in ifc['bundle-parents']) + + return _name_to_speed(ifc['name']) + + +def _load_interfaces_and_speeds(hostname=None): + """ + prepares the result of a call to /speeds + + :param hostname: hostname or None for all + :return: generator yielding interface elements + """ + basic_interfaces = _load_interfaces(hostname) + with_bundles = _add_bundle_parents(basic_interfaces, hostname) + with_bundles = list(with_bundles) + + def _result_ifc(ifc): + return { + 'router': ifc['router'], + 'name': ifc['name'], + 'speed': interface_speed(ifc) + } + + return map(_result_ifc, with_bundles) + + +@routes.route("/speeds", methods=['GET', 'POST']) +@routes.route('/speeds/<hostname>', methods=['GET', 'POST']) +@common.require_accepts_json +def interface_speeds(hostname=None): + """ + Handler for `/poller/speeds` and + `/poller/speeds/<hostname>` + which returns information for either all interfaces + or those on the requested hostname. + + The response is a list of maximum speed information (in bits + per second) for all known interfaces. + + *speed <= 0 means the max interface speed can't be determined* + + .. asjson:: + inventory_provider.routes.poller.INTERFACE_SPEED_LIST_SCHEMA + + :param hostname: optional, if present should be a router hostname + :return: + """ + + cache_key = f'classifier-cache:poller-interface-speeds:{hostname}' \ + if hostname else 'classifier-cache:poller-interface-speeds:all' + + r = common.get_current_redis() + + result = r.get(cache_key) + if result: + result = result.decode('utf-8') + else: + result = list(_load_interfaces_and_speeds(hostname)) if not result: return Response( response='no interfaces found', diff --git a/test/per_router/test_poller_routes.py b/test/per_router/test_poller_routes.py index d7351c09dd899ab785cf73568a2039c8c96a0d10..bc304e5b7aca1fca9134dfceeba2c65e4de8d770 100644 --- a/test/per_router/test_poller_routes.py +++ b/test/per_router/test_poller_routes.py @@ -1,6 +1,7 @@ import json import jsonschema -from inventory_provider.routes.poller import INTERFACE_LIST_SCHEMA +from inventory_provider.routes.poller \ + import INTERFACE_LIST_SCHEMA, INTERFACE_SPEED_LIST_SCHEMA DEFAULT_REQUEST_HEADERS = { "Content-type": "application/json", @@ -19,3 +20,16 @@ def test_router_interfaces(router, client): assert response # at least shouldn't be empty response_routers = {ifc['router'] for ifc in response} assert response_routers == {router} + + +def test_router_interface_speeds(router, client): + rv = client.post( + f'/poller/speeds/{router}', + headers=DEFAULT_REQUEST_HEADERS) + + assert rv.status_code == 200 + response = json.loads(rv.data.decode("utf-8")) + jsonschema.validate(response, INTERFACE_SPEED_LIST_SCHEMA) + assert response # at least shouldn't be empty + response_routers = {ifc['router'] for ifc in response} + assert response_routers == {router}