From 823004333017b67fbd5b6c07e50b577d1320d042 Mon Sep 17 00:00:00 2001 From: "th.l" <thl-cmk@outlook.com> Date: Fri, 27 Sep 2024 13:21:50 +0200 Subject: [PATCH] update project --- README.md | 2 +- mkp/nvdct-0.9.0-20240923.mkp | Bin 0 -> 39823 bytes source/bin/nvdct/conf/nvdct.toml | 24 ++- source/bin/nvdct/lib/args.py | 58 +++---- source/bin/nvdct/lib/backends.py | 127 +++++---------- source/bin/nvdct/lib/settings.py | 119 ++++++++------ source/bin/nvdct/lib/topologies.py | 6 +- source/bin/nvdct/lib/utils.py | 6 +- source/bin/nvdct/nvdct.py | 244 ++++++++++++----------------- source/packages/nvdct | 12 +- 10 files changed, 271 insertions(+), 327 deletions(-) create mode 100644 mkp/nvdct-0.9.0-20240923.mkp diff --git a/README.md b/README.md index bbc24b3..e77139d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[PACKAGE]: ../../raw/master/mkp/nvdct-0.8.12-20240702.mkp "nvdct-0.8.12-20240702.mkp" +[PACKAGE]: ../../raw/master/mkp/nvdct-0.9.0-20240923.mkp "nvdct-0.9.0-20240923.mkp" # Network Visualization Data Creation Tool (NVDCT) This script creates the topology data file needed for the [Checkmk Exchange Network visualization](https://exchange.checkmk.com/p/network-visualization) plugin.\ diff --git a/mkp/nvdct-0.9.0-20240923.mkp b/mkp/nvdct-0.9.0-20240923.mkp new file mode 100644 index 0000000000000000000000000000000000000000..e7925df7a841b83c72474e9ec4a66687335ad8bf GIT binary patch literal 39823 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 L9~XhF0DuAjpO6w~ literal 0 HcmV?d00001 diff --git a/source/bin/nvdct/conf/nvdct.toml b/source/bin/nvdct/conf/nvdct.toml index ac4235e..48d787b 100755 --- a/source/bin/nvdct/conf/nvdct.toml +++ b/source/bin/nvdct/conf/nvdct.toml @@ -84,6 +84,20 @@ CUSTOM_LAYERS = [ # { path = "networking,cdp_cache,neighbours", columns = "neighbour_name,local_port,neighbour_port", label = "custom_CDP", host_label = "nvdct/has_cdp_neighbours" }, ] +# list site so include/excluse, use option --filter-sites INCLUDE/EXCLUDE +SITES = [ + # "site1", + # "site2", + # "site3", +] + +# list customers so include/excluse, use option --filter-costumers INCLUDE/EXCLUDE +CUSTOMERS = [ + # "customer1", + # "customer2", + # "customer3", +] + [MAP_SPEED_TO_THICKNESS] # must be sorted from slower to faster speed # use only one entry to have all conections with the same thickness @@ -122,23 +136,23 @@ CUSTOM_LAYERS = [ [SETTINGS] # api_port = 80 -# backend = "MULTISITE" +# backend = "MULTISITE" | "RESTAPI" | "LIVESTATUS" +# case = "LOWER" | "UPPER" # default = false # dont_compare = false +# filter_customers = "INCLUDE" |"EXCLUDE" +# filter_sites = "INCLUDE" | "EXCLUDE" # keep = 0 # keep_domain = false # layers = ["LLDP", "CDP", "STATIC", "CUSTOM", "L3v4"] # log_file = "~/var/log/nvdct.log" # log_level = "WARNING" # log_to_stdout = false -# lowercase = false # min_age = 0 -# new_format = false # output_directory = '' -# prefix = "" # pre_fetch = false +# prefix = "" # quiet = true # skip_l3_if = false # skip_l3_ip = false # time_format = "%Y-%m-%dT%H:%M:%S.%m" -# uppercase = false diff --git a/source/bin/nvdct/lib/args.py b/source/bin/nvdct/lib/args.py index 9376073..080a075 100755 --- a/source/bin/nvdct/lib/args.py +++ b/source/bin/nvdct/lib/args.py @@ -18,22 +18,22 @@ # -u --user-data-file # -v --version # --api-port (deprecated ?) +# --case # --check-user-data-only # --dont-compare +# --filter-customers +# --filter-sites # --keep # --keep-domain # --log-file # --log-level # --log-to-stdout -# --lowercase # --min-age -# --new-format (deprecated) # --pre-fetch # --quiet # --skip-l3-if # --skip-l3-ip # --time-format -# --uppercase from argparse import ( @@ -41,6 +41,8 @@ from argparse import ( Namespace as arg_Namespace, RawTextHelpFormatter, ) +from pathlib import Path + from lib.utils import ( ExitCodes, HOME_URL, @@ -83,15 +85,12 @@ def parse_arguments() -> arg_Namespace: '\nUsage:\n' f'{SCRIPT} -s {SAMPLE_SEEDS} -d\n\n' ) - command_group = parser.add_mutually_exclusive_group() parser.add_argument( '-b', '--backend', - # nargs='+', - choices=['FILESYSTEM', 'LIVESTATUS', 'MULTISITE', 'RESTAPI'], + choices=['LIVESTATUS', 'MULTISITE', 'RESTAPI'], # default='MULTISITE', help='Backend used to retrieve the topology data\n' - ' - FILESYSTEM : fetches the data directly form the inventory files (deprecatred)\n' ' - LIVESTATUS : fetches data via local Livestatus (local site only)\n' ' - MULTISITE : like LIVESTATUS but for distributed environments (default)\n' ' - RESTAPI : uses the CMK REST API.', @@ -101,14 +100,12 @@ def parse_arguments() -> arg_Namespace: help='Set the created topology data as default. Will be created automatically\n' 'if it doesn\'t exists.', ) - 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/" (CMK2.2.0)\n' - 'For CMK 2.3.0 the path is under "~/var/check_mk/topology/data/".', + 'NOTE: the directory is a sub directory under "~/var/check_mk/topology/data/"\n', ) parser.add_argument( '-s', '--seed-devices', type=str, nargs='+', @@ -125,12 +122,10 @@ def parse_arguments() -> arg_Namespace: choices=['CDP', 'CUSTOM', 'LLDP', 'STATIC', 'L3v4'], # default=['CDP'], help=( - f'Layers with least significant layer first. Listed layers\n' - f'will be merged automatically (CMK2.2 only).\n' f' - CDP : needs inv_cdp_cache package at least in version {MIN_CDP_VERSION}\n' f' - LLDP : needs inv_lldp_cache package at least in version {MIN_LLDP_VERSION}\n' f' - L3v4 : needs inv_ipv4_addresses package at least in version {MIN_IPV4_ADDRESSES}\n' - f' adds, layer 3 topology fpr IPv4 (CMK 2.3.0 only)' + f' adds, layer 3 topology fpr IPv4 ' ) ) parser.add_argument( @@ -139,7 +134,8 @@ def parse_arguments() -> arg_Namespace: f'Default is ~/local/bin/nvdct/conf/conf/{USER_DATA_FILE}\n', ) parser.add_argument( - '-v', '--version', action='store_const', const=True, # default=False, + '-v', '--version', action='version', + version=f'{Path(SCRIPT).name} version: {NVDCT_VERSION}', help='Print version of this script and exit', ) parser.add_argument( @@ -147,6 +143,12 @@ def parse_arguments() -> arg_Namespace: help='TCP Port to access the REST API. Default is 80. NVDCT will try to automatically\n' 'detect the site apache port.', ) + parser.add_argument( + '--case', + choices=['LOWER', 'UPPER'], + # default='NONE', + help='Change neighbour name to all lower/upper case', + ) parser.add_argument( '--check-user-data-only', action='store_const', const=True, # default=False, help=f'Only tries to read/parse the user data from {USER_DATA_FILE} and exits.', @@ -174,6 +176,20 @@ 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( + '--filter-customers', + choices=['INCLUDE', 'EXCLUDE'], + # default='INCLUDE', + help='INCLUDE/EXCLUDE customer list from config file.\n' + 'Note: MULTISITE backend only.', + ) + parser.add_argument( + '--filter-sites', + choices=['INCLUDE', 'EXCLUDE'], + # default='INCLUDE', + help='INCLUDE/EXCLUDE site list from config file.\n' + 'Note: MULTISITE backend only.', + ) parser.add_argument( '--keep-domain', action='store_const', const=True, # default=False, help='Do not remove the domain name from the neighbor name', @@ -184,19 +200,10 @@ def parse_arguments() -> arg_Namespace: 'max will be deleted.\n' 'NOTE: The default topologies will be always kept.\n' ) - command_group.add_argument( - '--lowercase', action='store_const', const=True, # default=False, - help='Change neighbour names to all lower case', - ) parser.add_argument( '--min-age', type=int, help='The minimum number of days before a topology is deleted by "--keep".' ) - parser.add_argument( - '--new-format', action='store_const', const=True, # default=False, - help='Save data in new format. Use for CMK 2.3.x\n' - 'NVDCT will try to automatically detect the correct format. (deprecated)', - ) parser.add_argument( '--pre-fetch', action='store_const', const=True, # default=False, help='Try to fetch host data, with less API calls. Can improve RESTAPI backend\n' @@ -218,8 +225,5 @@ def parse_arguments() -> arg_Namespace: '--time-format', type=str, help=f'Format string to render the time. (default: {TIME_FORMAT_ARGPARSER})', ) - command_group.add_argument( - '--uppercase', action='store_const', const=True, # default=False, - help='Change neighbour names to all upper case', - ) + return parser.parse_args() diff --git a/source/bin/nvdct/lib/backends.py b/source/bin/nvdct/lib/backends.py index bfccbb2..6d003fd 100755 --- a/source/bin/nvdct/lib/backends.py +++ b/source/bin/nvdct/lib/backends.py @@ -10,7 +10,7 @@ # 2024-06-18: fixed host_exist returns always True if host was in host_cache, even with host=None -from _collections_abc import Mapping, Sequence +from collections.abc import Mapping, Sequence from abc import abstractmethod from ast import literal_eval from enum import Enum, unique @@ -334,10 +334,19 @@ class HostCacheLiveStatus(HostCache): class HostCacheMultiSite(HostCacheLiveStatus): - def __init__(self, pre_fetch: bool): + def __init__( + self, + pre_fetch: bool, + filter_sites: str | None = None, + sites: List[str] = [], + filter_customers: str | None = None, + customers: List[str] = None, + ): self._backend = '[MULTISITE]' self.sites: SiteConfigurations = SiteConfigurations({}) self.get_sites() + self.filter_sites(filter_sites, sites) + self.filter_costumers(filter_customers, customers) LOGGER.info(f'{self.backend} Create livestatus connection(s)') self.c = MultiSiteConnection(self.sites) # self.c.set_prepend_site(False) # is default @@ -351,9 +360,8 @@ class HostCacheMultiSite(HostCacheLiveStatus): if self.pre_fetch: self.pre_fetch_hosts() + # https://github.com/Checkmk/checkmk/blob/master/packages/cmk-livestatus-client/example_multisite.py def get_sites(self): - # see: https://github.com/Checkmk/checkmk/blob/master/packages/cmk-livestatus-client/example_multisite.py - sites_mk = Path(f'{OMD_ROOT}/etc/check_mk/multisite.d/sites.mk') socket_path = f'unix:{OMD_ROOT}/tmp/run/live' if sites_mk.exists(): @@ -372,6 +380,7 @@ class HostCacheMultiSite(HostCacheLiveStatus): self.sites.update({site: { 'alias': data['alias'], 'timeout': data['timeout'], + 'customer': data['customer'] # needed to filter by customer # 'nagios_url': '/nagios/', }}) if data['socket'] == ('local', None): @@ -395,10 +404,32 @@ class HostCacheMultiSite(HostCacheLiveStatus): }}) LOGGER.critical( - f'{self.backend} file {str(sites_mk.absolute())} not found. Fallback to' + f'{self.backend} file {str(sites_mk.absolute())} not found. Fallback to ' 'local site only. Try -b RESTAPI if you have a distributed environment.' ) + def filter_sites(self, filter: str| None, sites:List[str]): + match filter: + case 'INCLUDE': + self.sites = {site: data for site, data in self.sites.items() if site in sites} + case 'EXCLUDE': + self.sites = {site: data for site, data in self.sites.items() if site not in sites} + case _: + return + + def filter_costumers(self, filter: str | None, costumers:List[str]): + match filter: + case 'INCLUDE': + self.sites = { + site: data for site, data in self.sites.items() if data.get('customer') in costumers + } + case 'EXCLUDE': + self.sites = { + site: data for site, data in self.sites.items() if data.get('customer') not in costumers + } + case _: + return + def get_raw_data(self, query: str) -> object: return self.c.query(query=query) @@ -589,89 +620,3 @@ class HostCacheRestApi(HostCache): LOGGER.debug(f'{self.backend} # of host found: {len(self.cache.keys())}') else: LOGGER.warning(f'{self.backend} respons: {resp.text}') - - -class HostCacheFileSystem(HostCache): - def __init__(self, pre_fetch: bool): - super().__init__(pre_fetch, '[FILESYSTEM]') - - def get_inventory_data(self, hosts: Sequence[str]) -> Dict[str, Dict | None]: - host_data: Dict[str, Dict | None] = {} - __inventory_path = 'var/check_mk/inventory' - for host in hosts: - # init host_data with None - host_data[host] = None - inventory_file: Path = Path(f'{OMD_ROOT}/{__inventory_path}/{host}') - try: - data = literal_eval(inventory_file.read_text()) - except FileNotFoundError: - LOGGER.warning( - msg=f'{self.backend} Device: {host}: not found in inventory data path!' - ) - continue - - LOGGER.debug(f'{self.backend} data for host {host}: {data}') - host_data[host] = data - - return host_data - - def get_interface_data(self, hosts: Sequence[str]) -> Dict[str, Dict | None]: - """ - Sample autochecks data, we keep only the item - [ - {'check_plugin_name': 'if64', 'item': 'Fa0', 'parameters': {'discovered_oper_status': ['2'], ...}},\n # noqa: E501 - {'check_plugin_name': 'if64', 'item': 'Fa1/0/1', 'parameters': {'discovered_oper_status': ['1'], ...}},\n # noqa: E501 - {'check_plugin_name': 'if64', 'item': 'Fa1/0/10', 'parameters': {'discovered_oper_status': ['2'], ...}},\n # noqa: E501 - {'check_plugin_name': 'if64', 'item': 'Fa1/0/11', 'parameters': {'discovered_oper_status': ['2'], ...}},\n # noqa: E501 - {'check_plugin_name': 'if64', 'item': 'Fa1/0/12', 'parameters': {'discovered_oper_status': ['1'], ...}}\n # noqa: E501 - ] - - Args: - hosts: list of names of the host objects in cmk to fetch the data for - - Returns: - List of interface service items - - """ - host_data: Dict[str, Dict | None] = {} - __autochecks_path = 'var/check_mk/autochecks' - for host in hosts: - # init host_data with None - host_data[host] = None - autochecks_file: Path = Path(f'{OMD_ROOT}/{__autochecks_path}/{host}.mk') - items: Dict[str, object] = {} - try: - data: Sequence[Mapping[str, str]] = literal_eval(autochecks_file.read_text()) - except FileNotFoundError: - LOGGER.warning( - msg=f'{self.backend} Device: {host}: not found in auto checks path!' - ) - continue - LOGGER.debug(f'{self.backend} data for host {host}: {data}') - - for service in data: - if service['check_plugin_name'] in ['if64']: - items[service['item']] = {} - - if items: - LOGGER.info( - msg=f'{self.backend} Interfaces items found: {len(items)} an host {host}' - ) - host_data[host] = items - else: - LOGGER.warning( - msg=f'{self.backend} No Interfaces items found for host {host}' - ) - return host_data - - def host_exists(self, host: str) -> bool: - # always True, don't works in distributed environments as autocheck files - # are only locally available - return False - - def get_hosts_by_label(self, label: str) -> List[str] | None: - # not implemented, no (good) way to get a list of hosts from file system - return None - - def pre_fetch_hosts(self): - pass diff --git a/source/bin/nvdct/lib/settings.py b/source/bin/nvdct/lib/settings.py index 610abdc..8a55f55 100755 --- a/source/bin/nvdct/lib/settings.py +++ b/source/bin/nvdct/lib/settings.py @@ -10,11 +10,10 @@ # fixed path to default user data file -from _collections_abc import Mapping +from collections.abc import Mapping from ipaddress import AddressValueError, IPv4Address, IPv4Network, NetmaskValueError from logging import CRITICAL, FATAL, ERROR, WARNING, INFO, DEBUG from os import environ -from pathlib import Path from sys import exit as sys_exit from time import strftime from typing import Dict, List, NamedTuple @@ -23,10 +22,8 @@ from lib.utils import ( ExitCodes, get_data_from_toml, get_local_cmk_api_port, - get_local_cmk_version, + # get_local_cmk_version, LOGGER, - NVDCT_VERSION, - SCRIPT, TIME_FORMAT, USER_DATA_FILE, Layer @@ -71,24 +68,25 @@ class Settings: self.__omd_root = environ['OMD_ROOT'] self.__path_to_if_table = 'networking,interfaces' self.__topology_file_name = 'network_data.json' - self.__topology_save_path = 'var/topology_data' + # self.__topology_save_path = 'var/topology_data' self.__topology_save_path_cmk_2_3 = 'var/check_mk/topology/data' # cli defaults self.__settings = { # 'api_port': 80, 'backend': 'MULTISITE', + 'case': None, 'check_user_data_only': False, 'default': False, 'dont_compare': False, + 'filter_customers': None, + 'filter_sites': None, 'keep': False, 'keep_domain': False, 'layers': ['CDP'], 'log_file': f'{self.omd_root}/var/log/nvdct.log', 'log_level': 'WARNING', 'log_to_stdout': False, - 'lowercase': False, 'min_age': 0, - # 'new_format': False, 'output_directory': None, 'prefix': None, 'quiet': False, @@ -97,9 +95,8 @@ class Settings: 'skip_l3_if': False, 'skip_l3_ip': False, 'time_format': TIME_FORMAT, - 'uppercase': False, 'user_data_file': f'{self.omd_root}/local/bin/nvdct/conf/{USER_DATA_FILE}', - 'version': False, + # 'version': False, } # args in the form {'s, __seed_devices': 'CORE01', 'p, __path_in_inventory': None, ... }} # we will remove 's, __' @@ -107,10 +104,6 @@ class Settings: {k.split(',')[-1].strip(' ').strip('_'): v for k, v in cli_args.items() if v} ) - if self.__args.get('version'): - print(f'{Path(SCRIPT).name} version: {NVDCT_VERSION}') - sys_exit(ExitCodes.OK.value) - self.__user_data = get_data_from_toml( file=self.__args.get('user_data_file', self.user_data_file) ) @@ -134,7 +127,6 @@ class Settings: sys_exit(ExitCodes.BAD_OPTION_LIST.value) self.__api_port: int | None = None - self.__new_format: bool | None = None # init user data with defaults self.__custom_layers: List[StaticConnection] | None = None @@ -151,6 +143,8 @@ class Settings: self.__seed_devices: List[str] | None = None self.__static_connections: List[StaticConnection] | None = None self.__emblems: Emblems | None = None + self.__sites: List[str] | None = None + self.__customers: List[str] | None = None # # CLI settings @@ -169,7 +163,25 @@ class Settings: @property # -b --backend def backend(self) -> str: - return str(self.__settings['backend']) + if str(self.__settings['backend']) in ['LIVESTATUS', 'MULTISITE', 'RESTAPI']: + return str(self.__settings['backend']) + else: # fallback to defaukt -> exit ?? + LOGGER.warning( + f'Unknown backend: {self.__settings['backend']}. Accepted backends are: ' + 'LIVESTATUS, MULTISITE, RESTAPI. Fall back zo MULTISITE.' + ) + return 'MULTISITE' + + @property # --case + def case(self) -> str| None: + if self.__settings['case'] in ['LOWER', 'UPPER']: + return self.__settings['case'] + elif self.__settings['case'] is not None: + LOGGER.warning( + f'Unknon case setting {self.__settings["case"]}. ' + 'Accepted are LOWER|UPPER. Fallback to no change.' + ) + return None @property # --check-user-data-only def check_user_data_only(self) -> bool: @@ -183,6 +195,28 @@ class Settings: def dont_compare(self) -> bool: return bool(self.__settings['dont_compare']) + @property # --filter-customers + def filter_customers(self) -> str | None: + if self.__settings['filter_customers'] in ['INCLUDE', 'EXCLUDE']: + return self.__settings['filter_customers'] + elif self.__settings['filter_customers'] is not None: + LOGGER.error( + f'Wrong setting for "filter_customers": ' + f'{self.__settings["filter_customers"]}, supported settings INCLUDE|EXCLUDE.' + ) + return None + + @property # --filter-sites + def filter_sites(self) -> str | None: + if self.__settings['filter_sites'] in ['INCLUDE', 'EXCLUDE']: + return self.__settings['filter_sites'] + elif self.__settings['filter_sites'] is not None: + LOGGER.error( + f'Wrong setting for "filter_sites": ' + f'{self.__settings["filter_sites"]}, supported settings INCLUDE|EXCLUDE.' + ) + return None + @property # --keep def keep(self) -> int | None: if isinstance(self.__settings['keep'], int): @@ -225,10 +259,6 @@ class Settings: def log_to_stdtout(self) -> bool: return bool(self.__settings['log_to_stdout']) - @property # --lowercase - def lowercase(self) -> bool: - return bool(self.__settings['lowercase']) - @property # --min-age def min_age(self) -> int: if isinstance(self.__settings['min_age'], int): @@ -236,18 +266,6 @@ class Settings: else: return 0 - @property # --new-format - def new_format(self) -> bool: - if self.__new_format is None: - if self.__settings.get('new_format') is True: - self.__new_format = True - elif get_local_cmk_version().startswith('2.2'): - self.__new_format = False - else: - self.__new_format = True - - return self.__new_format - @property # --output-directory def output_directory(self) -> str: # init output directory with current time if not set @@ -289,17 +307,13 @@ class Settings: def time_format(self) -> str: return str(self.__settings['time_format']) - @property # --uppercase - def uppercase(self) -> bool: - return bool(self.__settings['uppercase']) - @property # --user-data-file def user_data_file(self) -> str: return str(self.__settings['user_data_file']) - @property # --version - def version(self) -> bool: - return bool(self.__settings['version']) + # @property # --version + # def version(self) -> bool: + # return bool(self.__settings['version']) # # user data setting @@ -330,7 +344,7 @@ class Settings: @property def drop_hosts(self) -> List[str]: if self.__drop_host is None: - self.__drop_host = [str(host) for host in self.__user_data.get('DROP_HOSTS', [])] + self.__drop_host = [str(host) for host in set(self.__user_data.get('DROP_HOSTS', []))] return self.__drop_host @property @@ -368,9 +382,9 @@ class Settings: @property def l3v4_ignore_hosts(self) -> List[str]: if self.__l3v4_ignore_hosts is None: - self.__l3v4_ignore_hosts = [str(host) for host in self.__user_data.get( + self.__l3v4_ignore_hosts = [str(host) for host in set(self.__user_data.get( 'L3V4_IGNORE_HOSTS', [] - )] + ))] return self.__l3v4_ignore_hosts @property @@ -523,6 +537,22 @@ class Settings: ) return self.__static_connections + @property + def sites(self) -> List[str]: + if self.__sites is None: + self.__sites = [str(site) for site in set(self.__user_data.get('SITES', []))] + LOGGER.info(f'fFound {len(self.__sites)} to filter on') + return self.__sites + + @property + def customers(self) -> List[str]: + if self.__customers is None: + self.__customers = [ + str(customer) for customer in set(self.__user_data.get('CUSTOMERS', [])) + ] + LOGGER.info(f'fFound {len(self.__customers)} to filter on') + return self.__customers + # # all other settings # @@ -532,10 +562,7 @@ class Settings: @property def topology_save_path(self) -> str: - if self.new_format: - return self.__topology_save_path_cmk_2_3 - - return self.__topology_save_path + return self.__topology_save_path_cmk_2_3 @property def topology_file_name(self) -> str: diff --git a/source/bin/nvdct/lib/topologies.py b/source/bin/nvdct/lib/topologies.py index 712fb33..8c35a45 100755 --- a/source/bin/nvdct/lib/topologies.py +++ b/source/bin/nvdct/lib/topologies.py @@ -8,7 +8,7 @@ # Date : 2024-06-09 # File : lib/topologies.py -from _collections_abc import Mapping, Sequence +from collections.abc import Mapping, Sequence from ipaddress import IPv4Address, IPv4Network from typing import Dict, List @@ -20,6 +20,8 @@ from lib.utils import LOGGER class NvObjects: def __init__(self) -> None: self.nv_objects: Dict[str, any] = {} + self.host_count: int = 0 + self.host_list: List[str] = [] def add_host_object( self, @@ -28,6 +30,8 @@ class NvObjects: emblem: str | None = None ) -> None: if host not in self.nv_objects: + self.host_count += 1 + self.host_list.append(host) link: Dict = {} metadata: Dict = {} # LOGGER.debug(f'host: {host}, {host_cache.host_exists(host=host)}') diff --git a/source/bin/nvdct/lib/utils.py b/source/bin/nvdct/lib/utils.py index 572fb9e..d92a87d 100755 --- a/source/bin/nvdct/lib/utils.py +++ b/source/bin/nvdct/lib/utils.py @@ -7,7 +7,7 @@ # Date : 2023-10-12 # File : nvdct/lib/utils.py -from _collections_abc import Mapping, Sequence +from collections.abc import Mapping, Sequence from ast import literal_eval from dataclasses import dataclass from enum import Enum, unique @@ -23,7 +23,7 @@ from time import time as now_time from tomllib import loads as toml_loads, TOMLDecodeError from typing import List, Dict, TextIO -NVDCT_VERSION = '0.8.12-20240702' +NVDCT_VERSION = '0.9.0-20240923' @unique @@ -251,6 +251,7 @@ def save_data_to_file(data: Mapping, path: str, file: str, make_default: bool) - Path(f'{parent_path}/default').symlink_to(target=Path(path), target_is_directory=True) +# CMK version 2.2.x format def save_topology( data: dict, base_directory: str, @@ -322,6 +323,7 @@ def is_list_of_str_equal(list1: List[str], list2: List[str]) -> bool: return tmp_list1 == tmp_list2 +# not used in cmk 2.3.x format def merge_topologies(topo_pri: Dict, topo_sec: Dict) -> Dict: """ Merge dict_prim into dict_sec diff --git a/source/bin/nvdct/nvdct.py b/source/bin/nvdct/nvdct.py index 39c334a..12eda3b 100755 --- a/source/bin/nvdct/nvdct.py +++ b/source/bin/nvdct/nvdct.py @@ -113,6 +113,12 @@ # moved (default) config file(s) to ./conf/ # 2024-06-14: added debug code for bad IPv4 address data # 2024-06-17: fixed bad IPv4 address data (just drop it) +# 2024-09-23: replaced options --lowercase/--uppercase with --case LOWER|UPPER +# changed version output from settings to argparse action +# removed backend FILESYSTEM -> will fallback to MULTISITE +# removed support for CMK2.2.x file format (removed option --new-format) +# 2024-09-24: added site filter for multisite deployments (MULTISITE only), option --filter-sites and SITES section in toml file +# added customer filter for MSP deployments (MULTISITE only), option --filter-customers and section CUSTOMERS in toml file # creating topology data json from inventory data # @@ -208,7 +214,7 @@ __data = { """ import sys -from _collections_abc import Mapping, Sequence +from collections.abc import Mapping, Sequence from ipaddress import IPv4Network from logging import DEBUG from re import compile as re_compile @@ -219,7 +225,6 @@ from lib.args import parse_arguments from lib.backends import ( CacheItems, HostCache, - HostCacheFileSystem, HostCacheLiveStatus, HostCacheMultiSite, HostCacheRestApi, @@ -227,7 +232,7 @@ from lib.backends import ( from lib.topologies import ( NvConnections, NvObjects, - get_list_of_devices, + # get_list_of_devices, get_network_summary, get_service_by_interface, is_ignore_ipv4, @@ -243,12 +248,12 @@ from lib.utils import ( Layer, LAYERS, LOGGER, - merge_topologies, + # merge_topologies, NVDCT_VERSION, PATH_L3v4, remove_old_data, save_data_to_file, - save_topology, + # save_topology, StdoutQuiet, ) from lib.settings import ( @@ -276,9 +281,7 @@ def create_l2_device_from_inv( inv_data: Sequence[Mapping[str, str]], inv_columns: InventoryColumns, label: str, -) -> Dict[str, object] | None: - data: Dict = {'connections': {}, "interfaces": []} - +) -> None: for topo_neighbour in inv_data: # check if required data are not empty if not (neighbour := topo_neighbour.get(inv_columns.neighbour)): @@ -311,10 +314,13 @@ def create_l2_device_from_inv( LOGGER.info(msg=f'drop neighbour: {neighbour}') continue - if SETTINGS.uppercase: + if SETTINGS.case == 'UPPER': neighbour = neighbour.upper() - if SETTINGS.lowercase: + LOGGER.debug(f'Changed neighbour to upper case: {neighbour}') + elif SETTINGS.case == 'LOWER': neighbour = neighbour.lower() + LOGGER.debug(f'Changed neighbour to lower case: {neighbour}') + if SETTINGS.prefix: neighbour = f'{SETTINGS.prefix}{neighbour}' # rewrite neighbour if inventory neighbour and checkmk host don't match @@ -346,17 +352,11 @@ def create_l2_device_from_inv( f'-> neighbour_port {neighbour_port}' ) - if neighbour and local_port and neighbour_port: - data['connections'].update({local_port: [neighbour, neighbour_port, label]}) - if local_port not in data['interfaces']: - data['interfaces'].append(local_port) - metadata = { 'duplex': topo_neighbour.get('duplex'), 'native_vlan': topo_neighbour.get('native_vlan'), } - # add to new object list NV_OBJECTS.add_host_object(host=host, host_cache=HOST_CACHE) NV_OBJECTS.add_host_object(host=neighbour, host_cache=HOST_CACHE) NV_OBJECTS.add_service_object( @@ -387,32 +387,10 @@ def create_l2_device_from_inv( right=f'{neighbour_port}@{neighbour}', ) - return {host: data} - -def create_static_connections(connections: Sequence[StaticConnection]) -> Dict: - data: Dict = {} +def create_static_connections(connections: Sequence[StaticConnection]): for connection in connections: LOGGER.info(msg=f'connection: {connection}') - if connection.host not in data: - data[connection.host] = {'connections': {}, 'interfaces': []} - - if connection.neighbour not in data: - data[connection.neighbour] = {'connections': {}, 'interfaces': []} - - # add connection from host to neighbour - data[connection.host]['connections'].update({connection.local_port: [ - connection.neighbour, connection.neighbour_port, connection.label - ]}) - data[connection.host]['interfaces'].append(connection.local_port) - - # add connection from neighbour to host - data[connection.neighbour]['connections'].update({ - connection.neighbour_port: [connection.host, connection.local_port, connection.label] - }) - data[connection.neighbour]['interfaces'].append(connection.neighbour_port) - - # new format NV_OBJECTS.add_host_object( host=connection.host, host_cache=HOST_CACHE, @@ -448,22 +426,14 @@ def create_static_connections(connections: Sequence[StaticConnection]) -> Dict: right=f'{connection.neighbour_port}@{connection.neighbour}', ) - LOGGER.debug(msg=data) - LOGGER.info(msg=f'Devices added: {len(data)}, source STATIC') - # if not SETTINGS.quiet: - print(f'Devices added.: {len(data)}, source STATIC') - - return data - def create_l2_topology( seed_devices: Sequence[str], path_in_inventory: str, inv_columns: InventoryColumns, label: str, -) -> Dict: +) -> None: devices_to_go = list(set(seed_devices)) # remove duplicates - topology_data: Dict = {} devices_done = [] while devices_to_go: @@ -482,15 +452,14 @@ def create_l2_topology( host=device, item=CacheItems.inventory, path=path_in_inventory ) if topo_data: - topology_data.update( - create_l2_device_from_inv( - host=device, - inv_data=topo_data, - inv_columns=inv_columns, - label=label, - )) - devices_list = get_list_of_devices(topology_data[device]['connections']) - for _entry in devices_list: + create_l2_device_from_inv( + host=device, + inv_data=topo_data, + inv_columns=inv_columns, + label=label, + ) + + for _entry in NV_OBJECTS.host_list: if _entry not in devices_done: devices_to_go.append(_entry) @@ -499,12 +468,6 @@ def create_l2_topology( devices_to_go.remove(device) LOGGER.info(msg=f'Device done: {device}, source: {label}') - LOGGER.info(f'Devices added: {len(devices_done)}, source {label}') - # if not SETTINGS.quiet: - print(f'Devices added.: {len(devices_done)}, source {label}') - - return topology_data - def create_l3v4_topology( ignore_hosts: Sequence[str], @@ -541,7 +504,7 @@ def create_l3v4_topology( emblem = EMBLEMS.ip_network try: ipv4_info = Ipv4Info(**_entry) - except TypeError as e: + except TypeError: # as e LOGGER.warning(f'Drop IPv4 address data for host: {host}, data: {_entry}') continue @@ -550,11 +513,16 @@ def create_l3v4_topology( continue if ipv4_info.cidr == 32: # drop host addresses - LOGGER.info(f'host: {host} dropped host address: {ipv4_info.address}/{ipv4_info.cidr}') + LOGGER.info( + f'host: {host} dropped host address: {ipv4_info.address}/{ipv4_info.cidr}' + ) continue if ipv4_info.type.lower() != 'ipv4': # drop if not ipv4 - LOGGER.warning(f'host: {host} dropped non ipv4 address: {ipv4_info.address}, type: {ipv4_info.type}') + LOGGER.warning( + f'host: {host} dropped non ipv4 address: {ipv4_info.address},' + ' type: {ipv4_info.type}' + ) continue if is_ignore_ipv4(ipv4_info.address, ignore_ips): @@ -626,14 +594,11 @@ def create_l3v4_topology( left=network, right=f'{ipv4_info.address}@{ipv4_info.device}@{host}', ) - LOGGER.info(msg=f'Devices added: {len(host_list)}, source L3v4') - # if not SETTINGS.quiet: - print(f'Devices added.: {len(host_list)}, source L3v4') - if __name__ == '__main__': start_time = time_ns() SETTINGS = Settings(vars(parse_arguments())) + sys.stdout = StdoutQuiet(quiet=SETTINGS.quiet) configure_logger( @@ -643,7 +608,6 @@ if __name__ == '__main__': ) LOGGER.info(msg='Data creation started') - # if not SETTINGS.quiet: print() print( f'Network Visualisation Data Creation Tool (NVDCT)\n' @@ -653,25 +617,25 @@ if __name__ == '__main__': print() print(f'Start time....: {strftime(SETTINGS.time_format)}') - _backends = { - 'LIVESTATUS': HostCacheLiveStatus, - 'FILESYSTEM': HostCacheFileSystem, - 'MULTISITE': HostCacheMultiSite, - 'RESTAPI': HostCacheRestApi, - } - host_cache_backend = _backends.get(SETTINGS.backend, None) - if not host_cache_backend: - LOGGER.error(msg=f'Backend {SETTINGS.backend} not (yet) implemented') - sys.exit(ExitCodes.BACKEND_NOT_IMPLEMENTED.value) - elif SETTINGS.backend == 'RESTAPI': - HOST_CACHE = host_cache_backend( - pre_fetch=SETTINGS.pre_fetch, - api_port=SETTINGS.api_port - ) - else: - HOST_CACHE = host_cache_backend( - pre_fetch=SETTINGS.pre_fetch, - ) + match SETTINGS.backend: + case 'RESTAPI': + HOST_CACHE = HostCacheRestApi( + pre_fetch=SETTINGS.pre_fetch, + api_port=SETTINGS.api_port + ) + case 'MULTISITE': + HOST_CACHE = HostCacheMultiSite( + pre_fetch=SETTINGS.pre_fetch, + filter_sites=SETTINGS.filter_sites, + sites = SETTINGS.sites, + ) + case 'LIVESTATUS': + HOST_CACHE = HostCacheLiveStatus( + pre_fetch=SETTINGS.pre_fetch, + ) + case _: + LOGGER.error(msg=f'Backend {SETTINGS.backend} not (yet) implemented') + sys.exit(ExitCodes.BACKEND_NOT_IMPLEMENTED.value) HOST_MAP = SETTINGS.host_map DROP_HOSTS = SETTINGS.drop_hosts @@ -717,27 +681,26 @@ if __name__ == '__main__': for job in jobs: match job: case 'STATIC': - topology: Dict | None = create_static_connections( + label = 'static' + create_static_connections( connections=SETTINGS.static_connections ) - label = 'static' case 'L3v4': topology = None label = 'l3v4' - if SETTINGS.new_format: - create_l3v4_topology( - ignore_hosts=SETTINGS.l3v4_ignore_hosts, - ignore_ips=SETTINGS.l3v4_ignore_ips, - ignore_wildcard=SETTINGS.l3v4_ignore_wildcard, - summarize=SETTINGS.l3v4_summarize, - replace=SETTINGS.l3v4_replace, - skip_if=SETTINGS.skip_l3_if, - skip_ip=SETTINGS.skip_l3_ip - ) + create_l3v4_topology( + ignore_hosts=SETTINGS.l3v4_ignore_hosts, + ignore_ips=SETTINGS.l3v4_ignore_ips, + ignore_wildcard=SETTINGS.l3v4_ignore_wildcard, + summarize=SETTINGS.l3v4_summarize, + replace=SETTINGS.l3v4_replace, + skip_if=SETTINGS.skip_l3_if, + skip_ip=SETTINGS.skip_l3_ip + ) case _: label = job.label.lower() columns = job.columns.split(',') - topology = create_l2_topology( + create_l2_topology( seed_devices=SETTINGS.seed_devices, path_in_inventory=job.path, inv_columns=InventoryColumns( @@ -748,52 +711,42 @@ if __name__ == '__main__': label=label, ) - if SETTINGS.new_format: - LOGGER.info(f'Save topology {label} in new format') - NV_CONNECTIONS.add_meta_data_to_connections( - nv_objects=NV_OBJECTS, - speed_map=MAP_SPEED_TO_THICKNESS, - ) - if not SETTINGS.loglevel == DEBUG: - connections = NV_CONNECTIONS.nv_connections - else: - connections = sorted(NV_CONNECTIONS.nv_connections) - _data = { - 'version': 1, - 'name': label, - 'objects': NV_OBJECTS.nv_objects if not SETTINGS.loglevel == DEBUG else dict( - sorted(NV_OBJECTS.nv_objects.items()) - ), - 'connections': connections - } - save_data_to_file( - data=_data, - path=( - f'{SETTINGS.omd_root}/{SETTINGS.topology_save_path}/' - f'{SETTINGS.output_directory}' - ), - file=f'data_{label}.json', - make_default=SETTINGS.default, - ) + NV_CONNECTIONS.add_meta_data_to_connections( + nv_objects=NV_OBJECTS, + speed_map=MAP_SPEED_TO_THICKNESS, + ) + if not SETTINGS.loglevel == DEBUG: + connections = NV_CONNECTIONS.nv_connections + else: + connections = sorted(NV_CONNECTIONS.nv_connections) + _data = { + 'version': 1, + 'name': label, + 'objects': NV_OBJECTS.nv_objects if not SETTINGS.loglevel == DEBUG else dict( + sorted(NV_OBJECTS.nv_objects.items()) + ), + 'connections': connections + } + save_data_to_file( + data=_data, + path=( + f'{SETTINGS.omd_root}/{SETTINGS.topology_save_path}/' + f'{SETTINGS.output_directory}' + ), + file=f'data_{label}.json', + make_default=SETTINGS.default, + ) + + message = ( + f'Source {label:.<7s}: Devices/Objects/Connections added {NV_OBJECTS.host_count}/' + f'{len(NV_OBJECTS.nv_objects)}/{len(NV_CONNECTIONS.nv_connections)}' + ) + LOGGER.info(msg=message) + print(message) NV_OBJECTS = NvObjects() NV_CONNECTIONS = NvConnections() - if topology: - final_topology = merge_topologies(topo_pri=topology, topo_sec=final_topology) - - if final_topology and not SETTINGS.new_format: - LOGGER.info('Save topology in CMK 2.2.x format') - - save_topology( - data=final_topology, - base_directory=f'{SETTINGS.omd_root}/{SETTINGS.topology_save_path}', - output_directory=SETTINGS.output_directory, - topology_file_name=SETTINGS.topology_file_name, - dont_compare=SETTINGS.dont_compare, - make_default=SETTINGS.default, - ) - if SETTINGS.keep: remove_old_data( keep=SETTINGS.keep, @@ -802,7 +755,6 @@ if __name__ == '__main__': protected=SETTINGS.protected_topologies, ) - # if not SETTINGS.quiet: print(f'Time taken....: {(time_ns() - start_time) / 1e9}/s') print(f'End time......: {strftime(SETTINGS.time_format)}') print() diff --git a/source/packages/nvdct b/source/packages/nvdct index da6ca14..61fa095 100644 --- a/source/packages/nvdct +++ b/source/packages/nvdct @@ -8,9 +8,8 @@ '\n' 'Features:\n' '\n' - ' - Ready for CMK 2.3.0\n' ' - Create Layer 2 (CDP/LLDP) topology data\n' - ' - Create Layer 3 (IPv4) topology data (CMK2.3.0 only)\n' + ' - Create Layer 3 (IPv4) topology data\n' ' - Highlight connection issues (Speed, Duplex/Half duplex, ' 'Native VLAN)\n' ' - Read connection data from the Checkmk HW/SW inventory.\n' @@ -19,16 +18,13 @@ ' - Add custom connections (STATIC) for connections that are ' 'not in the inventory\n' ' - Optimized for my CDP, LLDP and IPv4 inventory plugins\n' - ' - Merge CDP, LLDP, STATIC topologies (CMK 2.2.0 only)\n' ' - Can also be used with custom inventory plugins\n' '\n' 'For more information about the network visualization plugin ' 'see: \n' 'the announcement from schnetz ' 'https://forum.checkmk.com/t/network-visualization/41680\n' - 'and the plugin on the Exchange (CMK 2.2.0 only, CMK 2.3.0 has ' - 'this built-in) ' - 'https://exchange.checkmk.com/p/network-visualization\n' + '(CMK 2.3.0 has this plugin built-in) \n' '\n' 'The inventory data could be created with my inventory ' 'plugins:\n' @@ -58,7 +54,7 @@ 'htdocs/images/icons/location_80.png']}, 'name': 'nvdct', 'title': 'Network Visualization Data Creation Tool (NVDCT)', - 'version': '0.8.12-20240702', - 'version.min_required': '2.2.0b1', + 'version': '0.9.0-20240923', + 'version.min_required': '2.3.0b1', 'version.packaged': 'cmk-mkp-tool 0.2.0', 'version.usable_until': '2.4.0p1'} -- GitLab