From 9976a268764c740f2c2919ff31d5728fb7c11a29 Mon Sep 17 00:00:00 2001 From: "th.l" <thl-cmk@outlook.com> Date: Fri, 27 Oct 2023 20:08:16 +0200 Subject: [PATCH] update project --- README.md | 2 +- bin/topology_data/create_topology_args.py | 177 ++++++++++++++++ bin/topology_data/create_topology_classes.py | 115 +++++++++-- bin/topology_data/create_topology_data.py | 199 +++++++----------- bin/topology_data/create_topology_data.toml | 24 +-- bin/topology_data/create_topology_utils.py | 201 ++++++------------- mkp/create_topology_data-0.0.9-20231027.mkp | Bin 0 -> 11296 bytes packages/create_topology_data | 5 +- 8 files changed, 418 insertions(+), 305 deletions(-) create mode 100755 bin/topology_data/create_topology_args.py mode change 100755 => 100644 bin/topology_data/create_topology_data.toml create mode 100644 mkp/create_topology_data-0.0.9-20231027.mkp diff --git a/README.md b/README.md index de840b4..fbb9884 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[PACKAGE]: ../../raw/master/mkp/create_topology_data-0.0.8-20231020.mkp "create_topology_data-0.0.8-20231020.mkp" +[PACKAGE]: ../../raw/master/mkp/create_topology_data-0.0.9-20231027.mkp "create_topology_data-0.0.9-20231027.mkp" # PoC for Network Visualization data creation 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 new file mode 100755 index 0000000..0460d88 --- /dev/null +++ b/bin/topology_data/create_topology_args.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# License: GNU General Public License v2 +# Author: thl-cmk[at]outlook[dot]com +# URL : https://thl-cmk.hopto.org +# Date : 2023-10-12 +# File : create_topology_utils.py + +# +# options used +# -d --default +# -o --output-directory +# -s --seed-devices +# -u --user-data-file +# -v --version +# --check-user-data-only +# --data-source +# --debug +# --dont-compare +# --inventory-columns +# --keep-domain +# --keep +# --lldp +# --lowercase +# --min-age +# --path-in-inventory +# --time-format +# --uppercase + +from argparse import ( + Namespace as arg_Namespace, + ArgumentParser, + RawTextHelpFormatter, +) +from create_topology_utils import( + CREATE_TOPOLOGY_VERSION, + SCRIPT, + SAMPLE_SEEDS, + USER_DATA_FILE, + LABEL_CDP, + COLUMNS_CDP, + LABEL_LLDP, + PATH_LLDP, + COLUMNS_LLDP, + PATH_CDP +) + + +def parse_arguments() -> arg_Namespace: + parser = ArgumentParser( + prog='create_topology_data.py', + description='This script creates the topology data file needed for the Checkmk "network_visualization"\n' + 'plugin by Andreas Boesl and schnetz. For more information see\n' + 'the announcement from schnetz: https://forum.checkmk.com/t/network-visualization/41680\n' + 'and the plugin on the Exchange: https://exchange.checkmk.com/p/network-visualization .\n' + '\n' + 'The required inventory data can be created with my inventory plugins:\n' + 'CDP: https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_cdp_cache\n' + 'LLDP: https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_lldp_cache\n' + '\n' + f'\nVersion: {CREATE_TOPOLOGY_VERSION} | Written by: thl-cmk\n' + f'for more information see: https://thl-cmk.hopto.org', + formatter_class=RawTextHelpFormatter, + epilog='Usage:\n' + 'for CDP (the default):\n' + f'{SCRIPT} -s {SAMPLE_SEEDS} -d\n' + 'for LLDP:\n' + f'{SCRIPT} -s {SAMPLE_SEEDS} -d --lldp\n', + ) + command_group = parser.add_mutually_exclusive_group() + + parser.add_argument( + '-d', '--default', default=False, action='store_const', const=True, + help='Set the created topology data as default', + ) + parser.add_argument( + '-m', '--merge', + nargs=2, + choices=['CDP', 'LLDP'], + help=f'Merge topologies. This runs the topology creation for CDP and LLDP.\n' + f'The topologies are then merged in the specified order.\n' + f'I.e. -m CDP LLDP merges the CDP topology into the LLDP topology, overwriting\n' + f'the LLDP data in case there are conflicts.\n' + f'NOTE: static connection data from the user file will always merged with the\n' + f' highest priority', + ) + parser.add_argument( + '-o', '--output-directory', type=str, + help='Directory name where to save the topology data.\n' + 'I.e.: my_topology. Default is the actual date/time\n' + 'in "--time-format" format.\n' + 'NOTE: the directory is a sub directory under "~/var/topology_data/"', + ) + parser.add_argument( + '-s', '--seed-devices', type=str, nargs='+', + help=f'List of devices to start the topology discovery from.\n' + f'I.e. {SAMPLE_SEEDS}', + ) + parser.add_argument( + '-u', '--user-data-file', type=str, + help='Set the name uf the user provided data file\n' + 'Default is ~local/bin/topology_data/create_topology_data.toml\n', + ) + parser.add_argument( + '-v', '--version', default=False, action='store_const', const=True, + help='Print version of this script and exit', + ) + parser.add_argument( + '--check-user-data-only', default=False, action='store_const', const=True, + help=f'Only tries to read/parse the user data from {USER_DATA_FILE} and exits.', + ) + parser.add_argument( + '--data-source', type=str, + help=f'The source from which the topology data originates.\n' + f'I.e. {LABEL_CDP} for CDP data from the inventory.\n' + 'NOTE: right now this only an unused label.', + ) + parser.add_argument( + '--debug', default=False, action='store_const', const=True, + help='Print debug information', + ) + parser.add_argument( + '--dont-compare', default=False, action='store_const', const=True, + help='Do not compare the actual topology data with the default topology\n' + 'data. By default, the actual topology is compared with the default\n' + 'topology. If the data matches, the actual topology is not saved.\n' + 'So, if you run this tool in a cron job, a new topology will be\n' + 'created only if there was a change, unless you use "--dont-compare".' + ) + parser.add_argument( + '--inventory-columns', type=str, + help=f'Columns used from the inventory data.\n' + f'I.e. "{COLUMNS_CDP}"\n' + 'NOTE: the columns must be in the order: neighbour, local_port,\n' + 'neighbour_port', + ) + parser.add_argument( + '--keep-domain', default=False, action='store_const', const=True, + help='Do not remove the domain name from the neighbor name', + ) + parser.add_argument( + '--keep', type=int, + help=f'Number of topologies to keep. The oldest topologies above keep\n' + f'max will be deleted. The minimum value for --keep is 1.\n' + f'NOTE: The default topologies will be always kept.\n' + ) + parser.add_argument( + '--lldp', default=False, action='store_const', const=True, + help=f'Sets data source to {LABEL_LLDP}, inventory path \n' + f'to "{PATH_LLDP}" and columns\n' + f'to "{COLUMNS_LLDP}"', + ) + command_group.add_argument( + '--lowercase', default=False, action='store_const', const=True, + help='Change neighbour names to all lower case', + ) + parser.add_argument( + '--min-age', type=int, + help=f'The minimum number of days before a topology is deleted\n' + f'by "--keep".\n' + f'NOTE: Topologies that are not older than 1 days are always kept.', + ) + parser.add_argument( + '--path-in-inventory', type=str, + help=f'Checkmk inventory path to the topology data.\n' + f'I.e. "{PATH_CDP}"', + ) + parser.add_argument( + '--time-format', type=str, + help='Format string to render the time. (default: %%Y-%%m-%%dT%%H:%%M:%%S.%%m)', + ) + command_group.add_argument( + '--uppercase', default=False, action='store_const', const=True, + help='Change neighbour names to all upper case', + ) + return parser.parse_args() diff --git a/bin/topology_data/create_topology_classes.py b/bin/topology_data/create_topology_classes.py index 145a84d..8bf30df 100755 --- a/bin/topology_data/create_topology_classes.py +++ b/bin/topology_data/create_topology_classes.py @@ -10,17 +10,27 @@ from os import environ +from pathlib import Path from time import strftime from typing import Dict, List, Any, NamedTuple from enum import Enum, unique from create_topology_utils import ( - cdp_path, - cdp_columns, - cdp_label, - lldp_path, - lldp_columns, - lldp_label, - user_data_file, + CREATE_TOPOLOGY_VERSION, + PATH_CDP, + COLUMNS_CDP, + LABEL_CDP, + PATH_LLDP, + COLUMNS_LLDP, + LABEL_LLDP, + PATH_INTERFACES, + USER_DATA_FILE, + LQ_INTERFACES, + SCRIPT, + get_inventory_data, + get_table_from_inventory, + get_interface_items_from_lq, + ExitCodes, + get_data_from_toml, ) @@ -32,8 +42,14 @@ class TopologyParams(NamedTuple): @unique class Topologies(Enum): - CDP = TopologyParams(path=cdp_path, columns=cdp_columns, label=cdp_label) - LLDP = TopologyParams(path=lldp_path, columns=lldp_columns, label=lldp_label) + CDP = TopologyParams(path=PATH_CDP, columns=COLUMNS_CDP, label=LABEL_CDP) + LLDP = TopologyParams(path=PATH_LLDP, columns=COLUMNS_LLDP, label=LABEL_LLDP) + + +@unique +class CacheSources(Enum): + inventory = 'inventory' + lq = 'lq' class InventoryColumns(NamedTuple): @@ -72,11 +88,11 @@ class Settings: self.__settings = { 'seed_devices': None, - 'path_in_inventory': cdp_path, - 'inventory_columns': cdp_columns, - 'data_source': cdp_label, + 'path_in_inventory': PATH_CDP, + 'inventory_columns': COLUMNS_CDP, + 'data_source': LABEL_CDP, 'time_format': '%Y-%m-%dT%H:%M:%S.%m', - 'user_data_file': f'{self.__omd_root}/local/bin/topology_data/{user_data_file}', + 'user_data_file': f'{self.__omd_root}/local/bin/topology_data/{USER_DATA_FILE}', 'merge': None, 'output_directory': None, 'default': False, @@ -96,16 +112,31 @@ class Settings: self.__args = ({k.split(',')[-1].strip(' ').strip('_'): v for k, v in cli_args.items() if v}) # first set values if self.__args.get('lldp'): - self.__settings['data_source'] = lldp_label - self.__settings['path_in_inventory'] = lldp_path - self.__settings['inventory_columns'] = lldp_columns + self.__settings['data_source'] = LABEL_LLDP + self.__settings['path_in_inventory'] = PATH_LLDP + self.__settings['inventory_columns'] = COLUMNS_LLDP # Then update values with cli values self.__settings.update(self.__args) - if not self.merge: - if not self.lldp: - self.merge = ['CDP'] - else: + + if self.version: + print(f'{Path(SCRIPT).name} version: {CREATE_TOPOLOGY_VERSION}') + exit(code=ExitCodes.OK.value) + + if self.check_user_data_only: + user_data = get_data_from_toml(file=self.user_data_file) + print(f'Could read/parse the user data from {self.user_data_file}') + exit(code=ExitCodes.OK.value) + + if self.merge: + _merge = list(set(self.merge.copy())) + if len(_merge) != len(self.merge): + print(f'-m/--merge options must be unique. Use "-m CDP LLDP" oder "-m LLDP CDP"') + exit(code=ExitCodes.BAD_OPTION_LIST.value) + else: + if self.lldp: self.merge = ['LLDP'] + else: + self.merge = ['CDP'] def set_topology_param(self, topology: TopologyParams): self.__settings['path_in_inventory'] = topology.path @@ -199,8 +230,9 @@ class Settings: @property def path_in_inventory(self) -> List[str]: - path = ('Nodes,' + ',Nodes,'.join(self.__settings['path_in_inventory'].split(',')) + ',Table,Rows').split(',') - return path + # path = ('Nodes,' + ',Nodes,'.join(self.__settings['path_in_inventory'].split(',')) + ',Table,Rows').split(',') + # return path + return self.__settings['path_in_inventory'] @property def path_to_if_table(self) -> List[str]: @@ -233,3 +265,42 @@ class Settings: return f'{strftime(self.__settings["time_format"])}' else: return self.__settings['output_directory'] + + +class HostCache: + def __init__(self): + self.__cache = {} + self.__inventory_pre_fetch_list: List[str] = [ + PATH_CDP, + PATH_LLDP, + PATH_INTERFACES, + ] + + def __fill_cache(self, host: str): + # pre fill inventory data + inventory = get_inventory_data(host=host) + if inventory: + self.__cache[host][CacheSources.inventory.value] = {} + self.__cache[host][CacheSources.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) + + def get_data(self, host: str, source: CacheSources, 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] + 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])) diff --git a/bin/topology_data/create_topology_data.py b/bin/topology_data/create_topology_data.py index c47adbc..cded083 100755 --- a/bin/topology_data/create_topology_data.py +++ b/bin/topology_data/create_topology_data.py @@ -23,6 +23,8 @@ # added option -u, --user-data-file to provide a user specific data file # 2023-10-20: changed option -m/--make-default to -d/--default (-m is needed for -m/--merge # added -m/--merge option +# 2023-10-27: improved matching of port name from CDP/LLDP data to interface (item) name of service +# improved internal data handling (HostCache) # # PoC for creating topology_data.json from inventory data @@ -37,78 +39,39 @@ # NOTE: the topology_data configuration (layout etc.) is saved under ~/var/check_mk/topology # # The inventory data could be created with my CDP/LLDP inventory plugins: -# # CDP: https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_cdp_cache\ -# CDP path-in-inventory: 'networking,cdp_cache' -# CDP inventory-columns: 'device_id,local_port,device_port' -# # LLDP: https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_lldp_cache\ -# LLDP path-in-inventory: 'networking,lldp_cache' -# LLDP inventory-columns: 'system_name,local_port_num,port_id' # -# USAGE CDP (is the default): -# ~/local/lib/topology_data/create_topology_data.py -s CORE01 -m -# -# USAGE LLDP: -# ~/local/lib/topology_data/create_topology_data.py -s CORE01 -m --lldp +# USAGE: +# ~/local/lib/topology_data/create_topology_data.py --help # -from ast import literal_eval -from pathlib import Path from typing import Dict, List, Any from time import strftime, time_ns from create_topology_utils import ( - parse_arguments, remove_old_data, get_data_from_toml, merge_topologies, save_topology, - get_data_form_live_status, - ExitCodes, + LQ_INTERFACES, + PATH_INTERFACES, ) +from create_topology_args import parse_arguments from create_topology_classes import ( InventoryColumns, Settings, StaticConnection, Topologies, - TopologyParams, + HostCache, + CacheSources, ) - -CREATE_TOPOLOGY_VERSION = '0.0.8-202310120' - -ITEMS = {} HOST_MAP: Dict[str, str] = {} DROP_HOSTS: List[str] = [] STATIC_CONNECTIONS: Dict = {} -def get_interface_items_from_lq(host: str, debug: bool = False) -> Dict: - 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: - if host: - if host not in items.keys(): - items[host] = [] - items[host].append(description[10:]) # remove 'Interface ' from description - - interfaces = 0 - for host in items.keys(): - items[host] = list(set(items[host])) - items[host].sort() - interfaces += len(items[host]) - # print(f'Interfaces found: {interfaces}') - return items - - def get_list_of_devices(data) -> List[str]: devices = [] for connection in data.values(): @@ -116,66 +79,64 @@ def get_list_of_devices(data) -> List[str]: return list(set(devices)) -def get_service_by_interface(host: str, interface: str, debug: bool = False) -> str | None: +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')] + for value in values: + if value in items: + return value + + index = str(_entry.get('index')) + # for index try with padding + for i in range(1, 6): + index_padded = f'{index:0>{i}}' + if index_padded in items: + return index_padded + # still not found try values + index + for value in values: + if f'{value} {index_padded}' in items: + return f'{value} {index_padded}' + + return interface + # empty host/neighbour should never happen here if not host: return interface - items = ITEMS.get(host, []) + # get list of interface items + items = HOST_CACHE.get_data(host=host, source=CacheSources.lq, path=LQ_INTERFACES) + + # the easy case if interface in items: return interface else: - if_padding = interface - if '/' in interface: - if_padding = '/'.join(interface.split('/')[:-1]) + f'/{interface.split("/")[-1]:0>2}' - - for item in items: - # interface = Gi0/1 - # item = Gi0/1 - Access port - # this might not catch the correct interface (fa0 and fa0/0) - item = item.split(' ')[0] - if item == interface: - return item - elif item == if_padding: - return item - + # try to find the interface in the host interface inventory list + inventory = HOST_CACHE.get_data(host=host, source=CacheSources.inventory, path=PATH_INTERFACES) + if 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'), + ]: + return _match_entry_with_item(entry) if debug: - print(f'Device: {host}: interface ({interface}) not found in services') + print(f'Device: {host}: service for interface {interface} not found') return interface -def get_inventory_data_lq(host: str, path: List[str], debug: bool = False, ) -> List[Dict[str, str]]: - 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 [] - for m in path: - try: - data = data[m] - except KeyError: - return [] - return data - return [] - - def create_device_from_inv( host: str, inv_data: List[Dict[str, str]], inv_columns: InventoryColumns, data_source: str, + debug: bool = False, ) -> Dict[str, Any] | None: data = {'connections': {}, "interfaces": []} - if not host in ITEMS.keys(): - ITEMS.update(get_interface_items_from_lq(host=host)) for topo_neighbour in inv_data: neighbour = topo_neighbour.get(inv_columns.neighbour) if not neighbour: @@ -194,18 +155,18 @@ def create_device_from_inv( if neighbour in HOST_MAP.keys(): neighbour = HOST_MAP[neighbour] - # has to be done before checking interfaces - if not neighbour in ITEMS.keys(): - ITEMS.update(get_interface_items_from_lq(host=neighbour)) - # getting/checking interfaces local_port = topo_neighbour.get(inv_columns.local_port) - - local_port = get_service_by_interface(host, local_port) + _local_port = local_port + local_port = get_service_by_interface(host, local_port, debug=debug) + if debug and local_port != _local_port: + print(f'host: {host}, local_port: {local_port}, _local_port: {_local_port}') neighbour_port = topo_neighbour.get(inv_columns.neighbour_port) - - neighbour_port = get_service_by_interface(neighbour, neighbour_port) + _neighbour_port = neighbour_port + neighbour_port = get_service_by_interface(neighbour, neighbour_port, debug=debug) + if debug and neighbour_port != _neighbour_port: + print(f'neighbour: {neighbour}, neighbour_port {neighbour_port}, _neighbour_port {_neighbour_port}') if neighbour and local_port and neighbour_port: data['connections'].update({local_port: [neighbour, neighbour_port, data_source]}) @@ -272,18 +233,20 @@ def create_topology( if device in devices_done: continue - topo_data = get_inventory_data_lq(host=device, path=path_in_inventory, debug=debug) - topology_data.update( - create_device_from_inv( - host=device, - inv_data=topo_data, - inv_columns=inv_columns, - data_source=data_source, - )) - 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) + # 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) + if topo_data: + topology_data.update( + create_device_from_inv( + host=device, + inv_data=topo_data, + inv_columns=inv_columns, + data_source=data_source, + )) + 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) devices_to_go = list(set(devices_to_go)) devices_done.append(device) @@ -297,26 +260,12 @@ def create_topology( if __name__ == '__main__': - SETTINGS = Settings(vars(parse_arguments(CREATE_TOPOLOGY_VERSION))) - - if SETTINGS.version: - print(f'{Path(__file__).name} version: {CREATE_TOPOLOGY_VERSION}') - exit(code=ExitCodes.ok) - - if SETTINGS.check_user_data_only: - user_data = get_data_from_toml(file=SETTINGS.user_data_file) - print(f'Could read/parse the user data from {SETTINGS.user_data_file}') - exit(code=ExitCodes.OK) - - if SETTINGS.merge: - _merge = list(set(SETTINGS.merge.copy())) - if len(_merge) != len(SETTINGS.merge): - print(f'-m/--merge options must be unique. Use "-m cdp lldp" oder "-m lldp cdp"') - exit(code=ExitCodes.BAD_OPTION_LIST) + start_time = time_ns() + SETTINGS = Settings(vars(parse_arguments())) + HOST_CACHE = HostCache() print() print(f'Start time: {strftime(SETTINGS.time_format)}') - start_time = time_ns() user_data = get_data_from_toml(file=SETTINGS.user_data_file) HOST_MAP = user_data.get('HOST_MAP', {}) @@ -353,7 +302,7 @@ if __name__ == '__main__': remove_old_data( keep=SETTINGS.keep, min_age=SETTINGS.min_age, - path=Path(f'{SETTINGS.omd_root}/{SETTINGS.topology_save_path}'), + path=f'{SETTINGS.omd_root}/{SETTINGS.topology_save_path}', protected=user_data.get('PROTECTED_TOPOLOGIES', []), ) diff --git a/bin/topology_data/create_topology_data.toml b/bin/topology_data/create_topology_data.toml old mode 100755 new mode 100644 index 5e31d3d..9e1c7c8 --- a/bin/topology_data/create_topology_data.toml +++ b/bin/topology_data/create_topology_data.toml @@ -14,33 +14,33 @@ # list of (additional to -s/--seed-devices) seed devices SEED_DEVICES = [ - "CORE01", - "LOCATION01", - "LOCATION02", + # "CORE01", + # "LOCATION01", + # "LOCATION02", ] # drop neighbours with invalid names DROP_HOSTS = [ - "not advertised", - "a nother invalid name", + # "not advertised", + # "a nother invalid name", ] # topologies will not be deleted by "--keep" PROTECTED_TOPOLOGIES = [ - "2023-10-17T14:08:05.10", - "your_important_topology" + # "2023-10-17T14:08:05.10", + # "your_important_topology" ] # user defined static connections # connections will be added from host to neighbour and in reverese # hosts/neighbours in this section will be added to SEED_DEVICES STATIC_CONNECTIONS = [ - ["cmk_host1", "local-port1", "neighbour-port1", "neighbour1", "label"], - ["cmk_host1", "local-port2", "neighbour-port2", "neighbour2", "label"], + # ["cmk_host1", "local-port1", "neighbour-port1", "neighbour1", "label"], + # ["cmk_host1", "local-port2", "neighbour-port2", "neighbour2", "label"], ] # map inventory neighbour name to Checkmk host name [HOST_MAP] -inventory_neighbour1 = "cmk_host1" -inventory_neighbour2 = "cmk_host2" -inventory_neighbour3 = "cmk_host3" +# inventory_neighbour1 = "cmk_host1" +# inventory_neighbour2 = "cmk_host2" +# inventory_neighbour3 = "cmk_host3" diff --git a/bin/topology_data/create_topology_utils.py b/bin/topology_data/create_topology_utils.py index bf6776f..ab4bda8 100755 --- a/bin/topology_data/create_topology_utils.py +++ b/bin/topology_data/create_topology_utils.py @@ -37,12 +37,7 @@ from tomllib import loads as toml_loads from tomllib import TOMLDecodeError from re import match as re_match from pathlib import Path -from typing import List, Dict -from argparse import ( - Namespace as arg_Namespace, - ArgumentParser, - RawTextHelpFormatter, -) +from typing import List, Dict, Any from enum import Enum, unique @@ -53,15 +48,19 @@ class ExitCodes(Enum): BAD_TOML_FORMAT = 3 -script = '~/local/bin/network-topology/create_topology_data.py' -sample_seeds = 'Core01 Core02' -cdp_path = 'networking,cdp_cache' -cdp_columns = 'device_id,local_port,device_port' -cdp_label = 'inv_CDP' -lldp_path = 'networking,lldp_cache' -lldp_columns = 'system_name,local_port_num,port_id' -lldp_label = 'inv_LLDP' -user_data_file = 'create_topology_data.toml' +# constants +CREATE_TOPOLOGY_VERSION = '0.0.9-202310127' +SCRIPT = '~/local/bin/network-topology/create_topology_data.py' +SAMPLE_SEEDS = 'Core01 Core02' +PATH_CDP = 'networking,cdp_cache' +PATH_LLDP = 'networking,lldp_cache' +PATH_INTERFACES = 'networking,interfaces' +LABEL_CDP = 'inv_CDP' +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' def get_data_form_live_status(query: str): @@ -91,7 +90,7 @@ def get_data_from_toml(file: str, debug: bool = False) -> Dict: except TOMLDecodeError as e: print( f'ERROR: data file {toml_file} is not in valid TOML format! ({e}), (see https://toml.io/en/)') - exit(code=ExitCodes.BAD_TOML_FORMAT) + exit(code=ExitCodes.BAD_TOML_FORMAT.value) else: print(f'WARNING: User data {file} not found.') if debug: @@ -113,7 +112,8 @@ def rm_tree(root: Path): root.rmdir() -def remove_old_data(keep: int, min_age: int, path: Path, protected: List[str], debug: bool = False): +def remove_old_data(keep: int, min_age: int, path: str, protected: List[str], debug: bool = False): + path = Path(path) default_topo = path.joinpath('default') directories = [str(directory) for directory in list(path.iterdir())] # keep default top @@ -317,131 +317,46 @@ def is_equal_with_default(data: Dict, file: str) -> bool: return compare_dicts(data, default_data) -def parse_arguments(create_topology_version: str) -> arg_Namespace: - parser = ArgumentParser( - prog='create_topology_data.py', - description='This script creates the topology data file needed for the Checkmk "network_visualization"\n' - 'plugin by Andreas Boesl and schnetz. For more information see\n' - 'the announcement from schnetz: https://forum.checkmk.com/t/network-visualization/41680\n' - 'and the plugin on the Exchange: https://exchange.checkmk.com/p/network-visualization .\n' - '\n' - 'The required inventory data can be created with my inventory plugins:\n' - 'CDP: https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_cdp_cache\n' - 'LLDP: https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_lldp_cache\n' - '\n' - f'\nVersion: {create_topology_version} | Written by: thl-cmk\n' - f'for more information see: https://thl-cmk.hopto.org', - formatter_class=RawTextHelpFormatter, - epilog='Usage:\n' - 'for CDP (the default):\n' - f'{script} -s {sample_seeds} -d\n' - 'for LLDP:\n' - f'{script} -s {sample_seeds} -d --lldp\n', - ) - command_group = parser.add_mutually_exclusive_group() +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) - parser.add_argument( - '-d', '--default', default=False, action='store_const', const=True, - help='Set the created topology data as default', - ) - parser.add_argument( - '-m', '--merge', - nargs=2, - choices=['CDP', 'LLDP'], - help=f'Merge topologies. This runs the topology creation for CDP and LLDP.\n' - f'The topologies are then merged in the specified order.\n' - f'I.e. -m CDP LLDP merges the CDP topology into the LLDP topology, overwriting\n' - f'the LLDP data in case there are conflicts.\n' - f'NOTE: static connection data from the user file will always merged with the\n' - f' highest priority', - ) - parser.add_argument( - '-o', '--output-directory', type=str, - help='Directory name where to save the topology data.\n' - 'I.e.: my_topology. Default is the actual date/time\n' - 'in "--time-format" format.\n' - 'NOTE: the directory is a sub directory under "~/var/topology_data/"', - ) - parser.add_argument( - '-s', '--seed-devices', type=str, nargs='+', - help=f'List of devices to start the topology discovery from.\n' - f'I.e. {sample_seeds}', - ) - parser.add_argument( - '-u', '--user-data-file', type=str, - help='Set the name uf the user provided data file\n' - 'Default is ~local/bin/topology_data/create_topology_data.toml\n', - ) - parser.add_argument( - '-v', '--version', default=False, action='store_const', const=True, - help='Print version of this script and exit', - ) - parser.add_argument( - '--check-user-data-only', default=False, action='store_const', const=True, - help=f'Only tries to read/parse the user data from {user_data_file} and exits.', - ) - parser.add_argument( - '--data-source', type=str, - help=f'The source from which the topology data originates.\n' - f'I.e. {cdp_label} for CDP data from the inventory.\n' - 'NOTE: right now this only an unused label.', - ) - parser.add_argument( - '--debug', default=False, action='store_const', const=True, - help='Print debug information', - ) - parser.add_argument( - '--dont-compare', default=False, action='store_const', const=True, - help='Do not compare the actual topology data with the default topology\n' - 'data. By default, the actual topology is compared with the default\n' - 'topology. If the data matches, the actual topology is not saved.\n' - 'So, if you run this tool in a cron job, a new topology will be\n' - 'created only if there was a change, unless you use "--dont-compare".' - ) - parser.add_argument( - '--inventory-columns', type=str, - help=f'Columns used from the inventory data.\n' - f'I.e. "{cdp_columns}"\n' - 'NOTE: the columns must be in the order: neighbour, local_port,\n' - 'neighbour_port', - ) - parser.add_argument( - '--keep-domain', default=False, action='store_const', const=True, - help='Do not remove the domain name from the neighbor name', - ) - parser.add_argument( - '--keep', type=int, - help=f'Number of topologies to keep. The oldest topologies above keep\n' - f'max will be deleted. The minimum value for --keep is 1.\n' - f'NOTE: The default topologies will be always kept.\n' - ) - parser.add_argument( - '--lldp', default=False, action='store_const', const=True, - help=f'Sets data source to {lldp_label}, inventory path \n' - f'to "{lldp_path}" and columns\n' - f'to "{lldp_columns}"', - ) - command_group.add_argument( - '--lowercase', default=False, action='store_const', const=True, - help='Change neighbour names to all lower case', - ) - parser.add_argument( - '--min-age', type=int, - help=f'The minimum number of days before a topology is deleted\n' - f'by "--keep".\n' - f'NOTE: Topologies that are not older than 1 days are always kept.', - ) - parser.add_argument( - '--path-in-inventory', type=str, - help=f'Checkmk inventory path to the topology data.\n' - f'I.e. "{cdp_path}"', - ) - parser.add_argument( - '--time-format', type=str, - help='Format string to render the time. (default: %%Y-%%m-%%dT%%H:%%M:%%S.%%m)', - ) - command_group.add_argument( - '--uppercase', default=False, action='store_const', const=True, - help='Change neighbour names to all upper case', + 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() + for m in path: + try: + table = table[m] + 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' ) - return parser.parse_args() + 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.0.9-20231027.mkp b/mkp/create_topology_data-0.0.9-20231027.mkp new file mode 100644 index 0000000000000000000000000000000000000000..f47d7e12ba7db7499e18557f65dab4193738595a GIT binary patch literal 11296 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 Q{`z1Mqf9x3xPgH9A2Nn6YybcN literal 0 HcmV?d00001 diff --git a/packages/create_topology_data b/packages/create_topology_data index 85ce824..e181976 100644 --- a/packages/create_topology_data +++ b/packages/create_topology_data @@ -53,10 +53,11 @@ 'files': {'bin': ['topology_data/create_topology_classes.py', 'topology_data/create_topology_data.py', 'topology_data/create_topology_utils.py', - 'topology_data/create_topology_data.toml']}, + 'topology_data/create_topology_data.toml', + 'topology_data/create_topology_args.py']}, 'name': 'create_topology_data', 'title': 'Network Visualization data creation', - 'version': '0.0.8-20231020', + 'version': '0.0.9-20231027', 'version.min_required': '2.2.0p1', 'version.packaged': '2.2.0p11', 'version.usable_until': '2.3.0p1'} -- GitLab