From 16cac35e878a629a58115c24d2855b31c5edd382 Mon Sep 17 00:00:00 2001 From: "th.l" <thl-cmk@outlook.com> Date: Sat, 28 Oct 2023 17:21:55 +0200 Subject: [PATCH] update project --- README.md | 2 +- bin/topology_data/create_topology_args.py | 59 +++------------ bin/topology_data/create_topology_classes.py | 73 +++++-------------- bin/topology_data/create_topology_data.py | 54 +++++++++----- bin/topology_data/create_topology_data.toml | 6 ++ bin/topology_data/create_topology_utils.py | 27 ++----- mkp/create_topology_data-0.1.0-20231028.mkp | Bin 0 -> 11078 bytes packages/create_topology_data | 2 +- 8 files changed, 76 insertions(+), 147 deletions(-) mode change 100644 => 100755 bin/topology_data/create_topology_data.toml create mode 100644 mkp/create_topology_data-0.1.0-20231028.mkp diff --git a/README.md b/README.md index 1730be2..1a04927 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[PACKAGE]: ../../raw/master/mkp/create_topology_data-0.0.9-20231027.mkp "create_topology_data-0.0.9-20231027.mkp" +[PACKAGE]: ../../raw/master/mkp/create_topology_data-0.1.0-20231028.mkp "create_topology_data-0.1.0-20231028.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 index 0460d88..8ffb328 100755 --- a/bin/topology_data/create_topology_args.py +++ b/bin/topology_data/create_topology_args.py @@ -10,21 +10,18 @@ # # options used # -d --default +# -l --layer # -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 @@ -33,17 +30,11 @@ from argparse import ( ArgumentParser, RawTextHelpFormatter, ) -from create_topology_utils import( +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 ) @@ -65,8 +56,6 @@ def parse_arguments() -> arg_Namespace: 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() @@ -74,17 +63,7 @@ def parse_arguments() -> arg_Namespace: '-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' @@ -97,6 +76,14 @@ def parse_arguments() -> arg_Namespace: help=f'List of devices to start the topology discovery from.\n' f'I.e. {SAMPLE_SEEDS}', ) + parser.add_argument( + '-l', '--layers', + nargs='+', + choices=['CDP', 'CUSTOM', 'LLDP', 'STATIC'], + default=['CDP', 'STATIC'], + help=f'Layers with least significant layer first. Listed layers\n' + f'will be merged automatically. Default is "-l CDP STATIC"', + ) parser.add_argument( '-u', '--user-data-file', type=str, help='Set the name uf the user provided data file\n' @@ -110,12 +97,6 @@ def parse_arguments() -> arg_Namespace: '--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', @@ -128,13 +109,6 @@ def parse_arguments() -> arg_Namespace: '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', @@ -145,12 +119,6 @@ def parse_arguments() -> arg_Namespace: 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', @@ -161,11 +129,6 @@ def parse_arguments() -> arg_Namespace: 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)', diff --git a/bin/topology_data/create_topology_classes.py b/bin/topology_data/create_topology_classes.py index 8bf30df..f3d1794 100755 --- a/bin/topology_data/create_topology_classes.py +++ b/bin/topology_data/create_topology_classes.py @@ -20,8 +20,6 @@ from create_topology_utils import ( COLUMNS_CDP, LABEL_CDP, PATH_LLDP, - COLUMNS_LLDP, - LABEL_LLDP, PATH_INTERFACES, USER_DATA_FILE, LQ_INTERFACES, @@ -40,12 +38,6 @@ class TopologyParams(NamedTuple): label: str -@unique -class Topologies(Enum): - 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' @@ -87,20 +79,19 @@ class Settings: # self.__autochecks_path = 'var/check_mk/autochecks' self.__settings = { + 'layers': [], 'seed_devices': None, '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}', - 'merge': None, 'output_directory': None, 'default': False, 'keep_domain': False, 'lowercase': False, 'uppercase': False, 'debug': False, - 'lldp': False, 'version': False, 'dont_compare': False, 'check_user_data_only': False, @@ -110,12 +101,6 @@ class Settings: # args in the form {'s, __seed_devices': 'CORE01', 'p, __path_in_inventory': None, ... }} # we will remove 's, __' 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'] = 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 self.version: @@ -123,37 +108,28 @@ class Settings: exit(code=ExitCodes.OK.value) if self.check_user_data_only: - user_data = get_data_from_toml(file=self.user_data_file) + _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"') + if self.layers: + layers = list(set(self.layers)) + if len(layers) != len(self.layers): + print(f'-l/--layers options must be unique. Don~\'t use any layer more than once.') 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 - self.__settings['inventory_columns'] = topology.columns - self.__settings['data_source'] = topology.label + def set_topology_param(self, topology: Dict[str, str]): + self.__settings['path_in_inventory'] = topology['path'] + self.__settings['inventory_columns'] = topology['columns'] + self.__settings['data_source'] = topology['label'] @property def version(self) -> bool: return self.__settings['version'] @property - def merge(self) -> List[str] | None: - return self.__settings['merge'] - - @merge.setter - def merge(self, topologies: List[str]): - self.__settings['merge'] = topologies + def layers(self) -> List[str]: + return self.__settings['layers'] @property def keep(self) -> int | None: @@ -171,10 +147,6 @@ class Settings: def check_user_data_only(self) -> bool: return self.__settings['check_user_data_only'] - @property - def lldp(self) -> bool: - return self.__settings['lldp'] - @property def debug(self) -> bool: return self.__settings['debug'] @@ -207,15 +179,6 @@ class Settings: def omd_root(self) -> str: return self.__omd_root - # replaced by live status query - # @property - # def inventory_path(self) -> str: - # return self.__inventory_path - - # @property - # def autochecks_path(self) -> str: - # return self.__autochecks_path - @property def user_data_file(self) -> str: return self.__settings['user_data_file'] @@ -230,14 +193,12 @@ 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 return self.__settings['path_in_inventory'] - @property - def path_to_if_table(self) -> List[str]: - path = ('Nodes,' + ',Nodes,'.join(self.__path_to_if_table.split(',')) + ',Table,Rows').split(',') - return path + # @property + # def path_to_if_table(self) -> List[str]: + # path = ('Nodes,' + ',Nodes,'.join(self.__path_to_if_table.split(',')) + ',Table,Rows').split(',') + # return path @property def inventory_columns(self) -> InventoryColumns: @@ -299,7 +260,7 @@ class HostCache: self.__fill_cache(host=host) try: return self.__cache[host][source.value][path] - except (KeyError, TypeError) as e: + except (KeyError, TypeError) as _e: return None def add_inventory_prefetch_path(self, path: str): diff --git a/bin/topology_data/create_topology_data.py b/bin/topology_data/create_topology_data.py index cded083..70ce9bb 100755 --- a/bin/topology_data/create_topology_data.py +++ b/bin/topology_data/create_topology_data.py @@ -25,16 +25,22 @@ # 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) +# 2023-10-28: reworked handling of layers (see option -l/--layers) +# added option -l/-layers {CDP,CUSTOM,LLDP,STATIC} [{CDP,CUSTOM,LLDP,STATIC} ...] +# added section CUSTOM_LAYERS to user data file +# removed option -m/--merge, included in option -l/--layers +# removed option --lldp, included in option -l/--layers +# removed option --data-source, now handled in CUSTOM_LAYERS +# removed option --inventory-columns, now handled in CUSTOM_LAYERS +# removed option --path-in-inventory, now handled in CUSTOM_LAYERS # # PoC for creating topology_data.json from inventory data # # This script creates the topology data file needed for the Checkmk "network_visualization" plugin by -# Andreas Boesl and schnetz. See +# Andreas Boesl and schnetz. For more information see # https://forum.checkmk.com/t/network-visualization/41680 -# and # https://exchange.checkmk.com/p/network-visualization -# for more information. # # NOTE: the topology_data configuration (layout etc.) is saved under ~/var/check_mk/topology # @@ -56,13 +62,13 @@ from create_topology_utils import ( save_topology, LQ_INTERFACES, PATH_INTERFACES, + LAYERS, ) from create_topology_args import parse_arguments from create_topology_classes import ( InventoryColumns, Settings, StaticConnection, - Topologies, HostCache, CacheSources, ) @@ -270,24 +276,34 @@ if __name__ == '__main__': user_data = get_data_from_toml(file=SETTINGS.user_data_file) HOST_MAP = user_data.get('HOST_MAP', {}) DROP_HOSTS = user_data.get('DROP_HOSTS', []) - + CUSTOM_LAYERS = user_data.get('CUSTOM_LAYERS', []) + jobs = [] final_topology = {} - while SETTINGS.merge: - SETTINGS.set_topology_param(Topologies[SETTINGS.merge[-1]].value) - topology = create_topology( - seed_devices=list(set(SETTINGS.seed_devices + user_data.get('SEED_DEVICES', []))), - path_in_inventory=SETTINGS.path_in_inventory, - inv_columns=SETTINGS.inventory_columns, - data_source=SETTINGS.data_source, - debug=SETTINGS.debug, - ) + + for layer in SETTINGS.layers: + if layer == 'STATIC': + jobs.append('STATIC') + elif layer in LAYERS.keys(): + jobs.append(LAYERS[layer]) + elif layer == 'CUSTOM': + for entry in CUSTOM_LAYERS: + jobs.append(entry) + + for job in jobs: + topology = {} + if job == 'STATIC': + topology = create_static_connections(connections=user_data.get('STATIC_CONNECTIONS', [])) + else: + SETTINGS.set_topology_param(job) + topology = create_topology( + seed_devices=list(set(SETTINGS.seed_devices + user_data.get('SEED_DEVICES', []))), + path_in_inventory=SETTINGS.path_in_inventory, + inv_columns=SETTINGS.inventory_columns, + data_source=SETTINGS.data_source, + debug=SETTINGS.debug, + ) if topology: final_topology = merge_topologies(topo_pri=topology, topo_sec=final_topology) - SETTINGS.merge.remove(SETTINGS.merge[-1]) - - STATIC_CONNECTIONS = create_static_connections(connections=user_data.get('STATIC_CONNECTIONS', [])) - - final_topology = merge_topologies(topo_pri=STATIC_CONNECTIONS, topo_sec=final_topology) save_topology( data=final_topology, diff --git a/bin/topology_data/create_topology_data.toml b/bin/topology_data/create_topology_data.toml old mode 100644 new mode 100755 index 9e1c7c8..bb5a2a0 --- a/bin/topology_data/create_topology_data.toml +++ b/bin/topology_data/create_topology_data.toml @@ -31,6 +31,12 @@ PROTECTED_TOPOLOGIES = [ # "your_important_topology" ] +# optional custom layers use option -l/--layers CUSTOM to include this layers +CUSTOM_LAYERS = [ + # { path = "networking,lldp_cache", columns = "system_name,local_port_num,port_id", label = "custom_LLDP" }, + # { path = "networking,cdp_cache", columns = "device_id,local_port,device_port", label = "custom_CDP" }, +] + # 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 diff --git a/bin/topology_data/create_topology_utils.py b/bin/topology_data/create_topology_utils.py index ab4bda8..cd0d023 100755 --- a/bin/topology_data/create_topology_utils.py +++ b/bin/topology_data/create_topology_utils.py @@ -7,27 +7,6 @@ # 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 os import environ from json import dumps import socket @@ -49,7 +28,7 @@ class ExitCodes(Enum): # constants -CREATE_TOPOLOGY_VERSION = '0.0.9-202310127' +CREATE_TOPOLOGY_VERSION = '0.1.0-202310128' SCRIPT = '~/local/bin/network-topology/create_topology_data.py' SAMPLE_SEEDS = 'Core01 Core02' PATH_CDP = 'networking,cdp_cache' @@ -61,6 +40,10 @@ 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' +LAYERS = { + 'CDP': {'path': PATH_CDP, 'columns': COLUMNS_CDP, 'label': LABEL_CDP}, + 'LLDP': {'path': PATH_LLDP, 'columns': COLUMNS_LLDP, 'label': LABEL_LLDP}, +} def get_data_form_live_status(query: str): diff --git a/mkp/create_topology_data-0.1.0-20231028.mkp b/mkp/create_topology_data-0.1.0-20231028.mkp new file mode 100644 index 0000000000000000000000000000000000000000..364cbc7b4d86abb72761f1b79cb1d2cbb89379c8 GIT binary patch literal 11078 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 literal 0 HcmV?d00001 diff --git a/packages/create_topology_data b/packages/create_topology_data index e181976..1fc13fc 100644 --- a/packages/create_topology_data +++ b/packages/create_topology_data @@ -57,7 +57,7 @@ 'topology_data/create_topology_args.py']}, 'name': 'create_topology_data', 'title': 'Network Visualization data creation', - 'version': '0.0.9-20231027', + 'version': '0.1.0-20231028', 'version.min_required': '2.2.0p1', 'version.packaged': '2.2.0p11', 'version.usable_until': '2.3.0p1'} -- GitLab