diff --git a/docs/source/protocol/msr.rst b/docs/source/protocol/msr.rst index 112a7113ce33de8244fdda4c58fda7661ee134a0..12eb41eefaaf3fc24995b1dac69591a808860c62 100644 --- a/docs/source/protocol/msr.rst +++ b/docs/source/protocol/msr.rst @@ -14,7 +14,33 @@ These endpoints are intended for use by MSR. .. autofunction:: inventory_provider.routes.msr.access_services -/msr/logical-system-peerings</name> +/msr/bgp/logical-systems ------------------------------------- -.. autofunction:: inventory_provider.routes.msr.logical_system_peerings \ No newline at end of file +.. autofunction:: inventory_provider.routes.msr.get_logical_systems + + +/msr/bgp/logical-system-peerings</name> +------------------------------------------ + +.. autofunction:: inventory_provider.routes.msr.logical_system_peerings + + +/msr/bgp/groups +------------------------------------- + +.. autofunction:: inventory_provider.routes.msr.get_peering_groups + + +/msr/bgp/group-peerings</name> +------------------------------------- + +.. autofunction:: inventory_provider.routes.msr.bgp_group_peerings + + +helpers +------------------------------------- + +.. autofunction:: inventory_provider.routes.msr._handle_peering_group_list_request + +.. autofunction:: inventory_provider.routes.msr._handle_peering_group_request diff --git a/inventory_provider/routes/msr.py b/inventory_provider/routes/msr.py index 58693b68a2ae1b80cffa33891ded878847ec1e78..62e25212e3291d23d0b5635c63ce9bba9cde1c35 100644 --- a/inventory_provider/routes/msr.py +++ b/inventory_provider/routes/msr.py @@ -38,8 +38,14 @@ ACCESS_SERVICES_LIST_SCHEMA = { "items": {"$ref": "#/definitions/service"} } +PEERING_GROUP_LIST_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "items": {"type": "string"} + +} -LOGICAL_SYSTEM_PEERING_LIST_SCHEMA = { +PEERING_LIST_SCHEMA = { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "peering-instance": { @@ -50,13 +56,16 @@ LOGICAL_SYSTEM_PEERING_LIST_SCHEMA = { "logical-system": {"type": "string"}, "group": {"type": "string"}, "hostname": {"type": "string"}, - "remote-asn": {"type": "integer"} + "remote-asn": {"type": "integer"}, + "local-asn": {"type": "integer"}, + "instance": {"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", - "description", - "logical-system", "group", "hostname"], "additionalProperties": False @@ -114,30 +123,32 @@ def access_services(): return jsonify(result) -@routes.route("/logical-system-peerings", methods=['GET', 'POST']) -@routes.route("/logical-system-peerings/<name>", methods=['GET', 'POST']) -@common.require_accepts_json -def logical_system_peerings(name=None): +def _handle_peering_group_request(name, cache_key, group_key_base): """ - Handler for `/msr/logical-system-peerings` + Common method for used by + :meth:`inventory_provider.routes.msr.logical_system_peerings` and + :meth:`inventory_provider.routes.msr.bgp_group_peerings`. This method will return a list of all peerings configured - for the requested logical-system name on any router, or for any - logical system if no parameter is given. + for the specified group `name on any router, + or for all group names if `name` None. The response will be formatted according to the following schema: .. asjson:: - inventory_provider.routes.msr.LOGICAL_SYSTEM_PEERING_LIST_SCHEMA + inventory_provider.routes.msr.PEERING_LIST_SCHEMA - :return: + :param name: group/logical-system name, or None + :param cache_key: base cache key for this type of request + :param group_key_base: key above which the peerings are grouped + :return: a json list, formatted as above """ r = common.get_current_redis() - def _get_all_ls_keys(): + def _get_all_subkeys(): keys = [] - for k in r.scan_iter('juniper-peerings:logical-system:*', count=1000): + for k in r.scan_iter(f'{group_key_base}:*', count=1000): keys.append(k.decode('utf-8')) return keys @@ -146,7 +157,6 @@ def logical_system_peerings(name=None): if value: yield from json.loads(value.decode('utf-8')) - cache_key = 'classifier-cache:msr:logical-system-peerings' if name: cache_key = f'{cache_key}:{name}' @@ -156,9 +166,9 @@ def logical_system_peerings(name=None): items = json.loads(items.decode('utf-8')) else: if name: - items = _load_list_items(f'juniper-peerings:logical-system:{name}') + items = _load_list_items(f'{group_key_base}:{name}') else: - gen_list = list(map(_load_list_items, _get_all_ls_keys())) + gen_list = list(map(_load_list_items, _get_all_subkeys())) items = itertools.chain(*gen_list) items = list(items) @@ -171,3 +181,117 @@ def logical_system_peerings(name=None): r.set(cache_key, json.dumps(items).encode('utf-8')) return jsonify(items) + + +@routes.route("/bgp/logical-system-peerings", methods=['GET', 'POST']) +@routes.route("/bgp/logical-system-peerings/<name>", methods=['GET', 'POST']) +@common.require_accepts_json +def logical_system_peerings(name=None): + """ + Handler for `/msr/bgp/logical-system-peerings` + + This method will return a list of all peerings configured + for the requested logical-system name on any router, or for any + logical system if no parameter is given. + + :return: see :meth:`inventory_provider.routes.msr._handle_peering_group_request` + """ # noqa: E501 + return _handle_peering_group_request( + name=name, + cache_key='classifier-cache:msr:logical-system-peerings', + group_key_base='juniper-peerings:logical-system') + + +@routes.route("/bgp/group-peerings", methods=['GET', 'POST']) +@routes.route("/bgp/group-peerings/<name>", methods=['GET', 'POST']) +@common.require_accepts_json +def bgp_group_peerings(name=None): + """ + Handler for `/msr/bgp/group-peerings` + + This method will return a list of all peerings configured + for the requested logical-system name on any router, or for any + logical system if no parameter is given. + + :return: see :meth:`inventory_provider.routes.msr._handle_peering_group_request` + """ # noqa: E501 + return _handle_peering_group_request( + name=name, + cache_key='classifier-cache:msr:group-peerings', + group_key_base='juniper-peerings:group') + + +def _handle_peering_group_list_request(cache_key, group_key_base): + """ + Common method for used by + :meth:`inventory_provider.routes.msr.get_logical_systems` and + :meth:`inventory_provider.routes.msr.get_peering_groups`. + + This method will return a list of all immediate subkeys of + `group_key_base`. + + The response will be formatted according to the following schema: + + .. asjson:: + inventory_provider.routes.msr.PEERING_GROUP_LIST_SCHEMA + + :param cache_key: base cache key for this type of request + :param group_key_base: key above which the peerings are grouped + :return: a json list, formatted as above + """ + + r = common.get_current_redis() + + def _get_all_subkeys(): + for k in r.scan_iter(f'{group_key_base}:*', count=1000): + k = k.decode('utf-8') + yield k[len(group_key_base) + 1:] + + names = r.get(cache_key) + + if names: + names = json.loads(names.decode('utf-8')) + else: + names = list(_get_all_subkeys()) + if not names: + return Response( + response='no groups found', + status=404, + mimetype="text/html") + names = sorted(names) + + r.set(cache_key, json.dumps(names).encode('utf-8')) + + return jsonify(names) + + +@routes.route("/bgp/logical-systems", methods=['GET', 'POST']) +@common.require_accepts_json +def get_logical_systems(): + """ + Handler for `/msr/bgp/logical-systems` + + Returns a list of logical system names for which peering + information is available. + + :return: see :meth:`inventory_provider.routes.msr._handle_peering_group_list_request` + """ # noqa: E501 + return _handle_peering_group_list_request( + cache_key='classifier-cache:msr:logical-systems', + group_key_base='juniper-peerings:logical-system') + + +@routes.route("/bgp/groups", methods=['GET', 'POST']) +@common.require_accepts_json +def get_peering_groups(): + """ + Handler for `/msr/bgp/groups` + + Returns a list of group names for which peering + information is available. + + :return: see :meth:`inventory_provider.routes.msr._handle_peering_group_list_request` + """ # noqa: E501 + return _handle_peering_group_list_request( + cache_key='classifier-cache:msr:peering-groups', + group_key_base='juniper-peerings:group') diff --git a/test/test_msr_routes.py b/test/test_msr_routes.py index a54695271cee86b1377ba8c8cd4aa57e2e33ac29..173cf1528a35ee8d051b17e1f6f6a7883d9a7e34 100644 --- a/test/test_msr_routes.py +++ b/test/test_msr_routes.py @@ -3,8 +3,8 @@ import jsonschema import pytest -from inventory_provider.routes.msr \ - import ACCESS_SERVICES_LIST_SCHEMA, LOGICAL_SYSTEM_PEERING_LIST_SCHEMA +from inventory_provider.routes.msr import ACCESS_SERVICES_LIST_SCHEMA, \ + PEERING_LIST_SCHEMA, PEERING_GROUP_LIST_SCHEMA DEFAULT_REQUEST_HEADERS = { "Content-type": "application/json", @@ -26,25 +26,26 @@ def test_access_services(client): def test_logical_system_peerings_all(client): rv = client.get( - '/msr/logical-system-peerings', + '/msr/bgp/logical-system-peerings', 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, LOGICAL_SYSTEM_PEERING_LIST_SCHEMA) + jsonschema.validate(response_data, PEERING_LIST_SCHEMA) assert response_data # test data is non-empty + assert all('logical-system' in p for p in response_data) @pytest.mark.parametrize('name', ['VRR', 'VPN-PROXY']) def test_logical_system_peerings_specific(client, name): rv = client.get( - f'/msr/logical-system-peerings/{name}', + f'/msr/bgp/logical-system-peerings/{name}', 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, LOGICAL_SYSTEM_PEERING_LIST_SCHEMA) + jsonschema.validate(response_data, PEERING_LIST_SCHEMA) assert response_data # test data is non-empty assert all(p['logical-system'] == name for p in response_data) @@ -59,6 +60,53 @@ def test_logical_system_peerings_specific(client, name): ]) def test_logical_system_peerings_404(client, name): rv = client.get( - f'/msr/logical-system-peerings/{name}', + f'/msr/bgp/logical-system-peerings/{name}', headers=DEFAULT_REQUEST_HEADERS) assert rv.status_code == 404 + + +def test_group_peerings_all(client): + rv = client.get( + '/msr/bgp/group-peerings', + 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, PEERING_LIST_SCHEMA) + + assert response_data # test data is non-empty + + +@pytest.mark.parametrize('name', ['BGPLU', 'eGEANT', 'eGEANT-mcast']) +def test_group_peerings_specific(client, name): + rv = client.get( + f'/msr/bgp/group-peerings/{name}', + 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, PEERING_LIST_SCHEMA) + + assert response_data # test data is non-empty + assert all(p['group'] == name for p in response_data) + + +@pytest.mark.parametrize('name', ['EGEANT', 'eGEANT mcast']) +def test_group_peerings_404(client, name): + rv = client.get( + f'/msr/bgp/logical-system-peerings/{name}', + headers=DEFAULT_REQUEST_HEADERS) + assert rv.status_code == 404 + + +@pytest.mark.parametrize('uri', [ + '/msr/bgp/logical-systems', + '/msr/bgp/groups']) +def test_peerings_group_list(client, uri): + rv = client.get(uri, 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, PEERING_GROUP_LIST_SCHEMA) + + assert response_data # test data is non-empty diff --git a/test/test_worker_utils.py b/test/test_worker_utils.py index 1d49ffed06556d3f09a472a3d50cdcf8e57f0364..27727533628e63498d4e436c6c82b396f4f9a4e8 100644 --- a/test/test_worker_utils.py +++ b/test/test_worker_utils.py @@ -9,7 +9,7 @@ import jsonschema from inventory_provider.tasks import worker from inventory_provider.tasks import common -from inventory_provider.routes.msr import LOGICAL_SYSTEM_PEERING_LIST_SCHEMA +from inventory_provider.routes import msr def backend_db(): @@ -141,7 +141,7 @@ def test_build_juniper_peering_db(mocked_worker_module): "additionalProperties": False } - PEERING_LIST_SCHEMA = { + DETAILED_PEERING_LIST_SCHEMA = { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "top-level-peering": TOP_LEVEL_PEERING_SCHEMA, @@ -194,15 +194,16 @@ def test_build_juniper_peering_db(mocked_worker_module): assert address == canonical continue - jsonschema.validate(value, PEERING_LIST_SCHEMA) + jsonschema.validate(value, DETAILED_PEERING_LIST_SCHEMA) if 'logical-system:' in key: - jsonschema.validate(value, LOGICAL_SYSTEM_PEERING_LIST_SCHEMA) + jsonschema.validate(value, msr.PEERING_LIST_SCHEMA) m = re.match(r'.*logical-system:(.+)$', key) assert all(p['logical-system'] == m.group(1) for p in value) found_logical_system = True if 'group:' in key: + jsonschema.validate(value, msr.PEERING_LIST_SCHEMA) m = re.match(r'.*group:(.+)$', key) assert all(p['group'] == m.group(1) for p in value) found_group = True