From 0e4dce1cc607cfd8d6e055663a9e0ea45f1ddf20 Mon Sep 17 00:00:00 2001 From: "th.l" <thl-cmk@outlook.com> Date: Thu, 16 Nov 2023 21:15:07 +0100 Subject: [PATCH] update project --- README.md | 2 +- bin/topology_data/create_topology_args.py | 11 ++ bin/topology_data/create_topology_classes.py | 160 ++++++++++++++++--- bin/topology_data/create_topology_data.py | 47 +++--- bin/topology_data/create_topology_utils.py | 41 +---- mkp/create_topology_data-0.2.0-20231116.mkp | Bin 0 -> 11625 bytes packages/create_topology_data | 8 +- 7 files changed, 188 insertions(+), 81 deletions(-) create mode 100644 mkp/create_topology_data-0.2.0-20231116.mkp diff --git a/README.md b/README.md index bdcad01..f903ebd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[PACKAGE]: ../../raw/master/mkp/create_topology_data-0.1.0-20231028.mkp "create_topology_data-0.1.0-20231028.mkp" +[PACKAGE]: ../../raw/master/mkp/create_topology_data-0.2.0-20231116.mkp "create_topology_data-0.2.0-20231116.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 fd8a801..af60d86 100755 --- a/bin/topology_data/create_topology_args.py +++ b/bin/topology_data/create_topology_args.py @@ -9,6 +9,7 @@ # # options used +# -b --backend # -d --default # -l --layer # -o --output-directory @@ -59,6 +60,16 @@ def parse_arguments() -> arg_Namespace: ) command_group = parser.add_mutually_exclusive_group() + parser.add_argument( + '-b', '--backend', + # nargs='+', + choices=['LIVESTATUS', 'FILESYSTEM', 'MULTISITE'], + default='LIVESTATUS', + help=f'Backend used to retrieve the topology data\n' + f'LIVESTATUS fetches the data only from the local site. In distributed environments use FILESYSTEM.\n' + f'For performance reasons LIVESTATUS is the default.', + ) + parser.add_argument( '-d', '--default', default=False, action='store_const', const=True, help='Set the created topology data as default', diff --git a/bin/topology_data/create_topology_classes.py b/bin/topology_data/create_topology_classes.py index 14631a5..2610ead 100755 --- a/bin/topology_data/create_topology_classes.py +++ b/bin/topology_data/create_topology_classes.py @@ -14,26 +14,27 @@ from pathlib import Path from time import strftime from typing import Dict, List, Any, NamedTuple from enum import Enum, unique +from abc import abstractmethod +from ast import literal_eval + from create_topology_utils import ( CREATE_TOPOLOGY_VERSION, - PATH_CDP, - PATH_LLDP, PATH_INTERFACES, USER_DATA_FILE, - LQ_INTERFACES, + CACHE_INTERFACES_ITEM, SCRIPT, - get_inventory_data, + get_data_form_live_status, get_table_from_inventory, - get_interface_items_from_lq, ExitCodes, get_data_from_toml, + OMD_ROOT, ) @unique -class CacheSources(Enum): +class CacheItems(Enum): inventory = 'inventory' - lq = 'lq' + interfaces = 'interfaces' class InventoryColumns(NamedTuple): @@ -59,9 +60,6 @@ class Settings: self.__topology_save_path = 'var/topology_data' self.__topology_file_name = 'network_data.json' self.__path_to_if_table = 'networking,interfaces' - # self.__inventory_path = 'var/check_mk/inventory' - # self.__autochecks_path = 'var/check_mk/autochecks' - self.__settings = { 'layers': [], 'seed_devices': None, @@ -78,6 +76,7 @@ class Settings: 'check_user_data_only': False, 'keep': None, 'min_age': None, + 'backend': 'LIVESTATUS', } # args in the form {'s, __seed_devices': 'CORE01', 'p, __path_in_inventory': None, ... }} # we will remove 's, __' @@ -99,6 +98,10 @@ class Settings: print(f'-l/--layers options must be unique. Don~\'t use any layer more than once.') exit(code=ExitCodes.BAD_OPTION_LIST.value) + @property + def backend(self) -> str: + return self.__settings['backend'] + @property def version(self) -> bool: return self.__settings['version'] @@ -186,36 +189,151 @@ class HostCache: def __init__(self): self.__cache = {} self.__inventory_pre_fetch_list: List[str] = [ - # PATH_CDP, - # PATH_LLDP, PATH_INTERFACES, ] + @abstractmethod + def get_inventory_data(self, host: str, debug: bool = False) -> Dict[str, str] | None: + """ + Args: + host: the host name to return the inventory data for + debug: enable debug output + + Returns: + the inventory data as dictionary + """ + + @abstractmethod + def get_interface_items(self, host: str, debug: bool = False) -> List: + """ + + Args: + host: the host name to return the interface items + debug: enable debug output + + Returns: + list of the interface items + """ + def __fill_cache(self, host: str): # pre fill inventory data - inventory = get_inventory_data(host=host) + inventory = self.get_inventory_data(host=host) if inventory: - self.__cache[host][CacheSources.inventory.value] = {} - self.__cache[host][CacheSources.inventory.value].update({ + self.__cache[host][CacheItems.inventory.value] = {} + self.__cache[host][CacheItems.inventory.value].update({ entry: get_table_from_inventory( inventory=inventory, path=entry ) for entry in self.__inventory_pre_fetch_list }) else: - self.__cache[host][CacheSources.inventory.value] = None - # prefill live status data - self.__cache[host][CacheSources.lq.value] = {} - self.__cache[host][CacheSources.lq.value][LQ_INTERFACES] = get_interface_items_from_lq(host) + self.__cache[host][CacheItems.inventory.value] = None + self.__cache[host][CacheItems.interfaces.value] = {} + self.__cache[host][CacheItems.interfaces.value][CACHE_INTERFACES_ITEM] = self.get_interface_items(host) - def get_data(self, host: str, source: CacheSources, path: str): + def get_data(self, host: str, item: CacheItems, path: str): if host not in self.__cache.keys(): self.__cache[host]: Dict[str, Any] = {} self.__fill_cache(host=host) try: - return self.__cache[host][source.value][path] + return self.__cache[host][item.value][path] except (KeyError, TypeError) as _e: return None def add_inventory_prefetch_path(self, path: str): self.__inventory_pre_fetch_list = list(set(self.__inventory_pre_fetch_list + [path])) + + +class HostCacheLiveStatus(HostCache): + def get_inventory_data(self, host: str, debug: bool = False) -> Dict[str, str] | None: + query = f'GET hosts\nColumns: mk_inventory\nOutputFormat: python3\nFilter: host_name = {host}\n' + data = get_data_form_live_status(query=query) + if data: + try: + data = literal_eval(data[0][0].decode('utf-8')) + except SyntaxError as e: + if debug: + print(f'data: |{data}|') + print(f'type: {type(data)}') + print(f'exception: {e}') + return + return data + + def get_interface_items(self, host: str, debug: bool = False) -> List: + """ + Sample data from lq query, we keep only the item (description without "Interface" ). + [ + ['C9540-7-1', 'Interface Vlan-999', 'check_mk-if64'], + ['C9540-7-1', 'Interface Vlan-998', 'check_mk-if64'], + ['C9540-7-1', 'Interface Vlan-997', 'check_mk-if64'], + ['C9540-7-1', 'Interface Vlan-996', 'check_mk-if64'], + ['C9540-7-1', 'Interface Vlan-8', 'check_mk-if64'], + ['C9540-7-1', 'Interface Te2/0/2', 'check_mk-if64'], + ['C9540-7-1', 'Interface Te2/0/30', 'check_mk-if64'] + ] + Args: + host: + debug: + + Returns: + + """ + query = ( + 'GET services\n' + 'Columns: host_name description check_command\n' + 'Filter: description ~ Interface\n' + f'Filter: host_name = {host}\n' + 'OutputFormat: python3\n' + ) + data = get_data_form_live_status(query=query) + 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}') + return items + + +class HostCacheFileSystem(HostCache): + def get_inventory_data(self, host: str, debug: bool = False) -> Dict[str, str] | None: + __inventory_path = 'var/check_mk/inventory' + inventory_file = Path(f'{OMD_ROOT}/{__inventory_path}/{host}') + if inventory_file.exists(): + data = literal_eval(inventory_file.read_text()) + return data + else: + return None + + def get_interface_items(self, host: str, debug: bool = False) -> List: + """ + 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'], ...}} + ] + + Args: + host: + debug: + + Returns: + + """ + __autochecks_path = 'var/check_mk/autochecks' + autochecks_file = Path(f'{OMD_ROOT}/{__autochecks_path}/{host}.mk') + __data = [] + if autochecks_file.exists(): + data: List[Dict[str, str]] = literal_eval(autochecks_file.read_text()) + for service in data: + if service['check_plugin_name'] in ['if64']: + __data.append(service['item']) + else: + if debug: + print(f'Device: {host}: not found in auto checks path!') + return [] + + return __data diff --git a/bin/topology_data/create_topology_data.py b/bin/topology_data/create_topology_data.py index 86d43c5..f0b3578 100755 --- a/bin/topology_data/create_topology_data.py +++ b/bin/topology_data/create_topology_data.py @@ -34,6 +34,7 @@ # removed option --inventory-columns, now handled in CUSTOM_LAYERS # removed option --path-in-inventory, now handled in CUSTOM_LAYERS # removed lower limits for --keep and --min-age +# 2023-1ß-29: fixed missing path from custom layer in host prefetch # # PoC for creating topology_data.json from inventory data @@ -61,7 +62,7 @@ from create_topology_utils import ( get_data_from_toml, merge_topologies, save_topology, - LQ_INTERFACES, + CACHE_INTERFACES_ITEM, PATH_INTERFACES, LAYERS, ) @@ -70,8 +71,9 @@ from create_topology_classes import ( InventoryColumns, Settings, StaticConnection, - HostCache, - CacheSources, + HostCacheLiveStatus, + HostCacheFileSystem, + CacheItems, ) HOST_MAP: Dict[str, str] = {} @@ -89,7 +91,7 @@ def get_list_of_devices(data) -> List[str]: def get_service_by_interface(host: str, interface: str, debug: bool = False) -> str: # try to find the item for an interface def _match_entry_with_item(_entry: Dict[str, str]): - values = [_entry.get('name'), _entry.get('description'), entry.get('alias')] + values = [_entry.get('name'), _entry.get('description'), _entry.get('alias')] for value in values: if value in items: return value @@ -112,24 +114,24 @@ def get_service_by_interface(host: str, interface: str, debug: bool = False) -> return interface # get list of interface items - items = HOST_CACHE.get_data(host=host, source=CacheSources.lq, path=LQ_INTERFACES) + items = HOST_CACHE.get_data(host=host, item=CacheItems.interfaces, path=CACHE_INTERFACES_ITEM) # the easy case if interface in items: return interface else: # try to find the interface in the host interface inventory list - inventory = HOST_CACHE.get_data(host=host, source=CacheSources.inventory, path=PATH_INTERFACES) + inventory = HOST_CACHE.get_data(host=host, item=CacheItems.inventory, path=PATH_INTERFACES) if inventory: - for entry in inventory: + for _entry in inventory: if interface in [ - entry.get('name'), - entry.get('description'), - entry.get('alias'), - str(entry.get('index')), - entry.get('phys_address'), + _entry.get('name'), + _entry.get('description'), + _entry.get('alias'), + str(_entry.get('index')), + _entry.get('phys_address'), ]: - return _match_entry_with_item(entry) + return _match_entry_with_item(_entry) if debug: print(f'Device: {host}: service for interface {interface} not found') @@ -242,7 +244,7 @@ def create_topology( continue # topo_data = get_inventory_data_lq(host=device, path=path_in_inventory, debug=debug) - topo_data = HOST_CACHE.get_data(host=device, source=CacheSources.inventory, path=path_in_inventory) + topo_data = HOST_CACHE.get_data(host=device, item=CacheItems.inventory, path=path_in_inventory) if topo_data: topology_data.update( create_device_from_inv( @@ -252,9 +254,9 @@ def create_topology( label=label, )) devices_list = get_list_of_devices(topology_data[device]['connections']) - for entry in devices_list: - if entry not in devices_done: - devices_to_go.append(entry) + for _entry in devices_list: + if _entry not in devices_done: + devices_to_go.append(_entry) devices_to_go = list(set(devices_to_go)) devices_done.append(device) @@ -270,7 +272,14 @@ def create_topology( if __name__ == '__main__': start_time = time_ns() SETTINGS = Settings(vars(parse_arguments())) - HOST_CACHE = HostCache() + match SETTINGS.backend: + case 'LIVESTATUS': + HOST_CACHE = HostCacheLiveStatus() + case 'FILESYSTEM': + HOST_CACHE = HostCacheFileSystem() + case _: + print(f'Backend {SETTINGS.backend} not (yet) implemented') + exit() print() print(f'Start time: {strftime(SETTINGS.time_format)}') @@ -287,7 +296,7 @@ if __name__ == '__main__': jobs.append('STATIC') elif layer in LAYERS.keys(): jobs.append(LAYERS[layer]) - HOST_CACHE.add_inventory_prefetch_path(LAYERS[layer]['path']) + HOST_CACHE.add_inventory_prefetch_path(path=LAYERS[layer]['path']) elif layer == 'CUSTOM': for entry in CUSTOM_LAYERS: jobs.append(entry) diff --git a/bin/topology_data/create_topology_utils.py b/bin/topology_data/create_topology_utils.py index cd0d023..a59bbde 100755 --- a/bin/topology_data/create_topology_utils.py +++ b/bin/topology_data/create_topology_utils.py @@ -39,15 +39,16 @@ LABEL_LLDP = 'inv_LLDP' COLUMNS_LLDP = 'system_name,local_port_num,port_id' COLUMNS_CDP = 'device_id,local_port,device_port' USER_DATA_FILE = 'create_topology_data.toml' -LQ_INTERFACES = 'interface_items' +CACHE_INTERFACES_ITEM = 'interface_items' LAYERS = { 'CDP': {'path': PATH_CDP, 'columns': COLUMNS_CDP, 'label': LABEL_CDP}, 'LLDP': {'path': PATH_LLDP, 'columns': COLUMNS_LLDP, 'label': LABEL_LLDP}, } +OMD_ROOT = environ["OMD_ROOT"] def get_data_form_live_status(query: str): - address = f'{environ.get("OMD_ROOT")}/tmp/run/live' + address = f'{OMD_ROOT}/tmp/run/live' family = socket.AF_INET if type(address) is tuple else socket.AF_UNIX sock = socket.socket(family, socket.SOCK_STREAM) sock.connect(address) @@ -84,7 +85,7 @@ def get_data_from_toml(file: str, debug: bool = False) -> Dict: def rm_tree(root: Path): # safety - if not str(root).startswith(f'{environ["OMD_ROOT"]}/var/topology_data'): + if not str(root).startswith(f'{OMD_ROOT}/var/topology_data'): print(f"WARNING: bad path to remove, {str(root)}, don\'t delete it.") return for p in root.iterdir(): @@ -300,22 +301,6 @@ def is_equal_with_default(data: Dict, file: str) -> bool: return compare_dicts(data, default_data) -def get_inventory_data(host: str, debug: bool = False, ) -> Dict[str, str] | None: - query = f'GET hosts\nColumns: mk_inventory\nOutputFormat: python3\nFilter: host_name = {host}\n' - data = get_data_form_live_status(query=query) - - if data: - try: - data = literal_eval(data[0][0].decode('utf-8')) - except SyntaxError as e: - if debug: - print(f'data: |{data}|') - print(f'type: {type(data)}') - print(f'exception: {e}') - return - return data - - def get_table_from_inventory(inventory: Dict[str, Any], path: List[str]) -> List | None: path = ('Nodes,' + ',Nodes,'.join(path.split(',')) + ',Table,Rows').split(',') table = inventory.copy() @@ -325,21 +310,3 @@ def get_table_from_inventory(inventory: Dict[str, Any], path: List[str]) -> List except KeyError: return return table - - -def get_interface_items_from_lq(host: str, debug: bool = False) -> List: - query = ( - 'GET services\n' - 'Columns: host_name description check_command\n' - 'Filter: description ~ Interface\n' - f'Filter: host_name = {host}\n' - 'OutputFormat: python3\n' - ) - data = get_data_form_live_status(query=query) - 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}') - return items diff --git a/mkp/create_topology_data-0.2.0-20231116.mkp b/mkp/create_topology_data-0.2.0-20231116.mkp new file mode 100644 index 0000000000000000000000000000000000000000..3dfb59c892d676c4d87023841ca96f214fbbc05a GIT binary patch literal 11625 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 literal 0 HcmV?d00001 diff --git a/packages/create_topology_data b/packages/create_topology_data index 1fc13fc..49be6c7 100644 --- a/packages/create_topology_data +++ b/packages/create_topology_data @@ -1,6 +1,5 @@ {'author': 'Th.L. (thl-cmk[at]outlook[dot]com)', - 'description': 'PoC for Network Visualization data creation from inventory ' - 'data\n' + 'description': 'Network Visualization data creation tool from inventory data\n' '\n' 'This tool creates the topology data file needed for the ' 'Checkmk "Network Visualization" plugin \n' @@ -34,6 +33,9 @@ 'LLDP: ' 'https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_lldp_cache\n' '\n' + 'For the latest version and documentation see:\n' + 'https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/create_topology_data\n' + '\n' '#########################################################################\n' 'Important:\n' '\n' @@ -57,7 +59,7 @@ 'topology_data/create_topology_args.py']}, 'name': 'create_topology_data', 'title': 'Network Visualization data creation', - 'version': '0.1.0-20231028', + 'version': '0.2.0-20231116', 'version.min_required': '2.2.0p1', 'version.packaged': '2.2.0p11', 'version.usable_until': '2.3.0p1'} -- GitLab