diff --git a/inventory_provider/config.py b/inventory_provider/config.py index 9cd8085ce85dbac1f61fa46d290e2d29d1809092..f332f5bb2bdf60b66c99c231addf5f0c23fcc94e 100644 --- a/inventory_provider/config.py +++ b/inventory_provider/config.py @@ -184,11 +184,16 @@ CONFIG_SCHEMA = { 'items': {'$ref': '#/definitions/gws-direct-nren-isp'} }, 'nren-asn-map': { - 'type': 'object', - 'patternProperties': { - r'^\d+$': {'type': 'string'} + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'nren': {'type': 'string'}, + 'asn': {'type': 'integer'} + }, + 'required': ['nren', 'asn'], + 'additionalProperties': False }, - 'additionalProperties': False } }, diff --git a/inventory_provider/routes/msr.py b/inventory_provider/routes/msr.py index 6f5087b2fdf6ff14fa2ddd40c298a0ba140c2733..f179a52c73e46727f6834e901383ab6b4351a00c 100644 --- a/inventory_provider/routes/msr.py +++ b/inventory_provider/routes/msr.py @@ -59,6 +59,11 @@ These endpoints are intended for use by MSR. .. autofunction:: inventory_provider.routes.msr.bgp_all_peerings +/msr/mdpvn +-------------------------------------------- + +.. autofunction:: inventory_provider.routes.msr.mdvpn + /msr/services -------------------------------------------- @@ -256,6 +261,66 @@ SYSTEM_CORRELATION_SERVICES_LIST_SCHEMA = { 'minItems': 1 # otherwise the route should return 404 } +MDVPN_LIST_SCHEMA = { + '$schema': 'http://json-schema.org/draft-07/schema#', + 'definitions': { + 'mdvpn_group': { + 'type': 'object', + 'properties': { + 'asn': {'type': 'integer'}, + 'AP': {'$ref': '#/definitions/ap_peerings'}, + 'VRR': {'$ref': '#/definitions/vrr_peerings'} + }, + 'required': [ + 'asn', 'AP', 'VRR' + ], + 'additionalProperties': False + }, + 'ap_peerings': { + 'type': 'array', + 'items': { + '$ref': '#/definitions/bgplu_peering' + } + }, + 'bgplu_peering': { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + 'v4': {'type': 'string'}, + 'v6': {'type': 'string'}, + 'hostname': {'type': 'string'} + }, + 'required': [ + 'name', 'v4', 'v6', 'hostname' + ], + 'additionalProperties': False + }, + 'vrr_peerings': { + 'type': 'array', + 'items': { + '$ref': '#/definitions/vpn_peering' + } + }, + 'vpn_peering': { + 'type': 'object', + 'properties': { + 'description': {'type': 'string'}, + 'v4': {'type': 'string'}, + 'hostname': { + 'type': 'array', + 'items': { + 'type': 'string' + }, + 'minItems': 1 + } + }, + 'additionalProperties': False + } + }, + 'type': 'array', + 'items': {'$ref': '#/definitions/mdvpn_group'} +} + @routes.after_request def after_request(resp): @@ -875,3 +940,116 @@ def bgp_all_peerings(): r = common.get_current_redis() response = r.get('juniper-peerings:all') return Response(response.decode('utf-8'), mimetype="application/json") + + +@routes.route('/mdvpn', methods=['GET', 'POST']) +@common.require_accepts_json +def mdvpn(): + """ + Handler for `/mdvpn` + + This method returns a list of all BGP-LU peerings, and the VR peerings + for both Paris & Ljubljana. + + The response will be formatted according to the following schema: + + .. asjson:: + inventory_provider.routes.msr.MDVPN_LIST_SCHEMA + + :return: + """ + + def _get_consistent_description(description): + """ + The same interface in VRR peerings can have multiple names. + These names are (currently) the same but with a different local prefix, + with no ordering guaranteed by the redis cache. + As only one description is returned by this endpoint for each + IPv4 address, this serves as a quick and dirty way of merging these + multiple descriptions into one an external user can use to identify + the peering reliably. + + :param description: The raw description for a VRR peering + :return: The same description with location prefix removed + """ + # it is incredibly likely this will need revision later down the line + expected_prefixes = [ + "MD-VPN-VRR-PARIS-", + "MD-VPN-VRR-LJUBLJANA-" + ] + for prefix in expected_prefixes: + if description.startswith(prefix): + return description.replace(prefix, '') + return description + + def _make_group_index(group, index_key): + """ + Utility function to take a list and make it a dict based off a given + key, for fast lookup of a specific key field. + + :param group: A list of dicts which should all have `index_key` as a + field + :param index_key: Name of the key to index on + :return: Dict with `index_key` as the key field and a list of all + matching dicts as the value + """ + index = {} + for peering in group: + key = peering.get(index_key) + index.setdefault(key, []).append(peering) + return index + + def _bgplu_peerings(asn, bgplu_index): + for peering in bgplu_index.get(asn, []): + formatted_peering = { + "name": peering['description'], + "v4": peering['address'], + "v6": '', + "hostname": peering['hostname'] + } + yield formatted_peering + + def _vpnrr_peerings(asn, vpnrr_index): + if asn in vpnrr_index: + vrr_peering_group = vpnrr_index[asn] + # rearrange into index using ipv4 as key + # this will collect related entries under the same ipv4 + ip_index = _make_group_index(vrr_peering_group, 'address') + for ip in ip_index: + ip_details = ip_index[ip] # a list of all info for given ipv4 + hostnames = [item['hostname'] for item in ip_details] + description = ip_details[0]['description'] + + formatted_peering = { + "description": _get_consistent_description(description), + "v4": ip, + "hostname": hostnames + } + yield formatted_peering + + def _peerings_for_nren(asn, bgplu_index, vpnrr_index): + return { + "asn": asn, + "AP": list(_bgplu_peerings(asn, bgplu_index)), + "VRR": list(_vpnrr_peerings(asn, vpnrr_index)) + } + + r = common.get_current_redis() + cache_key = 'classifier-cache:msr:mdvpn' + response = _ignore_cache_or_retrieve(request, cache_key, r) + if not response: + bgplu = json.loads( + r.get('juniper-peerings:group:BGPLU').decode('utf-8')) + vpnrr = json.loads( + r.get('juniper-peerings:group:VPN-RR').decode('utf-8')) + bgplu_index = _make_group_index(bgplu, 'remote-asn') + vpnrr_index = _make_group_index(vpnrr, 'remote-asn') + config = current_app.config['INVENTORY_PROVIDER_CONFIG'] + nren_asn_map = config['nren-asn-map'] + nren_details = [ + _peerings_for_nren(pair['asn'], + bgplu_index, + vpnrr_index) + for pair in nren_asn_map] + response = json.dumps(nren_details) + return Response(response, mimetype='application/json') diff --git a/test/conftest.py b/test/conftest.py index b1be8a02b80f93784bbc4aee244ca66efbfc766d..4e6d794b9c1417fee51526d360f79ed58a04c844 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -60,11 +60,20 @@ def data_config_filename(): } ], 'gws-direct': {}, - 'nren-asn-map': { - "100": "BogusNREN", - "200": "FoobarNREN", - "300": "AlsoNET" - } + 'nren-asn-map': [ + { + "nren": "FOO", + "asn": 1930 + }, + { + "nren": "BAR", + "asn": 680 + }, + { + "nren": "BAT", + "asn": 2200 + } + ] } with open(os.path.join(TEST_DATA_DIRNAME, 'gws-direct.json')) as gws: diff --git a/test/test_msr_routes.py b/test/test_msr_routes.py index 0bec3d504185fa7070f8e06fa5a3686ec91c6c74..eda590d2613d9b45d0383cb660c0626ee96e7098 100644 --- a/test/test_msr_routes.py +++ b/test/test_msr_routes.py @@ -5,7 +5,8 @@ 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 + SYSTEM_CORRELATION_SERVICES_LIST_SCHEMA, _get_services_for_address, \ + MDVPN_LIST_SCHEMA from inventory_provider.routes.poller import SERVICES_LIST_SCHEMA from inventory_provider.tasks.common import _get_redis @@ -321,3 +322,15 @@ def test_get_all_peerings(client): response_data = json.loads(rv.data.decode('utf-8')) jsonschema.validate(response_data, PEERING_LIST_SCHEMA) assert response_data # test data is non-empty + + +def test_get_mdvpn_peerings(client, mocked_redis): + rv = client.get( + '/msr/mdvpn', + 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, MDVPN_LIST_SCHEMA) + assert response_data # test data is non-empty