diff --git a/inventory_provider/routes/msr.py b/inventory_provider/routes/msr.py index 5b28b7a6b5aada58fb371c808d71709f0d69e7a3..8c052a67f30d3f540375a2666c9b5ffd460fb3d9 100644 --- a/inventory_provider/routes/msr.py +++ b/inventory_provider/routes/msr.py @@ -74,7 +74,13 @@ These endpoints are intended for use by MSR. /msr/vpn-proxy -------------------------------------------- -.. autofunction:: inventory_provider.routes.msr.vpn-proxy +.. autofunction:: inventory_provider.routes.msr.vpn_proxy + + +/msr/asn-peers +-------------------------------------------- + +.. autofunction:: inventory_provider.routes.msr.asn_peers helpers @@ -356,6 +362,40 @@ DOMAIN_TO_POP_MAPPING = { "ams.nl": "Amsterdam" } +# very similar to PEERING_LIST_SCHEMA but +# with a field for NREN, which is required +ASN_PEER_LIST_SCHEMA = { + '$schema': 'http://json-schema.org/draft-07/schema#', + 'definitions': { + 'peering-instance': { + 'type': 'object', + 'properties': { + 'address': {'type': 'string'}, + 'description': {'type': 'string'}, + 'logical-system': {'type': 'string'}, + 'group': {'type': 'string'}, + 'hostname': {'type': 'string'}, + 'remote-asn': {'type': 'integer'}, + 'local-asn': {'type': 'integer'}, + 'instance': {'type': 'string'}, + 'nren': {'type': 'string'} + }, + # only vrr peerings have remote-asn + # only group peerings have local-asn or instance + # not all group peerings have 'description' + # and only vrr or vpn-proxy peerings are within a logical system + 'required': [ + 'address', + 'group', + 'hostname', + 'nren'], + 'additionalProperties': False + } + }, + 'type': 'array', + 'items': {'$ref': '#/definitions/peering-instance'} +} + @routes.after_request def after_request(resp): @@ -1173,3 +1213,88 @@ def vpn_proxy(): peerings = list(_format_peerings(vpnproxy)) response = json.dumps(peerings) return Response(response, mimetype='application/json') + + +@routes.route('/asn-peers', methods=['GET', 'POST'], defaults={'asn': None}) +@routes.route('/asn-peers/<int:asn>', methods=['GET', 'POST']) +@common.require_accepts_json +def asn_peers(asn): + """ + Handler for `/asn-peers` + + This method returns a list of all peers filtered by `group` and `instance`, + which can be passed either as URL query parameters or as entries in a + POST request with a JSON body that matches this schema: + `{ + "group": "group to filter by", + "instance": "instance to filter by" + }` + Results are returned where all filters given are true, and exact string + matches. + + An optional URL parameter can be used to also filter by a specific ASN. + + The response will be formatted according to the following schema: + + .. asjson:: + inventory_provider.routes.msr.ASN_PEER_LIST_SCHEMA + + :param asn: specific ASN to get peers for + :return: + """ + r = common.get_current_redis() + + def _get_filtered_peers_for_asn(asn, nren, group, instance): + peers = json.loads(r.get(f'juniper-peerings:peer-asn:{asn}')) + + def _attribute_filter(peer, name, value): + if value is None: + return True # no filter parameter given in request + if name not in peer: + return False # no value exists, cannot meet condition + return peer[name] == value + + for peer in peers: + if _attribute_filter(peer, "group", group) and \ + _attribute_filter(peer, "instance", instance): + peer['nren'] = nren + yield peer + + def _get_filtered_peers(asn_nren_map, group, instance): + for asn, nren in asn_nren_map.items(): + asn_peers = _get_filtered_peers_for_asn(asn, nren, group, instance) + for peer in asn_peers: + yield peer + + # handle getting parameters regardless of method of input + if request.method == 'GET': + group = request.args.get('group') + instance = request.args.get('instance') + else: + params = json.loads(request.json) + group = params.get('group', None) + instance = params.get('instance', None) + + cache_key = f'classifier-cache:msr:asn-peers:{asn}:{group}:{instance}' + response = _ignore_cache_or_retrieve(request, cache_key, r) + + if not response: + config = current_app.config['INVENTORY_PROVIDER_CONFIG'] + # set up quick lookup based on ASN + asn_nren_map = { + item['asn']: item['nren'] for item in config['nren-asn-map'] + } + + if asn is not None: + nren = asn_nren_map.get(asn, None) + peers = list( + _get_filtered_peers_for_asn(asn, nren, group, instance) + ) + else: + peers = list( + _get_filtered_peers(asn_nren_map, group, instance) + ) + response = json.dumps(peers) + r.set(cache_key, response.encode('utf-8')) + + return Response(response, mimetype='application/json') diff --git a/test/conftest.py b/test/conftest.py index 4e6d794b9c1417fee51526d360f79ed58a04c844..c95e25c6ce8af247f6e92ea9f5a8d633500d5d50 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -72,6 +72,10 @@ def data_config_filename(): { "nren": "BAT", "asn": 2200 + }, + { + "nren": "BAZ", + "asn": 1853 } ] } diff --git a/test/test_msr_routes.py b/test/test_msr_routes.py index b47c09307be1458eb1c0706ca0476138cc75a9ac..25bd97f80e88357c737a49998c2312b0858128e1 100644 --- a/test/test_msr_routes.py +++ b/test/test_msr_routes.py @@ -6,7 +6,7 @@ import pytest from inventory_provider.routes.msr import PEERING_LIST_SCHEMA, \ PEERING_GROUP_LIST_SCHEMA, PEERING_ADDRESS_SERVICES_LIST, \ SYSTEM_CORRELATION_SERVICES_LIST_SCHEMA, _get_services_for_address, \ - MDVPN_LIST_SCHEMA, VPN_PROXY_LIST_SCHEMA + MDVPN_LIST_SCHEMA, VPN_PROXY_LIST_SCHEMA, ASN_PEER_LIST_SCHEMA from inventory_provider.routes.poller import SERVICES_LIST_SCHEMA from inventory_provider.tasks.common import _get_redis @@ -346,3 +346,48 @@ def test_get_vpn_proxy_peerings(client, mocked_redis): response_data = json.loads(rv.data.decode('utf-8')) jsonschema.validate(response_data, VPN_PROXY_LIST_SCHEMA) assert response_data # test data is non-empty + + +@pytest.mark.parametrize('endpoint_variant', [ + "", # default, no filter + "/1853", + "?group=IAS-NRENS", + "?instance=IAS", + "?group=IAS-NRENS&instance=IAS", + "/1853?group=IAS-NRENS", + "/1853?instance=IAS", + "/1853?group=IAS-NRENS&instance=IAS" +]) +def test_get_asn_peers_get(endpoint_variant, client, mocked_redis): + rv = client.get( + f'/msr/asn-peers{endpoint_variant}', + 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, ASN_PEER_LIST_SCHEMA) + assert response_data # test data is non-empty + + +@pytest.mark.parametrize('endpoint_variant,post_body', [ + ("", '{}'), + ("", '{"group": "IAS-NRENS"}'), + ("", '{"instance": "IAS"}'), + ("", '{"group": "IAS-NRENS", "instance": "IAS"}'), + ("/1853", '{}'), + ("/1853", '{"group": "IAS-NRENS"}'), + ("/1853", '{"instance": "IAS"}'), + ("/1853", '{"group": "IAS-NRENS", "instance": "IAS"}') +]) +def test_get_asn_peers_post(endpoint_variant, post_body, client, mocked_redis): + rv = client.post( + f'/msr/asn-peers{endpoint_variant}', + headers=DEFAULT_REQUEST_HEADERS, + json=post_body + ) + assert rv.status_code == 200 + assert rv.is_json + response_data = json.loads(rv.data.decode('utf-8')) + jsonschema.validate(response_data, ASN_PEER_LIST_SCHEMA) + assert response_data # test data is non-empty