diff --git a/Changelog.md b/Changelog.md index 77862a31572c8463decb38e79c2c162ca0cf821c..6d142aa67cc2a5cfdb5b1b6f876e2439831b422c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,9 @@ All notable changes to this project will be documented in this file. +## [0.101] - 2023-03-28 +- DBOARD3-713: MIC endpoint - only include services that are monitored in Geant NMS + ## [0.100] - 2022-12-07 - POL1-646: Changed BRIAN interface description parsing to expect whitespace - POL1-643: Added port_type field to interfaces to distinguish access/service diff --git a/README.md b/README.md index e8216dd6425d784fec8fd47e769f0ce8452ed0ce..69a4990dbe570d66142a8fd7c5976bc7258b03f6 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ -# Inventory Provider - -The Inventory Provider -acts as a single point of truth for -information about the GÉANT network, exposed by -by an HTTP API. - -Documentation can be generated by running sphinx: - -```bash -sphinx-build -M html docs/source docs/build -``` - -The documents should be viewable in the -workspace of the most recent [Jenkins job](https://jenkins.geant.org/job/inventory-provider%20(develop)/ws/docs/build/html/index.html). \ No newline at end of file +# Inventory Provider + +The Inventory Provider +acts as a single point of truth for +information about the GÉANT network, exposed by +by an HTTP API. + +Documentation can be generated by running sphinx in the commandline: + +```bash +sphinx-build -M html docs/source docs/build +``` + +The documents should be viewable in the +workspace of the most recent [Jenkins job](https://jenkins.geant.org/job/inventory-provider%20(develop)/ws/docs/build/html/index.html). diff --git a/inventory_provider/db/ims.py b/inventory_provider/db/ims.py index d7b4eebd7297576c0936b5b2461bfca3117702f8..07aa68c7f759e735fbbb7bdb78716e90421613c0 100644 --- a/inventory_provider/db/ims.py +++ b/inventory_provider/db/ims.py @@ -1,9 +1,11 @@ +from enum import Enum +import functools import logging +import time import requests -import time -from enum import Enum +from inventory_provider import environment # Navigation Properties @@ -11,6 +13,9 @@ from enum import Enum from requests import HTTPError logger = logging.getLogger(__name__) +log_entry_and_exit = functools.partial( + environment.log_entry_and_exit, logger=logger) + # http://149.210.162.190:81/ImsVersions/21.9/html/50e6a1b1-3910-2091-63d5-e13777b2194e.htm # noqa CIRCUIT_CUSTOMER_RELATION = { @@ -235,7 +240,7 @@ class IMS(object): response.raise_for_status() break - def _get_entity( + def _get_ims_data( self, url, params=None, @@ -317,6 +322,7 @@ class IMS(object): IMS.cache[cache_key] = return_value return return_value + @log_entry_and_exit def get_entity_by_id( self, entity_type, @@ -326,8 +332,9 @@ class IMS(object): url = f'{entity_type}/{entity_id}' return \ - self._get_entity(url, None, navigation_properties, use_cache) + self._get_ims_data(url, None, navigation_properties, use_cache) + @log_entry_and_exit def get_entity_by_name( self, entity, @@ -335,8 +342,9 @@ class IMS(object): navigation_properties=None, use_cache=False): url = f'{entity}/byname/"{name}"' - return self._get_entity(url, None, navigation_properties, use_cache) + return self._get_ims_data(url, None, navigation_properties, use_cache) + @log_entry_and_exit def get_filtered_entities( self, entity, @@ -344,17 +352,31 @@ class IMS(object): navigation_properties=None, use_cache=False, step_count=50): + url = f'{entity}/filtered/{filter_string}' + yield from self.get_entities( + url, + navigation_properties, + use_cache, + step_count + ) + + def get_entities( + self, + url, + navigation_properties=None, + use_cache=False, + step_count=50): more_to_come = True start_entity = 0 + gateway_error_count = 0 while more_to_come: params = { 'paginatorStartElement': start_entity, 'paginatorNumberOfElements': step_count } - url = f'{entity}/filtered/{filter_string}' try: more_to_come = False - entities = self._get_entity( + entities = self._get_ims_data( url, params, navigation_properties, @@ -364,6 +386,17 @@ class IMS(object): if r.status_code == requests.codes.not_found \ and NO_FILTERED_RESULTS_MESSAGE in r.text.lower(): entities = None + elif r.status_code == 504: + gateway_error_count += 1 + logger.debug( + f"GATEWAY TIME-OUT for {url}" + f" -- COUNT: {gateway_error_count}") + if gateway_error_count > 4: + raise e + time.sleep(5) + logger.debug("WAKING UP") + more_to_come = True + continue else: raise e if entities: @@ -372,6 +405,7 @@ class IMS(object): start_entity += step_count yield from entities + @log_entry_and_exit def get_all_entities( self, entity, @@ -379,9 +413,9 @@ class IMS(object): use_cache=False, step_count=50 ): - yield from self.get_filtered_entities( - entity, - 'Id <> 0', + url = f'{entity}/all' + yield from self.get_entities( + url, navigation_properties, use_cache, step_count diff --git a/inventory_provider/db/ims_data.py b/inventory_provider/db/ims_data.py index 371e2f2cfc537d4509bc75d1f8a1222c4493bf9e..81cf11b7e430398537410e29ec27c2e085f871b6 100644 --- a/inventory_provider/db/ims_data.py +++ b/inventory_provider/db/ims_data.py @@ -1,3 +1,4 @@ +import functools import logging import re from collections import defaultdict @@ -9,10 +10,9 @@ from inventory_provider.db import ims from inventory_provider.db.ims import InventoryStatus, IMS, \ CUSTOMER_RELATED_CONTACT_PROPERTIES, EXTRA_FIELD_VALUE_PROPERTIES -environment.setup_logging() logger = logging.getLogger(__name__) - -# Dashboard V3 +log_entry_and_exit = functools.partial( + environment.log_entry_and_exit, logger=logger) IMS_OPSDB_STATUS_MAP = { InventoryStatus.PLANNED: 'planned', @@ -83,6 +83,7 @@ def get_flexils_by_circuitid(ds: IMS): return dict(by_circuit) +@log_entry_and_exit def get_non_monitored_circuit_ids(ds: IMS): # note the id for the relevant field is hard-coded. I didn't want to use # the name of the field as this can be changed by users @@ -94,6 +95,7 @@ def get_non_monitored_circuit_ids(ds: IMS): yield d['extrafieldvalueobjectinfo']['objectid'] +@log_entry_and_exit def get_monitored_circuit_ids(ds: IMS): # note the id for the relevant field is hard-coded. I didn't want to use # the name of the field as this can be changed by users @@ -106,6 +108,7 @@ def get_monitored_circuit_ids(ds: IMS): yield d['extrafieldvalueobjectinfo']['objectid'] +@log_entry_and_exit def get_ids_and_sids(ds: IMS): for sid_circuit in ds.get_filtered_entities( 'ExtraFieldValue', @@ -115,6 +118,7 @@ def get_ids_and_sids(ds: IMS): yield sid_circuit['objectid'], sid_circuit['value'] +@log_entry_and_exit def get_service_types(ds: IMS): for d in ds.get_filtered_entities( 'ComboBoxData', @@ -122,6 +126,7 @@ def get_service_types(ds: IMS): yield d['selection'] +@log_entry_and_exit def get_customer_tts_contacts(ds: IMS): customer_contacts = defaultdict(set) @@ -137,6 +142,7 @@ def get_customer_tts_contacts(ds: IMS): yield k, sorted(list(v)) +@log_entry_and_exit def get_customer_planned_work_contacts(ds: IMS): customer_contacts = defaultdict(set) @@ -152,6 +158,7 @@ def get_customer_planned_work_contacts(ds: IMS): yield k, sorted(list(v)) +@log_entry_and_exit def get_circuit_related_customers(ds: IMS): return_value = defaultdict(list) @@ -169,6 +176,7 @@ def get_circuit_related_customers(ds: IMS): return return_value +@log_entry_and_exit def get_port_id_services(ds: IMS): circuit_nav_props = [ ims.CIRCUIT_PROPERTIES['Ports'], @@ -300,6 +308,7 @@ def get_port_id_services(ds: IMS): } +@log_entry_and_exit def get_port_sids(ds: IMS): """ This function fetches SIDs for external ports that have them defined, @@ -313,6 +322,7 @@ def get_port_sids(ds: IMS): step_count=10000)} +@log_entry_and_exit def get_internal_port_sids(ds: IMS): """ This function fetches SIDs for external ports that have them defined, @@ -326,6 +336,7 @@ def get_internal_port_sids(ds: IMS): step_count=10000)} +@log_entry_and_exit def get_port_details(ds: IMS): port_nav_props = [ ims.PORT_PROPERTIES['Node'], @@ -381,6 +392,7 @@ def get_port_details(ds: IMS): 'internalport', internal_port_nav_props, step_count=2000), 'internal') +@log_entry_and_exit def get_circuit_hierarchy(ds: IMS): circuit_nav_props = [ ims.CIRCUIT_PROPERTIES['Customer'], @@ -425,6 +437,7 @@ def get_circuit_hierarchy(ds: IMS): } +@log_entry_and_exit def get_node_locations(ds: IMS): """ return location info for all Site nodes @@ -498,6 +511,7 @@ INTERNAL_POP_NAMES = { } +@log_entry_and_exit def lookup_lg_routers(ds: IMS): pattern = re.compile("vpn-proxy|vrr|taas", re.IGNORECASE) @@ -562,6 +576,7 @@ def lookup_lg_routers(ds: IMS): yield eq +@log_entry_and_exit def lookup_geant_nodes(ds: IMS): return (n["name"]for n in ds.get_filtered_entities( diff --git a/inventory_provider/environment.py b/inventory_provider/environment.py index 989c0a1355ebb7b0e44f110842b25f045868a47d..ea0c5bff839abe503bf24c6b60d3e79d6c0172f4 100644 --- a/inventory_provider/environment.py +++ b/inventory_provider/environment.py @@ -1,6 +1,24 @@ +import functools import json import logging.config import os +import time + + +def log_entry_and_exit(f, logger): + # cf. https://stackoverflow.com/a/47663642 + # cf. https://stackoverflow.com/a/59098957/6708581 + @functools.wraps(f) + def _w(*args, **kwargs): + logger.debug(f'>>> {f.__name__}{args}') + start_time = time.time() + try: + return f(*args, **kwargs) + finally: + end_time = time.time() + duration = end_time - start_time + logger.debug(f'<<< {f.__name__}{args} -- duration {duration}') + return _w def setup_logging(): diff --git a/inventory_provider/routes/mic.py b/inventory_provider/routes/mic.py index 43484ef028e2053da82566e7d42fc9537586ea85..a335ae3eb8ee176bba4de1bf93c3f9153d3683e0 100644 --- a/inventory_provider/routes/mic.py +++ b/inventory_provider/routes/mic.py @@ -222,7 +222,9 @@ def get_everything(): site = f'{d["pop_name"]} ({d["pop_abbreviation"]})' eq_name = d['equipment'] if_name = d['port'] - all_data[site][eq_name][if_name] = d['related-services'] + all_data[site][eq_name][if_name] = \ + [rs for rs in d['related-services'] + if rs['status'] == 'operational'] result = json.dumps(all_data) r.set(cache_key, result.encode('utf-8')) diff --git a/inventory_provider/tasks/worker.py b/inventory_provider/tasks/worker.py index 2a58d54adeb272fc831e7c5621f0398091c30131..f9b1e36b0ece9857c254da5a4c14e628fda41bb9 100644 --- a/inventory_provider/tasks/worker.py +++ b/inventory_provider/tasks/worker.py @@ -4,7 +4,6 @@ import json import logging import os import re -import time from typing import List from celery import Task, states, chord @@ -36,24 +35,9 @@ FINALIZER_TIMEOUT_S = 300 # TODO: error callback (cf. http://docs.celeryproject.org/en/latest/userguide/calling.html#linking-callbacks-errbacks) # noqa: E501 environment.setup_logging() - logger = logging.getLogger(__name__) - - -def log_task_entry_and_exit(f): - # cf. https://stackoverflow.com/a/47663642 - # cf. https://stackoverflow.com/a/59098957/6708581 - @functools.wraps(f) - def _w(*args, **kwargs): - logger.debug(f'>>> {f.__name__}{args}') - start_time = time.time() - try: - return f(*args, **kwargs) - finally: - end_time = time.time() - duration = end_time - start_time - logger.debug(f'<<< {f.__name__}{args} -- duration {duration}') - return _w +log_task_entry_and_exit = functools.partial( + environment.log_entry_and_exit, logger=logger) class InventoryTaskError(Exception): @@ -863,7 +847,8 @@ def _extract_ims_data(ims_api_url, ims_username, ims_password): executor.submit(_populate_hierarchy): 'hierarchy', executor.submit(_populate_port_id_details): 'port_id_details', executor.submit(_populate_circuit_info): 'circuit_info', - executor.submit(_populate_flexils_data): 'flexils_data' + executor.submit(_populate_flexils_data): 'flexils_data', + executor.submit(_populate_customers): 'customers' } for future in concurrent.futures.as_completed(futures): @@ -1105,8 +1090,13 @@ def transform_ims_data(data): for tlc in circ['related-services']: # why were these removed? # contacts.update(tlc.pop('contacts')) - contacts.update(tlc.get('contacts')) - pw_contacts.update(tlc.get('planned_work_contacts', [])) + if circ['status'] == 'operational' \ + and circ['id'] in circuit_ids_to_monitor \ + and tlc['status'] == 'operational' \ + and tlc['id'] in circuit_ids_to_monitor: + contacts.update(tlc.get('contacts')) + pw_contacts.update( + tlc.get('planned_work_contacts', [])) circ['contacts'] = sorted(list(contacts)) circ['planned_work_contacts'] = sorted(list(pw_contacts)) diff --git a/setup.py b/setup.py index 2dd9d8101f8408a6febaf8937a60c4f3b0997219..0cc25fe475947f1c97f1efc86dcfe6f036e76679 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name='inventory-provider', - version="0.100", + version="0.101", author='GEANT', author_email='swd@geant.org', description='Dashboard inventory provider', diff --git a/test/test_ims.py b/test/test_ims.py index 23f3066007be08ab543d51192e8000a4f66c2801..fc8c1c219d755659fe09fbd23a06659d5fa027d6 100644 --- a/test/test_ims.py +++ b/test/test_ims.py @@ -139,7 +139,7 @@ def test_ims_class_get_all_entities(mocker): list(ds.get_all_entities('Node', step_count=10)) mock_get.assert_called_once_with( - 'dummy_base/ims/Node/filtered/Id <> 0', + 'dummy_base/ims/Node/all', headers={'Authorization': 'Bearer dummy_bt'}, params={ 'paginatorStartElement': 0, diff --git a/tox.ini b/tox.ini index 88bf0caa1b18a625125d67275de52f68333f0f17..86a7ce2a3e4293a0f2b668507c7850acee255ae4 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = py36 exclude = venv,.tox,build [testenv] -passenv = TEST_OPSDB_HOSTNAME TEST_OPSDB_DBNAME TEST_OPSDB_USERNAME TEST_OPSDB_PASSWORD +passenv = TEST_OPSDB_HOSTNAME,TEST_OPSDB_DBNAME,TEST_OPSDB_USERNAME,TEST_OPSDB_PASSWORD deps = coverage flake8