diff --git a/inventory_provider/routes/msr.py b/inventory_provider/routes/msr.py index a1828a2082edb8f23454ddb32a72f7ccd564eac1..1ae5c5d8b4dffd6e3df5ac47588302480631d304 100644 --- a/inventory_provider/routes/msr.py +++ b/inventory_provider/routes/msr.py @@ -83,6 +83,11 @@ These endpoints are intended for use by MSR. .. autofunction:: inventory_provider.routes.msr.asn_peers +/msr/ip-services +-------------------------------------------- + +.. autofunction:: inventory_provider.routes.msr.ip_services + helpers ------------------------------------- @@ -363,6 +368,46 @@ DOMAIN_TO_POP_MAPPING = { "ams.nl": "Amsterdam" } +IP_SERVICES_LIST_SCHEMA = { + '$schema': 'http://json-schema.org/draft-07/schema#', + 'definitions': { + 'service': { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + 'customer': {'type': 'string'}, + 'type': {'type': 'string'}, + 'pop': { + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + 'abbrev': {'type': 'string'}, + }, + 'required': ['name', 'abbrev'], + 'additionalProperties': False + } + }, + 'required': ['name', 'customer', 'type', 'pop'], + 'additionalProperties': False + }, + 'interface-address': { + 'properties': { + 'hostname': {'type': 'string', 'format': 'hostname'}, + 'port': {'type': 'string'}, + 'address': {'type': 'string'}, + 'services': { + 'type': 'array', + 'items': {'$ref': '#/definitions/service'} + } + }, + 'required': ['hostname', 'port', 'address', 'services'], + 'additionalProperties': False + } + }, + 'type': 'array', + 'items': {'$ref': '#/definitions/interface-address'} +} + # very similar to PEERING_LIST_SCHEMA but # with a field for NREN, which is required ASN_PEER_LIST_SCHEMA = { @@ -1316,10 +1361,11 @@ def asn_peers(asn): 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" - }` + `{ + "group": "group to filter by", + "instance": "instance to filter by" + }` + Results are returned where all filters given are true, and exact string matches. @@ -1389,3 +1435,113 @@ def asn_peers(asn): r.set(cache_key, response.encode('utf-8')) return Response(response, mimetype='application/json') + + +def _dedupe(iter_of_objs): + """ + remove duplicates from the input iterable + + the elements of the input iterable must be json-serializable + + :param iter_of_objs: iterable + :return: a new iterable containing unique elements from the original + """ + iter_of_json = (json.dumps(_o, sort_keys=True) for _o in iter_of_objs) + return (json.loads(_s) for _s in set(iter_of_json)) + + +def _load_ip_services(): + """ + yields items that + :return: + """ + + ims_interface_services = {} + equip_port_re = re.compile(r'^ims:interface_services:([^:]+):(.+)') + for doc in common.load_json_docs( + config_params=current_app.config['INVENTORY_PROVIDER_CONFIG'], + key_pattern='ims:interface_services:*', + num_threads=40): + + m = equip_port_re.match(doc['key']) + assert m # sanity + equipment = m.group(1) + port = m.group(2) + assert all([_s['equipment'] == equipment + for _s in doc['value']]) # sanity + assert all([_s['port'] == port + for _s in doc['value']]) # sanity + hostname = ims_equipment_to_hostname(equipment) + ims_interface_services[f'{hostname}:{port.lower()}'] = doc['value'] + + netconf_interfaces = {} + host_if_extraction_re = re.compile(r'^netconf-interfaces:(.+?):') + for doc in common.load_json_docs( + config_params=current_app.config['INVENTORY_PROVIDER_CONFIG'], + key_pattern='netconf-interfaces:*', + num_threads=20): + matches = host_if_extraction_re.match(doc['key']) + if matches: + hostname = matches[1] + doc['value']['hostname'] = hostname + port = doc['value']['name'] + netconf_interfaces[f'{hostname}:{port}'] = doc['value'] + + def _service_info(_s): + return { + 'name': _s['name'], + 'customer': _s['customer'], + 'type': _s['service_type'], + 'pop': { + 'name': _s['pop_name'], + 'abbrev': _s['pop_abbreviation'] + } + } + + def _merged_result(): + for key, ifc in netconf_interfaces.items(): + _services = map( + _service_info, + ims_interface_services.get(key, [])) + _services = list(_dedupe(_services)) + + for address in ifc['ipv4'] + ifc['ipv6']: + try: + yield { + 'hostname': ifc['hostname'], + 'port': ifc['name'], + 'address': address, + 'services': _services + } + except TypeError: + raise + + yield from _merged_result() + + +@routes.route('/ip-services', methods=['GET', 'POST']) +@common.require_accepts_json +def ip_services(): + """ + This method will return a list of all interface addresses + with any operational services defined for that interface. + + The response will be formatted according to the following schema: + + .. asjson:: + inventory_provider.routes.msr.IP_SERVICES_LIST_SCHEMA + + :return: a json list, formatted as above + """ + + cache_key = 'classifier-cache:msr:ip-services' + + r = common.get_current_redis() + result = _ignore_cache_or_retrieve(request, cache_key, r) + + if not result: + result = list(_load_ip_services()) + result = json.dumps(result) + r.set(cache_key, result.encode('utf-8')) + + return Response(result.encode('utf-8'), mimetype='application/json') diff --git a/test/test_msr_routes.py b/test/test_msr_routes.py index 25bd97f80e88357c737a49998c2312b0858128e1..15f8844d9e098b2fabd6a554a2788ce24754e851 100644 --- a/test/test_msr_routes.py +++ b/test/test_msr_routes.py @@ -6,7 +6,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, \ - MDVPN_LIST_SCHEMA, VPN_PROXY_LIST_SCHEMA, ASN_PEER_LIST_SCHEMA + MDVPN_LIST_SCHEMA, VPN_PROXY_LIST_SCHEMA, ASN_PEER_LIST_SCHEMA, \ + IP_SERVICES_LIST_SCHEMA, _dedupe from inventory_provider.routes.poller import SERVICES_LIST_SCHEMA from inventory_provider.tasks.common import _get_redis @@ -391,3 +392,27 @@ def test_get_asn_peers_post(endpoint_variant, post_body, client, mocked_redis): 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 + + +def test_dedupe(): + data = [ + {'a': 1, 'b': {'a': 1, 'b': 2}, 'c': 3}, + {'a': 1, 'b': {'a': 1, 'b': 2}, 'c': 3, 'd': 4}, + {'a': 1, 'b': {'a': 1, 'b': 2}, 'c': 3}, + {'a': 1, 'b': {'a': 1, 'b': 2}, 'c': 3}, + ] + result = list(_dedupe(data)) + assert len(result) == 2 + + +def test_ip_services(client): + + rv = client.get( + '/msr/ip-services', + 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, IP_SERVICES_LIST_SCHEMA) + + assert response_data # test data is non-empty