diff --git a/inventory_provider/gap.py b/inventory_provider/gap.py index 608ca116341ebbd7b3cb11ec6ec22388f0071a5b..35ecd0200956d1b216deb9c0812dbf012f31c0ee 100644 --- a/inventory_provider/gap.py +++ b/inventory_provider/gap.py @@ -3,7 +3,6 @@ import logging import socket import requests -from flask import current_app from requests.adapters import HTTPAdapter from urllib3.poolmanager import PoolManager @@ -14,7 +13,9 @@ SCOPE = 'openid profile email aarc' class IPv4Adapter(HTTPAdapter): - """A custom adapter that forces the use of IPv4.""" + """A custom adapter that forces the use of IPv4. + The reason for this is that the orchestrator does not support IPv6. We use this adapter to force the use of IPv4 as + a temporary workaround. This adapter should be removed once the orchestrator supports IPv6.""" def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs): pool_kwargs['socket_options'] = [(socket.IPPROTO_IP, socket.IP_TOS, 0)] @@ -51,22 +52,28 @@ def get_token(aai_config: dict) -> str: return response.json()['access_token'] -def make_request(body: dict, token: str) -> dict: +def make_request(body: dict, token: str, app_config: 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' + api_url = f'{app_config["orchestrator"]["url"]}/api/graphql' headers = {'Authorization': f'Bearer {token}'} session = requests.Session() # Mount the adapter to force IPv4 + # This should be removed once the orchestrator supports IPv6 + # See the docstring of the IPv4Adapter class for more info adapter = IPv4Adapter() session.mount('http://', adapter) session.mount('https://', adapter) response = session.post(api_url, headers=headers, json=body) response.raise_for_status() + # The graphql API returns a 200 status code even if there are errors in the response + if errors := response.json().get('errors'): + err_msg = f'GraphQL query returned errors: {errors}' + logger.error(err_msg) + raise ValueError(err_msg) return response.json() -def extract_router_info(device: dict, token: str) -> dict or None: +def extract_router_info(device: dict, token: str, app_config: dict) -> dict or None: tag_to_key_map = { "RTR": "router", "OFFICE_ROUTER": "officeRouter", @@ -96,7 +103,7 @@ def extract_router_info(device: dict, token: str) -> dict or None: }} """ - response = make_request(body={'query': query}, token=token) + response = make_request(body={'query': query}, token=token, app_config=app_config) page_data = response.get('data', {}).get('subscriptions', {}).get('page') if not page_data: @@ -115,9 +122,9 @@ def extract_router_info(device: dict, token: str) -> dict or None: return None -def load_routers_from_orchestrator() -> dict: +def load_routers_from_orchestrator(app_config: dict) -> dict: """Gets devices from the orchestrator and returns a dictionary of FQDNs and vendors.""" - token = get_token(current_app.config['INVENTORY_PROVIDER_CONFIG']['aai']) + token = get_token(app_config['aai']) query = """ { subscriptions( @@ -135,14 +142,14 @@ def load_routers_from_orchestrator() -> dict: } """ routers = {} - response = make_request(body={'query': query}, token=token) + response = make_request(body={'query': query}, token=token, app_config=app_config) try: devices = response['data']['subscriptions']['page'] except (TypeError, KeyError): devices = [] with concurrent.futures.ThreadPoolExecutor() as executor: - futures = [executor.submit(extract_router_info, device, token) for device in devices] + futures = [executor.submit(extract_router_info, device, token, app_config) for device in devices] for future in concurrent.futures.as_completed(futures): router_info = future.result() if router_info is not None: diff --git a/inventory_provider/tasks/worker.py b/inventory_provider/tasks/worker.py index aea2a32a96e14c0412a963ea55fdb71b32fc4b3b..864dd803d4ce889d0659395e14c8cee2b6f5edcd 100644 --- a/inventory_provider/tasks/worker.py +++ b/inventory_provider/tasks/worker.py @@ -530,7 +530,7 @@ def retrieve_and_persist_neteng_managed_device_list( netdash_equipment = None try: info_callback('querying netdash for managed routers') - netdash_equipment = gap.load_routers_from_orchestrator() + netdash_equipment = gap.load_routers_from_orchestrator(InventoryTask.config) except Exception as e: warning_callback(f'Error retrieving device list: {e}') @@ -2068,4 +2068,4 @@ def collate_netconf_interfaces_all_cache(warning_callback=lambda s: None): "Failed to collate netconf-interfaces, logging exception") r.set(netconf_all_key, json.dumps(netconf_interface_docs)) - r.set(lab_netconf_all_key, json.dumps(lab_netconf_interface_docs)) + r.set(lab_netconf_all_key, json.dumps(lab_netconf_interface_docs)) \ No newline at end of file diff --git a/test/test_gap.py b/test/test_gap.py index c2a60279c3b66b0dd9d72b9a8eab49e48f3aca42..6801fa07fe1d213aae60ec776b63ed7d7ea4c6b2 100644 --- a/test/test_gap.py +++ b/test/test_gap.py @@ -3,79 +3,95 @@ from unittest.mock import patch, MagicMock from inventory_provider import gap -def test_get_token_endpoint(): - with patch('inventory_provider.gap.requests.get') as mock_get: - mock_response = MagicMock() - mock_response.json.return_value = {'token_endpoint': 'http://example.com/token'} - mock_response.raise_for_status = MagicMock() - mock_get.return_value = mock_response +def test_get_token_endpoint(mocker): + mock_response = MagicMock() + mock_response.json.return_value = {'token_endpoint': 'http://example.com/token'} + mock_response.raise_for_status = MagicMock() + mocker.patch('inventory_provider.gap.requests.get', return_value=mock_response) - discovery_endpoint_url = 'http://example.com/aai' - token_endpoint = gap.get_token_endpoint(discovery_endpoint_url) - assert token_endpoint == 'http://example.com/token' + discovery_endpoint_url = 'http://example.com/aai' + token_endpoint = gap.get_token_endpoint(discovery_endpoint_url) + assert token_endpoint == 'http://example.com/token' -def test_get_token(): - with patch('inventory_provider.gap.requests.post') as mock_post, patch( - 'inventory_provider.gap.get_token_endpoint') as mock_get_token_endpoint: - mock_get_token_endpoint.return_value = 'http://example.com/token' - mock_response = MagicMock() - mock_response.json.return_value = {'access_token': 'test_token'} - mock_response.raise_for_status = MagicMock() - mock_post.return_value = mock_response +def test_get_token(mocker): + mocker.patch('inventory_provider.gap.get_token_endpoint', return_value='http://example.com/token') + mock_response = MagicMock() + mock_response.json.return_value = {'access_token': 'test_token'} + mock_response.raise_for_status = MagicMock() + mocker.patch('inventory_provider.gap.requests.post', return_value=mock_response) - aai_config = { - 'discovery_endpoint_url': 'http://example.com/aai', - 'inventory_provider': {'client_id': 'test_id', 'secret': 'test_secret'} - } - token = gap.get_token(aai_config) - assert token == 'test_token' + aai_config = { + 'discovery_endpoint_url': 'http://example.com/aai', + 'inventory_provider': {'client_id': 'test_id', 'secret': 'test_secret'} + } + token = gap.get_token(aai_config) + assert token == 'test_token' -def test_extract_router_info(): +def test_extract_router_info(mocker): device = { 'product': {'tag': 'RTR'}, 'subscriptionId': 'test_subscription_id' } - with patch('inventory_provider.gap.make_request') as mock_make_request: - mock_response = { - 'data': { - 'subscriptions': { - 'page': [{ - 'productBlockInstances': [{ - 'productBlockInstanceValues': [ - {'field': 'routerFqdn', 'value': 'test_fqdn'}, - {'field': 'vendor', 'value': 'test_vendor'} - ] - }] + mocker.patch('inventory_provider.gap.make_request', return_value={ + 'data': { + 'subscriptions': { + 'page': [{ + 'productBlockInstances': [{ + 'productBlockInstanceValues': [ + {'field': 'routerFqdn', 'value': 'test_fqdn'}, + {'field': 'vendor', 'value': 'test_vendor'} + ] }] - } + }] } } - mock_make_request.return_value = mock_response - - router_info = gap.extract_router_info(device) - assert router_info == {'fqdn': 'test_fqdn', 'vendor': 'test_vendor'} + }) + config = { + "aai": { + "discovery_endpoint_url": "https://sample-url", + "inventory_provider": { + "client_id": "test_id", + "secret": "test_secret" + } + }, + "orchestrator": { + "url": "orchestrator-url" + } + } + router_info = gap.extract_router_info(device, 'test_token', config) + assert router_info == {'fqdn': 'test_fqdn', 'vendor': 'test_vendor'} -def test_load_routers_from_orchestrator(): - with patch('inventory_provider.gap.make_request') as mock_make_request, patch( - 'inventory_provider.gap.extract_router_info') as mock_extract_router_info: - mock_response = { - 'data': { - 'subscriptions': { - 'page': [ - {'subscriptionId': '1', 'product': {'tag': 'RTR'}}, - {'subscriptionId': '2', 'product': {'tag': 'OFFICE_ROUTER'}} - ] - } +def test_load_routers_from_orchestrator(mocker): + mocker.patch('inventory_provider.gap.make_request', { + 'data': { + 'subscriptions': { + 'page': [ + {'subscriptionId': '1', 'product': {'tag': 'RTR'}}, + {'subscriptionId': '2', 'product': {'tag': 'OFFICE_ROUTER'}} + ] + } + } + }) + mocker.patch('inventory_provider.gap.extract_router_info', return_value=[ + {'fqdn': 'fqdn1', 'vendor': 'vendor1'}, + {'fqdn': 'fqdn2', 'vendor': 'vendor2'} + ]) + mocker.patch('inventory_provider.gap.get_token', return_value='test_token') + config = { + "aai": { + "discovery_endpoint_url": "https://sample-url", + "inventory_provider": { + "client_id": "test_id", + "secret": "test_secret" } + }, + "orchestrator": { + "url": "orchestrator-url" } - mock_make_request.return_value = mock_response - mock_extract_router_info.side_effect = [ - {'fqdn': 'fqdn1', 'vendor': 'vendor1'}, - {'fqdn': 'fqdn2', 'vendor': 'vendor2'} - ] + } - routers = gap.load_routers_from_orchestrator() - assert routers == {'fqdn1': 'vendor1', 'fqdn2': 'vendor2'} + routers = gap.load_routers_from_orchestrator(config) + assert routers == {'fqdn1': 'vendor1', 'fqdn2': 'vendor2'}