From 8ab101d9b07764acc2ae51e7975bf78a921b52d1 Mon Sep 17 00:00:00 2001 From: Neda Moeini <neda.moeini@geant.org> Date: Thu, 6 Jun 2024 13:35:29 +0200 Subject: [PATCH] 1- Removed managed_routers from config and tests. 2- Updated tests 3- Made gap.py function-based --- inventory_provider/config.py | 3 - inventory_provider/gap.py | 204 +++++++++++++-------------- inventory_provider/juniper.py | 15 -- inventory_provider/routes/testing.py | 3 +- inventory_provider/tasks/worker.py | 5 +- test/data/router-info.json | Bin 137281293 -> 137282171 bytes test/test_flask_config.py | 11 +- test/test_junos_devices_query.py | 25 ---- test/test_worker.py | 12 +- 9 files changed, 120 insertions(+), 158 deletions(-) delete mode 100644 test/test_junos_devices_query.py diff --git a/inventory_provider/config.py b/inventory_provider/config.py index 950ad310..3fa03b9d 100644 --- a/inventory_provider/config.py +++ b/inventory_provider/config.py @@ -246,7 +246,6 @@ CONFIG_SCHEMA = { 'minItems': 1, 'items': {'type': 'integer'} }, - 'managed-routers': {'type': 'string'}, 'lab-routers': { 'type': 'object', 'properties': { @@ -276,7 +275,6 @@ CONFIG_SCHEMA = { 'redis', 'redis-databases', 'ims', - 'managed-routers', 'gws-direct', 'nren-asn-map', 'aai', @@ -289,7 +287,6 @@ CONFIG_SCHEMA = { 'sentinel', 'redis-databases', 'ims', - 'managed-routers', 'gws-direct', 'nren-asn-map', 'aai', diff --git a/inventory_provider/gap.py b/inventory_provider/gap.py index 7e2da432..2f728c31 100644 --- a/inventory_provider/gap.py +++ b/inventory_provider/gap.py @@ -1,124 +1,122 @@ import logging import requests - -from inventory_provider.tasks.config import inventory_provider_config +from flask import current_app logger = logging.getLogger(__name__) +GRANT_TYPE = 'client_credentials' +SCOPE = 'openid profile email aarc' -class Orchestrator: - GRANT_TYPE = 'client_credentials' - SCOPE = 'openid profile email aarc' - - def __init__(self): - self.config = inventory_provider_config['aai']['inventory_provider'] - self.base_url = f'{inventory_provider_config["orchestrator"]["url"]}/api/graphql' - - @staticmethod - def _get_token_endpoint() -> str: - response = requests.get(inventory_provider_config['aai']['discovery_endpoint_url']) - response.raise_for_status() - return response.json()['token_endpoint'] - - def _get_token(self) -> str: - response = requests.post( - self._get_token_endpoint(), - data={ - 'grant_type': self.GRANT_TYPE, - 'scope': self.SCOPE, - 'client_id': self.config['client_id'], - 'client_secret': self.config['secret'] - } - ) - response.raise_for_status() - return response.json()['access_token'] - - def _get_headers(self) -> dict: - token = self._get_token() - return { - 'Authorization': f'Bearer {token}' - } - def _make_request(self, url: str, body: dict) -> dict: - headers = self._get_headers() - response = requests.post(url, headers=headers, json=body) - response.raise_for_status() - return response.json() - - def _extract_router_info(self, device: dict) -> dict or None: - tag_to_key_map = { - "RTR": "router", - "OFFICE_ROUTER": "office_router", - "Super_POP_SWITCH": "super_pop_switch" - } +def get_token_endpoint(discovery_endpoint_url: str) -> str: + response = requests.get(discovery_endpoint_url) + response.raise_for_status() + return response.json()['token_endpoint'] - tag = device.get("product", {}).get("tag") - key = tag_to_key_map.get(tag) - subscription_id = device.get("subscriptionId") - - if key is None or subscription_id is None: - logger.warning(f"Skipping device with invalid tag or subscription ID: {device}") - return None - - query = f""" - query {{ - subscriptions( - filterBy: {{ field: "subscriptionId", value: "{subscription_id}" }} - ) {{ - page {{ - subscriptionId - productBlockInstances {{ - productBlockInstanceValues - }} + +def get_token(aai_config: dict) -> str: + """Get an access token using the given configuration.""" + response = requests.post( + get_token_endpoint(aai_config['discovery_endpoint_url']), + data={ + 'grant_type': GRANT_TYPE, + 'scope': SCOPE, + 'client_id': aai_config['inventory_provider']['client_id'], + 'client_secret': aai_config['inventory_provider']['secret'] + } + ) + response.raise_for_status() + return response.json()['access_token'] + + +def make_request(body: dict) -> dict: + """Make a request to the orchestrator using the given body.""" + config = current_app.config['INVENTORY_PROVIDER_CONFIG'] + api_url = f'{config["orchestrator"]["url"]}/api/graphql' + headers = {'Authorization': f'Bearer {get_token(config["aai"])}'} + response = requests.post(api_url, headers=headers, json=body) + response.raise_for_status() + return response.json() + + +def extract_router_info(device: dict) -> dict or None: + tag_to_key_map = { + "RTR": "router", + "OFFICE_ROUTER": "office_router", + "Super_POP_SWITCH": "super_pop_switch" + } + + tag = device.get("product", {}).get("tag") + key = tag_to_key_map.get(tag) + subscription_id = device.get("subscriptionId") + + if key is None or subscription_id is None: + logger.warning(f"Skipping device with invalid tag or subscription ID: {device}") + return None + + query = f""" + query {{ + subscriptions( + filterBy: {{ field: "subscriptionId", value: "{subscription_id}" }} + ) {{ + page {{ + subscriptionId + productBlockInstances {{ + productBlockInstanceValues }} }} }} - """ + }} + """ - response = self._make_request(self.base_url, body={'query': query}) - page_data = response.get('data', {}).get('subscriptions', {}).get('page') + response = make_request(body={'query': query}) + page_data = response.get('data', {}).get('subscriptions', {}).get('page') - if not page_data: - logger.warning(f"No data for subscription ID: {subscription_id}") - return None + if not page_data: + logger.warning(f"No data for subscription ID: {subscription_id}") + return None - instance_values = page_data[0].get('productBlockInstances', [{}])[0].get('productBlockInstanceValues', []) + instance_values = page_data[0].get('productBlockInstances', [{}])[0].get('productBlockInstanceValues', []) - fqdn = next((item.get('value') for item in instance_values if item.get('field') == f'{key}Fqdn'), None) - vendor = next((item.get('value') for item in instance_values if item.get('field') == 'vendor'), None) + fqdn = next((item.get('value') for item in instance_values if item.get('field') == f'{key}Fqdn'), None) + vendor = next((item.get('value') for item in instance_values if item.get('field') == 'vendor'), None) - if fqdn and vendor: - return {'fqdn': fqdn, 'vendor': vendor} - else: - logger.warning(f"Skipping device with missing FQDN or vendor: {device}") - return None + if fqdn and vendor: + return {'fqdn': fqdn, 'vendor': vendor} + else: + logger.warning(f"Skipping device with missing FQDN or vendor: {device}") + return None - def load_routers_from_orchestrator(self) -> dict: - query = """ - { - subscriptions( - filterBy: {field: "status", value: "ACTIVE"}, - query: "tag:(RTR|OFFICE_ROUTER|Super_POP_SWITCH)" - - ) { - page { - subscriptionId - product { - tag - } + +def load_routers_from_orchestrator() -> dict: + """Gets devices from the orchestrator and returns a dictionary of FQDNs and vendors.""" + + query = """ + { + subscriptions( + filterBy: {field: "status", value: "ACTIVE"}, + query: "tag:(RTR|OFFICE_ROUTER|Super_POP_SWITCH)" + ) { + page { + subscriptionId + product { + tag } } } - """ - routers = {} - response = self._make_request(self.base_url, body={'query': query}) - try: - devices = response['data']['subscriptions']['page'] - except TypeError: - devices = [] - for device in devices: - router_info = self._extract_router_info(device) - if router_info is not None: - routers[router_info['fqdn']] = router_info['vendor'] - return routers + } + """ + routers = {} + response = make_request(body={'query': query}) + + try: + devices = response['data']['subscriptions']['page'] + except (TypeError, KeyError): + devices = [] + + for device in devices: + router_info = extract_router_info(device) + if router_info is not None: + routers[router_info['fqdn']] = router_info['vendor'] + return routers diff --git a/inventory_provider/juniper.py b/inventory_provider/juniper.py index 47e1d1e3..3ca41b1c 100644 --- a/inventory_provider/juniper.py +++ b/inventory_provider/juniper.py @@ -393,21 +393,6 @@ def interface_addresses(netconf_config): "interface name": ifc['name'] } - -def load_routers_from_netdash(url): - """ - Query url for a linefeed-delimitted list of managed router hostnames. - - :param url: url of alldevices.txt file - :return: list of router hostnames - """ - r = requests.get(url=url) - r.raise_for_status() - return [ - ln.strip() for ln in r.text.splitlines() if ln.strip() - ] - - def local_interfaces( type=netifaces.AF_INET, omit_link_local=True, diff --git a/inventory_provider/routes/testing.py b/inventory_provider/routes/testing.py index 41e80603..5fcb7cce 100644 --- a/inventory_provider/routes/testing.py +++ b/inventory_provider/routes/testing.py @@ -55,7 +55,8 @@ def juniper_addresses(): routers = r.get('netdash') assert routers # sanity: value shouldn't be empty routers = json.loads(routers.decode('utf-8')) - return jsonify(routers) + juniper_routers = [k for k, v in routers.items() if v == 'juniper'] + return jsonify(juniper_routers) @routes.route("bgp/<hostname>", methods=['GET']) diff --git a/inventory_provider/tasks/worker.py b/inventory_provider/tasks/worker.py index 813af6f0..7bd89612 100644 --- a/inventory_provider/tasks/worker.py +++ b/inventory_provider/tasks/worker.py @@ -20,7 +20,6 @@ from ncclient.transport import TransportError from inventory_provider.db import ims_data from inventory_provider.db.ims import IMS -from inventory_provider.gap import Orchestrator from inventory_provider.routes.poller import load_error_report_interfaces, load_interfaces_to_poll from inventory_provider.tasks.app import app from inventory_provider.tasks.common \ @@ -28,7 +27,7 @@ from inventory_provider.tasks.common \ latch_db, get_latch, set_latch, update_latch_status, \ ims_sorted_service_type_key, set_single_latch from inventory_provider.tasks import monitor -from inventory_provider import config, nokia +from inventory_provider import config, nokia, gap from inventory_provider import environment from inventory_provider import snmp from inventory_provider import juniper @@ -530,7 +529,7 @@ def retrieve_and_persist_neteng_managed_device_list( netdash_equipment = None try: info_callback('querying netdash for managed routers') - netdash_equipment = Orchestrator().load_routers_from_orchestrator() + netdash_equipment = gap.load_routers_from_orchestrator() except Exception as e: warning_callback(f'Error retrieving device list: {e}') diff --git a/test/data/router-info.json b/test/data/router-info.json index c5d6758fdbe6dd4be250a18fc25afa3c04386104..5352029a65d23d054d1e445be633de72732417e2 100644 GIT binary patch delta 8147 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%0DmVt=n zAYuiGSP3Flfr!;0VhxB`3nJEmi1i?11BloNA~u1D%^+e6h}a4uwt<N4AYuoI*a;$b zfr#B8Vh@Pe3nKP`i2Weq0EjpUA`XFw!yw`ah&T!&j)92dAmRjwI0+(7fr!%};tYs5 z3nI>ei1Q%g0*JT>A})c5%OK(kh`0(Ou7QZ_AmRpyxCtU|fr#56;tq(o3nK1;i2ESo z0f=}AA|8Q=#~|Vfh<FMjo`HzxAmRmxcnKoFCDLmU>kWu_3nJcui1#4k1BmztB0hnL z&miIpi1-R3zJZAEAmRsz_z5C@fr#HA;tz=U3nKo3i2v-}iLW?7#UF@Z0ujt0f(1md zf(SMc!44ugKm;d<-~ticAc6-(@PY_F5Wx>31VDr!h!6r1!XQEfM2Lb2F%TgRA|ybB zB#4j#5z-(+21Lk$2sscT4<Zymgd&Jg0ujm}LIp&qf(SJbp$;N6K!hfU&;k+KAVP;@ Lf8r}$jx|>Reaw(j delta 7387 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 diff --git a/test/test_flask_config.py b/test/test_flask_config.py index 262b2a84..7ba83ec5 100644 --- a/test/test_flask_config.py +++ b/test/test_flask_config.py @@ -27,9 +27,18 @@ def config(): 'username': 'ims_username', 'password': 'ims_password', }, - 'managed-routers': 'router_list', 'gws-direct': [], 'nren-asn-map': [], + 'aai': { + 'discovery_endpoint_url': 'https://smaple.discovery.endpoint', + 'inventory_provider': { + 'client_id': 'sample-client-id', + 'secret': 'sample-secret' + } + }, + 'orchestrator': { + 'url': 'https://orchestrator.url' + } } diff --git a/test/test_junos_devices_query.py b/test/test_junos_devices_query.py deleted file mode 100644 index b1ef7ee2..00000000 --- a/test/test_junos_devices_query.py +++ /dev/null @@ -1,25 +0,0 @@ -import os - -import responses - -from inventory_provider import juniper - -TEST_DATA_FILENAME = os.path.realpath(os.path.join( - os.path.dirname(__file__), - 'data', - 'netdash-alldevices.txt')) - - -@responses.activate -def test_junosspace_devices_parsing(data_config): - data_config['managed-routers'] = 'http://sabababa' - with open(TEST_DATA_FILENAME) as f: - responses.add( - method=responses.GET, - url=data_config['managed-routers'], - body=f.read()) - - hostnames = juniper.load_routers_from_netdash( - data_config['managed-routers']) - - assert 'mx1.ams.nl.geant.net' in hostnames diff --git a/test/test_worker.py b/test/test_worker.py index dcc32853..d8628ff4 100644 --- a/test/test_worker.py +++ b/test/test_worker.py @@ -637,19 +637,16 @@ def test_persist_ims_data(mocker, data_config, mocked_redis): def test_retrieve_and_persist_neteng_managed_device_list( mocker, data_config, mocked_redis): - device_list = ['abc', 'def'] + device_list = [{'abc': 'juniper'}, {'def': 'nokia'}] r = common._get_redis(data_config) mocker.patch( 'inventory_provider.tasks.worker.InventoryTask.config' ) - mocker.patch('inventory_provider.tasks.worker.get_next_redis', - return_value=r) + mocker.patch('inventory_provider.tasks.worker.get_next_redis', return_value=r) r.delete('netdash') - mocked_j = mocker.patch( - 'inventory_provider.tasks.worker.juniper.load_routers_from_netdash' - ) - mocked_j.return_value = device_list + mocker.patch('inventory_provider.tasks.worker.get_current_redis', return_value=r) + mocker.patch('inventory_provider.gap.load_routers_from_orchestrator', return_value=device_list) result = retrieve_and_persist_neteng_managed_device_list() assert result == device_list assert json.loads(r.get('netdash')) == device_list @@ -1020,6 +1017,7 @@ def test_populate_error_report_interfaces_cache(mocker, data_config, mocked_redi mocker.patch( 'inventory_provider.tasks.worker.InventoryTask.config' ) + mocker.patch('inventory_provider.routes.poller.load_error_report_interfaces.get_vendor', return_value='juniper') exp_router_a_interfaces = [ { "router": "router_a.geant.net", -- GitLab