diff --git a/README.md b/README.md index 9bfcfca494d35623478a0d944d1f9914ff5887f3..e42d629affb5cd86002d24359dad5384ee4d6ec8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[PACKAGE]: ../../raw/master/mkp/create_topology_data-0.3.0-20231128.mkp "create_topology_data-0.3.0-20231128.mkp" +[PACKAGE]: ../../raw/master/mkp/create_topology_data-0.4.0-20231130.mkp "create_topology_data-0.4.0-20231130.mkp" Network Visualization data creation tool from inventory data This script creates the topology data file needed for the [Checkmk Exchange Network visualization](https://exchange.checkmk.com/p/network-visualization) plugin by Andreas Boesl and [schnetz](https://exchange.checkmk.com/u/schnetz).\ diff --git a/bin/topology_data/create_topology_args.py b/bin/topology_data/create_topology_args.py index 8fde0d9c41100a72f92eedf307c6f965da96f8ac..f0b696b3590be66f2bc1a5f9f61d282460fc9105 100755 --- a/bin/topology_data/create_topology_args.py +++ b/bin/topology_data/create_topology_args.py @@ -63,12 +63,13 @@ def parse_arguments() -> arg_Namespace: parser.add_argument( '-b', '--backend', # nargs='+', - choices=['LIVESTATUS', 'FILESYSTEM', 'MULTISITE'], + choices=['LIVESTATUS', 'FILESYSTEM', 'MULTISITE', 'RESTAPI'], default='LIVESTATUS', help=f'Backend used to retrieve the topology data\n' f' LIVESTATUS is the default for performance reasons -> only local site\n' f' FILESYSTEM fetches the data directly form the inventory files -> use in distributed environments\n' - f' MULTISITE like LIVESTATUS but for distribute environments -> may become the default', + f' MULTISITE like LIVESTATUS but for distribute environments -> may become the default\n' + f' RESTAPI uses the CMK REST API.', ) parser.add_argument( diff --git a/bin/topology_data/create_topology_classes.py b/bin/topology_data/create_topology_classes.py index 6b3a176282105c78f4c8cc13aed21cc72e4303cf..edbb2bc1979be20e88e9ca52b58ebfa3c0dcf552 100755 --- a/bin/topology_data/create_topology_classes.py +++ b/bin/topology_data/create_topology_classes.py @@ -11,11 +11,12 @@ from os import environ from pathlib import Path -from time import strftime +from time import strftime, time_ns from typing import Dict, List, Any, NamedTuple from enum import Enum, unique from abc import abstractmethod from ast import literal_eval +from requests import session import livestatus @@ -193,6 +194,10 @@ class HostCache: self.__inventory_pre_fetch_list: List[str] = [ PATH_INTERFACES, ] + self._count = 0 + self._debug = debug + if debug: + print('init HOST_CACHE') @abstractmethod def get_inventory_data(self, host: str, debug: bool = False) -> Dict[str, str] | None: @@ -221,7 +226,15 @@ class HostCache: def __fill_cache(self, host: str): # pre fill inventory data + if self._debug: + self._count += 1 + _pre_query = time_ns() + inventory = self.get_inventory_data(host=host) + + if self._debug: + print(f'{(time_ns() - _pre_query) / 1e9}|{self._count:0>4}|inventory|{host}') + if inventory: self.__cache[host][CacheItems.inventory.value] = {} self.__cache[host][CacheItems.inventory.value].update({ @@ -233,8 +246,16 @@ class HostCache: else: self.__cache[host][CacheItems.inventory.value] = None self.__cache[host][CacheItems.interfaces.value] = {} + + if self._debug: + self._count += 1 + _pre_query = time_ns() + self.__cache[host][CacheItems.interfaces.value][CACHE_INTERFACES_ITEM] = self.get_interface_items(host) + if self._debug: + print(f'{(time_ns() - _pre_query) / 1e9}|{self._count:0>4}|items|{host}') + def get_data(self, host: str, item: CacheItems, path: str): if host not in self.__cache.keys(): self.__cache[host]: Dict[str, Any] = {} @@ -262,6 +283,8 @@ class HostCacheLiveStatus(HostCache): print(f'exception: {e}') return return data + if debug: + print(f'Device: {host}: no inventory data found!') def get_interface_items(self, host: str, debug: bool = False) -> List: """ @@ -307,24 +330,27 @@ class HostCacheFileSystem(HostCache): data = literal_eval(inventory_file.read_text()) return data else: + if debug: + print(f'Device: {host}: not found in inventory data path!') return None def get_interface_items(self, host: str, debug: bool = False) -> List: """ - Sample autochecks data we keep only the item + Sample autochecks data, we keep only the item [ - {'check_plugin_name': 'if64', 'item': 'Fa0', 'parameters': {'discovered_oper_status': ['2'], ...}}, - {'check_plugin_name': 'if64', 'item': 'Fa1/0/1', 'parameters': {'discovered_oper_status': ['1'], ...}}, - {'check_plugin_name': 'if64', 'item': 'Fa1/0/10', 'parameters': {'discovered_oper_status': ['2'], ...}}, - {'check_plugin_name': 'if64', 'item': 'Fa1/0/11', 'parameters': {'discovered_oper_status': ['2'], ...}}, - {'check_plugin_name': 'if64', 'item': 'Fa1/0/12', 'parameters': {'discovered_oper_status': ['1'], ...}} + {'check_plugin_name': 'if64', 'item': 'Fa0', 'parameters': {'discovered_oper_status': ['2'], ...}},\n + {'check_plugin_name': 'if64', 'item': 'Fa1/0/1', 'parameters': {'discovered_oper_status': ['1'], ...}},\n + {'check_plugin_name': 'if64', 'item': 'Fa1/0/10', 'parameters': {'discovered_oper_status': ['2'], ...}},\n + {'check_plugin_name': 'if64', 'item': 'Fa1/0/11', 'parameters': {'discovered_oper_status': ['2'], ...}},\n + {'check_plugin_name': 'if64', 'item': 'Fa1/0/12', 'parameters': {'discovered_oper_status': ['1'], ...}}\n ] Args: - host: - debug: + host: name of the host object in cmk to fetch the data for + debug: output debug information Returns: + List of interface service items """ __autochecks_path = 'var/check_mk/autochecks' @@ -348,12 +374,14 @@ class HostCacheMultiSite(HostCache): super().__init__(debug=debug) self.__sites = {} self.get_sites(debug=debug) + if debug: + print('Create livestatus connection(s)') self.__c = livestatus.MultiSiteConnection(self.__sites) # self.__c.set_prepend_site(False) # is default # self.__c.parallelize = True # is default self.__dead_sites = [site['site']['alias'] for site in self.__c.dead_sites().values()] if self.__dead_sites: - self.__dead_sites = ', '.join( self.__dead_sites) + self.__dead_sites = ', '.join(self.__dead_sites) print(f'WARNING: use of dead site(s) {self.__dead_sites} is disabled') self.__c.set_only_sites(self.__c.alive_sites()) @@ -416,7 +444,74 @@ class HostCacheMultiSite(HostCache): items = [] for host, description, check_command in data: items.append(description[10:]) # remove 'Interface ' from description - if debug: - print(f'Interfaces items found: {len(items)} an host {host}') + if items: + print(f'Interfaces items found: {len(items)} an host {host}') + else: + print(f'No Interfaces items found for host {host}') return items + + +class HostCacheRestApi(HostCache): + def __init__(self, debug: bool = False): + super().__init__(debug=debug) + try: + self.__secret = Path(f'{OMD_ROOT}/var/check_mk/web/automation/automation.secret').read_text().strip('\n)') + except FileNotFoundError as e: + print(f'automation.secret not found, {e}') + exit() + self.__hostname = 'localhost' + self.__site = OMD_ROOT.split('/')[-1] + self.__api_url = f"http://{self.__hostname}/{self.__site}/check_mk/api/1.0" + self.__user = 'automation' + if debug: + print('Create REST API session') + self.__session = session() + self.__session.headers['Authorization'] = f"Bearer {self.__user} {self.__secret}" + self.__session.headers['Accept'] = 'application/json' + + def get_inventory_data(self, host: str, debug: bool = False) -> Dict[str, str] | None: + # self._count += 1 + + __query = '{"op": "=", "left": "name", "right": "' + host + '"}' + resp = self.__session.get( + f"{self.__api_url}/domain-types/host/collections/all", + params={ + "query": __query, + "columns": ['mk_inventory'], + }, + ) + if resp.status_code == 200: + # print(f'{resp.elapsed}|{self._count:0>4}|inventory|{host}') + try: + return resp.json()['value'][0]['extensions']['mk_inventory'] + except IndexError: + return None + else: + if debug: + print(f'Device: {host}: no inventory data found!') + return None + + def get_interface_items(self, host: str, debug: bool = False) -> List: + # self._count += 1 + + __query = '{"op": "~", "left": "description", "right": "Interface "}' + resp = self.__session.get( + f"{self.__api_url}/objects/host/NX01/collections/services", + params={ + "query": __query, + "columns": ['host_name', 'description', 'check_command'], + }, + ) + + if resp.status_code == 200: + # print(f'{resp.elapsed}|{self._count:0>4}|items|{host}') + + items = [service['extensions']['description'][10:] for service in resp.json()['value']] + if debug: + print(f'Interfaces items found: {len(items)} an host {host}') + return items + else: + if debug: + print(f'No Interfaces items found for host {host}') + return [] diff --git a/bin/topology_data/create_topology_data.py b/bin/topology_data/create_topology_data.py index 96198b1f7985d43f24cc3a8b24fa5cccacd7ccba..445249deac7e5ee2acc8975657d6ad830c442e73 100755 --- a/bin/topology_data/create_topology_data.py +++ b/bin/topology_data/create_topology_data.py @@ -39,6 +39,7 @@ # LIVESTATUS is the default for performance reasons -> for now only local site # FILESYSTEM fetches the data directly form the inventory files -> use in distributed environments # 2023-11-28: implemented MULTISITE as backend, via livestatus -> use in distributed environments, my become the default +# 2023-11-30: added RESTAPI backend --> this is kind of slow (20 seconds compared to 0.5 for MULTISITE) # # creating topology_data.json from inventory data # @@ -77,6 +78,7 @@ from create_topology_classes import ( HostCacheLiveStatus, HostCacheFileSystem, HostCacheMultiSite, + HostCacheRestApi, CacheItems, ) @@ -279,16 +281,18 @@ if __name__ == '__main__': print() print(f'Start time: {strftime(SETTINGS.time_format)}') - match SETTINGS.backend: - case 'LIVESTATUS': - HOST_CACHE = HostCacheLiveStatus() - case 'FILESYSTEM': - HOST_CACHE = HostCacheFileSystem() - case 'MULTISITE': - HOST_CACHE = HostCacheMultiSite(debug=SETTINGS.debug) - case _: - print(f'Backend {SETTINGS.backend} not (yet) implemented') - exit() + _backends = { + 'LIVESTATUS': HostCacheLiveStatus, + 'FILESYSTEM': HostCacheFileSystem, + 'MULTISITE': HostCacheMultiSite, + 'RESTAPI': HostCacheRestApi, + } + HOST_CACHE = _backends.get(SETTINGS.backend, None) + if not HOST_CACHE: + print(f'Backend {SETTINGS.backend} not (yet) implemented') + exit() + else: + HOST_CACHE = HOST_CACHE(debug=SETTINGS.debug) user_data = get_data_from_toml(file=SETTINGS.user_data_file) HOST_MAP = user_data.get('HOST_MAP', {}) diff --git a/mkp/create_topology_data-0.4.0-20231130.mkp b/mkp/create_topology_data-0.4.0-20231130.mkp new file mode 100644 index 0000000000000000000000000000000000000000..47c80f9322aeac5082214ab6b980a24bd42fafff Binary files /dev/null and b/mkp/create_topology_data-0.4.0-20231130.mkp differ diff --git a/packages/create_topology_data b/packages/create_topology_data index 9c199bfded9ea3c0421e6c880132febbbffc28e8..d37aa91badd352cae98a2bac0c6ec5b762ecc617 100644 --- a/packages/create_topology_data +++ b/packages/create_topology_data @@ -59,7 +59,7 @@ 'topology_data/create_topology_args.py']}, 'name': 'create_topology_data', 'title': 'Network Visualization data creation', - 'version': '0.3.0-20231128', + 'version': '0.4.0-20231130', 'version.min_required': '2.2.0p1', 'version.packaged': '2.2.0p14', 'version.usable_until': '2.3.0p1'}