diff --git a/inventory_provider/config.py b/inventory_provider/config.py index d62dbb82e23c4277824a1c2e19d74f10d765f1e7..950ad31076a818bf869949f58a2a1c84c562b0f1 100644 --- a/inventory_provider/config.py +++ b/inventory_provider/config.py @@ -201,13 +201,36 @@ CONFIG_SCHEMA = { 'items': { 'type': 'object', 'properties': { - 'nren': {'type': 'string'}, - 'asn': {'type': 'integer'} + 'nren': {'type': 'string'}, + 'asn': {'type': 'integer'} }, 'required': ['nren', 'asn'], 'additionalProperties': False }, - } + }, + 'aai': { + 'type': 'object', + 'properties': { + 'discovery_endpoint_url': {'type': 'string'}, + 'inventory_provider': {'type': 'object', + 'properties': { + 'client_id': {'type': 'string'}, + 'secret': {'type': 'string'} + }, + 'required': ['client_id', 'secret'], + 'additionalProperties': False} + }, + 'required': ['discovery_endpoint_url', 'inventory_provider'], + 'additionalProperties': False + }, + 'orchestrator': { + 'type': 'object', + 'properties': { + 'url': {'type': 'string'}, + }, + 'required': ['url'], + 'additionalProperties': False + }, }, 'type': 'object', @@ -225,8 +248,13 @@ CONFIG_SCHEMA = { }, 'managed-routers': {'type': 'string'}, 'lab-routers': { - 'type': 'array', - 'items': {'type': 'string'} + 'type': 'object', + 'properties': { + '^[a-zA-Z0-9.-]+$': { + 'type': 'string', + 'enum': ['nokia', 'juniper'] + } + } }, 'unmanaged-interfaces': { 'type': 'array', @@ -237,7 +265,8 @@ CONFIG_SCHEMA = { 'nokia-community-inventory-provider': {'type': 'string'}, 'nokia-community-dashboard': {'type': 'string'}, 'nokia-community-brian': {'type': 'string'}, - + 'aai': {'$ref': '#/definitions/aai'}, + 'orchestrator': {'$ref': '#/definitions/orchestrator'} }, 'oneOf': [ { @@ -249,7 +278,9 @@ CONFIG_SCHEMA = { 'ims', 'managed-routers', 'gws-direct', - 'nren-asn-map'] + 'nren-asn-map', + 'aai', + 'orchestrator'] }, { 'required': [ @@ -260,7 +291,9 @@ CONFIG_SCHEMA = { 'ims', 'managed-routers', 'gws-direct', - 'nren-asn-map'] + 'nren-asn-map', + 'aai', + 'orchestrator'] } ], 'additionalProperties': False diff --git a/inventory_provider/gap.py b/inventory_provider/gap.py new file mode 100644 index 0000000000000000000000000000000000000000..7e2da432682c58a280c5bf6e66f3f707f5ff0499 --- /dev/null +++ b/inventory_provider/gap.py @@ -0,0 +1,124 @@ +import logging + +import requests + +from inventory_provider.tasks.config import inventory_provider_config + +logger = logging.getLogger(__name__) + + +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" + } + + 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') + + 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', []) + + 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 + + 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 + } + } + } + } + """ + 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 diff --git a/inventory_provider/routes/poller.py b/inventory_provider/routes/poller.py index 45634297e78c08effbb9aafa804da806816d4681..4de3d461ecd74470ebe57394e6951c5269aa6f5d 100644 --- a/inventory_provider/routes/poller.py +++ b/inventory_provider/routes/poller.py @@ -932,14 +932,17 @@ def load_error_report_interfaces( ) ) - def transform_interface(interface: dict): + def get_vendor(router: str) -> str | None: + current_redis = common.get_current_redis() + netdash_equipment = json.loads(current_redis.get('netdash').decode('utf-8')) + return netdash_equipment.get(router) + + def transform_interface(interface: dict) -> dict: return { "router": interface["router"], "name": interface["name"], "description": interface["description"], - # TODO: This is a complete hack until we have a proper way to determine - # router vendor - "vendor": "nokia" if interface["router"].startswith("rt0") else "juniper" + "vendor": get_vendor(interface["router"]), } return sorted( diff --git a/inventory_provider/tasks/worker.py b/inventory_provider/tasks/worker.py index 82c0699cd5b44e5453e3cec646a776e5f92fcb1f..813af6f0e39db275872535697e318e6aa3a89207 100644 --- a/inventory_provider/tasks/worker.py +++ b/inventory_provider/tasks/worker.py @@ -20,6 +20,7 @@ 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 \ @@ -454,14 +455,6 @@ def check_task_status(task_id, parent=None, forget=False): yield result - -def get_router_vendors(): - c = InventoryTask.config["ims"] - ds = \ - IMS(c['api'], c['username'], c['password'], c.get('verify-ssl', False)) - return {r.lower(): v.lower() for r, v in ims_data.get_router_vendors(ds)} - - @app.task(base=InventoryTask, bind=True, name='update_entry_point') @log_task_entry_and_exit def update_entry_point(self): @@ -476,52 +469,37 @@ def update_entry_point(self): warning_callback=self.log_warning ) lab_routers = InventoryTask.config.get('lab-routers', []) - - ims_rv = get_router_vendors() - - def _get_router_vendor(router): - return ims_rv.get(router.lower().split('.geant.')[0], 'unknown') - - def _get_lab_router_vendor(router): - _rv = ims_rv.get(router.lower().split('.geant.')[0]) - if not _rv: - _rv = ims_rv.get(router.lower().split('.office.')[0], - 'unknown') - return _rv - - rv = {r: _get_router_vendor(r) for r in routers} - lab_rv = {r: _get_lab_router_vendor(r) for r in lab_routers} chord( ( ims_task.s().on_error(task_error_handler.s()), chord( - (reload_router_config_juniper.s(r) for r, v in rv.items() + (reload_router_config_juniper.s(r) for r, v in routers.items() if v == 'juniper'), empty_task.si('juniper router tasks complete') ), chord( (reload_lab_router_config_juniper.s(r) - for r, v in lab_rv.items() if v == 'juniper'), + for r, v in lab_routers.items() if v == 'juniper'), empty_task.si('juniper lab router tasks complete') ), chord( - (reload_router_config_nokia.s(r) for r, v in rv.items() + (reload_router_config_nokia.s(r) for r, v in routers.items() if v == 'nokia'), empty_task.si('nokia router tasks complete') ), chord( (reload_router_config_nokia.s(r, True) - for r, v in lab_rv.items() if v == 'nokia'), + for r, v in lab_routers.items() if v == 'nokia'), empty_task.si('nokia lab router tasks complete') ), chord( - (reload_router_config_try_all.s(r) for r, v in rv.items() + (reload_router_config_try_all.s(r) for r, v in routers.items() if v == 'unknown'), empty_task.si('unknown router tasks complete') ), chord( (reload_router_config_try_all.s(r, True) - for r, v in lab_rv.items() if v == 'unknown'), + for r, v in lab_routers.items() if v == 'unknown'), empty_task.si('unknown lab router tasks complete') ) ), @@ -552,8 +530,7 @@ def retrieve_and_persist_neteng_managed_device_list( netdash_equipment = None try: info_callback('querying netdash for managed routers') - netdash_equipment = list(juniper.load_routers_from_netdash( - InventoryTask.config['managed-routers'])) + netdash_equipment = Orchestrator().load_routers_from_orchestrator() except Exception as e: warning_callback(f'Error retrieving device list: {e}') diff --git a/test/conftest.py b/test/conftest.py index 47a350147bfd1b244e5742b15db437e891cb2c3b..bd976e63f94394dc9e9207c0583ac5476c72aa95 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -69,7 +69,6 @@ def data_config_filename(): "username": "ims_username", "password": "ims_password" }, - "managed-routers": "bogus url", "unmanaged-interfaces": [ { "address": "99.99.99.99", @@ -102,7 +101,17 @@ def data_config_filename(): "nren": "BAZ", "asn": 1853 } - ] + ], + "aai": { + "discovery_endpoint_url": "https://smaple.discovery.endpoint", + "inventory_provider": { + "client_id": "sample-client-id", + "secret": "sample-secret" + } + }, + "orchestrator": { + "url": "https://orchestrator.url" + } } config['gws-direct'] = read_json_test_data('gws-direct.json')