diff --git a/README.md b/README.md index e3024561eb5c0d84458ac301c8ddd912d5a5ca92..4620c9d1c6efc1f5ec8f583e3ff27eca33739e05 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[PACKAGE]: ../../raw/master/mkp/nvdct-0.9.8-20250205.mkp "nvdct-0.9.8-20250205.mkp" +[PACKAGE]: ../../raw/master/mkp/nvdct-0.9.9-20250214.mkp "nvdct-0.9.9-20250214.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.9-20250214.mkp b/mkp/nvdct-0.9.9-20250214.mkp new file mode 100644 index 0000000000000000000000000000000000000000..979efdd9cd8f8c8682f257efb4380f9e70c2c7a9 Binary files /dev/null and b/mkp/nvdct-0.9.9-20250214.mkp differ diff --git a/source/bin/nvdct/conf/nvdct.toml b/source/bin/nvdct/conf/nvdct.toml index 524e395d38864e29bd7ef959f39cd45f7400feff..6698fe7b296944873216998e669fc9475615e8f8 100644 --- a/source/bin/nvdct/conf/nvdct.toml +++ b/source/bin/nvdct/conf/nvdct.toml @@ -168,6 +168,7 @@ FILTER_BY_SITE = [ # l2_case = "OFF" | "LOWER" | "UPPER" | "IGNORE" | "AUTO" # l2_display_neighbours = false | true # l2_display_ports = false | true +# l2_ignore_mismatch = ["DUPLEX", "SPEED", "VLAN"] # l2_prefix = "" # l2_remove_domain = "OFF" | "ON" | "AUTO" # l2_skip_external = false | true diff --git a/source/bin/nvdct/lib/args.py b/source/bin/nvdct/lib/args.py index 3f9a81f629ee74ce192dc5669815b72bf3d4efb6..8f2fa78ebbe3ea4b0527e40f9ca6339760d09c29 100755 --- a/source/bin/nvdct/lib/args.py +++ b/source/bin/nvdct/lib/args.py @@ -24,6 +24,7 @@ # --l2-case # --l2-display-ports # --l2-display-neighbours +# --l2-ignore-mismatch # --l2-prefix # --l2-remove-domain # --l2-skip-external @@ -55,16 +56,17 @@ from pathlib import Path from lib.constants import ( Backends, CONFIG_FILE, - Case, CliLong, CliShort, ExitCodes, IncludeExclude, + L2Case, + L2IgnoreMismatch, + L2RemoveDomain, Layers, LogLevels, MinVersions, NVDCT_VERSION, - RemoveDomain, SCRIPT, TIME_FORMAT_ARGPARSER, TomlSections, @@ -95,11 +97,11 @@ def parse_arguments() -> arg_Namespace: f' {ExitCodes.AUTOMATION_SECRET_NOT_FOUND} - Automation secret not found\n' f' {ExitCodes.NO_LAYER_CONFIGURED} - No layer to work on\n' '\nUsage:\n' - f'{SCRIPT} -u ~/local/bin/nvdct/conf/my_{CONFIG_FILE} \n\n' + f'{SCRIPT} -c ~/local/bin/nvdct/conf/my_{CONFIG_FILE} \n\n' ) parser.add_argument( CliShort.BACKEND, CliLong.BACKEND, - choices=[Backends.LIVESTATUS, Backends.MULTISITE, Backends.RESTAPI], + choices=Backends.list(), # default='MULTISITE', help='Backend used to retrieve the topology data\n' f' - {Backends.LIVESTATUS} : fetches data via local Livestatus (local site only)\n' @@ -119,13 +121,7 @@ def parse_arguments() -> arg_Namespace: parser.add_argument( CliShort.LAYERS, CliLong.LAYERS, nargs='+', - choices=[ - Layers.CDP, - Layers.LLDP, - Layers.L3V4, - Layers.STATIC, - ], - # default=['CDP'], + choices=Layers.list(), help=( f' - {Layers.CDP} : needs inv_cdp_cache package at least in version {MinVersions.CDP}\n' f' - {Layers.LLDP} : needs inv_lldp_cache package at least in version {MinVersions.LLDP}\n' @@ -166,14 +162,14 @@ def parse_arguments() -> arg_Namespace: ) parser.add_argument( CliLong.FILTER_CUSTOMERS, - choices=[IncludeExclude.INCLUDE, IncludeExclude.EXCLUDE], + choices=IncludeExclude.list(), # default='INCLUDE', help=f'{IncludeExclude.INCLUDE}/{IncludeExclude.EXCLUDE} customer list "[{TomlSections.FILTER_BY_CUSTOMER}]" from TOML file.' f'NOTE: {Backends.MULTISITE} backend only.', ) parser.add_argument( CliLong.FILTER_SITES, - choices=[IncludeExclude.EXCLUDE, IncludeExclude.EXCLUDE], + choices=IncludeExclude.list(), # default='INCLUDE', help=f'{IncludeExclude.INCLUDE}/{IncludeExclude.EXCLUDE} site list "[{TomlSections.FILTER_BY_SITE}]" from TOML file.' ) @@ -185,13 +181,13 @@ def parse_arguments() -> arg_Namespace: ) parser.add_argument( CliLong.L2_CASE, - choices=[Case.INSENSITIVE, Case.LOWER, Case.UPPER, Case.AUTO, Case.OFF], - help='Change L2 neighbour name case before matching to Checkmk host.\n' - f'- {Case.OFF} : Do not change the case of the neighbour name.' - f'- {Case.INSENSITIVE} : search for a matching host by ignoring the case of neighbour name and host name\n' - f'- {Case.LOWER} : change to all lower case\n' - f'- {Case.UPPER} : change to all upper case, without the domain part of the neighbour name\n' - f'- {Case.AUTO} : try all the above variants\n' + choices=L2Case.list(), + help='Change L2 neighbour name case before matching to Checkmk host\n' + f'- {L2Case.OFF} : Do not change the case of the neighbour name\n' + f'- {L2Case.INSENSITIVE} : search for a matching host by ignoring the case of neighbour name and host name\n' + f'- {L2Case.LOWER} : change to all lower case\n' + f'- {L2Case.UPPER} : change to all upper case, without the domain part of the neighbour name\n' + f'- {L2Case.AUTO} : try all the above variants\n' f'Default is let the case of the neighbour name untouched ("OFF").\n' f'Takes place after "{CliLong.L2_REMOVE_DOMAIN}" and before "{CliLong.L2_PREFIX}"', ) @@ -203,6 +199,16 @@ def parse_arguments() -> arg_Namespace: CliLong.L2_DISPLAY_NEIGHBOURS, action='store_const', const=True, # default=False, help='Use L2 neighbour name as display name in L2 topologies', ) + parser.add_argument( + CliLong.L2_IGNORE_MISMATCH, + nargs='+', + choices=L2IgnoreMismatch.list(), + help=( + f' - {L2IgnoreMismatch.DUPLEX} : Ignore duplex mismatch in layer 2 topologies\n' + f' - {L2IgnoreMismatch.SPEED} : Ignore speed mismatch in layer 2 topologies\n' + f' - {L2IgnoreMismatch.VLAN} : Ignore native vlan mismatch in layer 2 topologies\n' + ) + ) parser.add_argument( CliLong.L2_PREFIX, type=str, help=f'Prepends each L2 neighbour name with the prefix before matching to a Checkmk host name.\n' @@ -210,12 +216,12 @@ def parse_arguments() -> arg_Namespace: ) parser.add_argument( CliLong.L2_REMOVE_DOMAIN, - choices=[RemoveDomain.OFF, RemoveDomain.ON, RemoveDomain.AUTO], + choices=L2RemoveDomain.list(), help=f'Handle the the domain name part of a neighbour name before matching it to a Checkmk host.\n' - f'- {RemoveDomain.OFF} : dont touch the neighbour name, keep host name and domain part\n' - f'- {RemoveDomain.ON} : will remove the domain part from the neighbour name, keep only the host name part\n' - f'- {RemoveDomain.AUTO} : try all of the above variants\n' - f'Default: "{RemoveDomain.OFF}". Takes place after "{CliLong.L2_REMOVE_DOMAIN}" and before "{CliLong.L2_PREFIX}"', + f'- {L2RemoveDomain.OFF} : dont touch the neighbour name, keep host name and domain part\n' + f'- {L2RemoveDomain.ON} : will remove the domain part from the neighbour name, keep only the host name part\n' + f'- {L2RemoveDomain.AUTO} : try all of the above variants\n' + f'Default: "{L2RemoveDomain.OFF}". Takes place after "{CliLong.L2_REMOVE_DOMAIN}" and before "{CliLong.L2_PREFIX}"', ) parser.add_argument( CliLong.L2_SKIP_EXTERNAL, action='store_const', const=True, # default=False, @@ -260,15 +266,7 @@ def parse_arguments() -> arg_Namespace: parser.add_argument( CliLong.LOG_LEVEL, # nargs='+', - choices=[ - LogLevels.CRITICAL, - LogLevels.FATAL, - LogLevels.ERROR, - LogLevels.WARNING, - LogLevels.INFO, - LogLevels.DEBUG, - LogLevels.OFF - ], + choices=LogLevels.list(), # default='WARNING', help=f'Sets the log level. The default is "{LogLevels.WARNING}"' ) @@ -295,6 +293,6 @@ def parse_arguments() -> arg_Namespace: ) parser.add_argument( CliLong.UPDATE_CONFIG, action='store_const', const=True, # default=False, - help='Adjusts old options in TOML file.', + help='Adjusts options in config file.', ) return parser.parse_args() diff --git a/source/bin/nvdct/lib/backends.py b/source/bin/nvdct/lib/backends.py index 8fce63c9be89a7c6c98fdcaf29c773ba3c6351ee..35860ed20cbdfa94d4ad27120782a61a9e6b747c 100755 --- a/source/bin/nvdct/lib/backends.py +++ b/source/bin/nvdct/lib/backends.py @@ -31,16 +31,16 @@ from lib.constants import ( Backends, CACHE_INTERFACES_DATA, CacheItems, - Case, ExitCodes, HostFilter, IncludeExclude, InvPaths, + L2Case, L2InvColumns, + L2RemoveDomain, LiveStatusOperator, MIN_CMK_VERSION_POST, OMD_ROOT, - RemoveDomain, ) from lib.utils import ( LOGGER, @@ -62,7 +62,7 @@ class HostCache: LOGGER.info(f'{backend} init HOST_CACHE') self.cache: Dict = {} - self.neighbour_to_host: MutableMapping[str, str] = {} + self.neighbour_to_host: MutableMapping[str, str | None] = {} self._inventory_pre_fetch_list: List[str] = [InvPaths.INTERFACES] self.backend: str = str(backend) @@ -71,7 +71,7 @@ class HostCache: self.l2_neighbour_replace_regex: List[Tuple[str, str]] = [] self.pre_fetch: bool = bool(pre_fetch) self.prefix: str = '' - self.remove_domain: str = RemoveDomain.OFF + self.remove_domain: str = L2RemoveDomain.OFF self.filter_include: MutableSequence[str] = [] self.filter_exclude: MutableSequence[str] = [] self.no_case_host_map: MutableMapping[str, str] = {} @@ -94,9 +94,9 @@ class HostCache: def init_filter_lists( self, - filter_by_folder: Mapping[str, Sequence[str]], - filter_by_host_label: Mapping[str, Sequence[str]], - filter_by_host_tag: Mapping[str, Sequence[str]], + filter_by_folder: Mapping[str, set[str]], + filter_by_host_label: Mapping[str, set[str]], + filter_by_host_tag: Mapping[str, set[str]], ): for folder in filter_by_folder[IncludeExclude.INCLUDE]: self.filter_include += self.query_hosts_by_filter(HostFilter.FOLDER, folder, LiveStatusOperator.SUPERSET) @@ -243,7 +243,7 @@ class HostCache: Returns: None, the data is directly writen to self.cache """ - inventory_of_hosts: Mapping[str, Mapping | None] = self.get_inventory_data(hosts=hosts) + inventory_of_hosts: Mapping[str, Dict | None] = self.get_inventory_data(hosts=hosts) if inventory_of_hosts: for host, inventory in inventory_of_hosts.items(): if host not in self.cache: @@ -343,30 +343,30 @@ class HostCache: host_no_domain: str = host.split('.')[0] match self.remove_domain: - case RemoveDomain.ON: + case L2RemoveDomain.ON: LOGGER.debug(f'{self.backend} Remove domain: {host} -> {host_no_domain}') host = host_no_domain possible_hosts.add(host_no_domain) - case RemoveDomain.AUTO: + case L2RemoveDomain.AUTO: possible_hosts.add(host) possible_hosts.add(host_no_domain) - case RemoveDomain.OFF | _: + case L2RemoveDomain.OFF | _: possible_hosts.add(host) match self.case: - case Case.UPPER: + case L2Case.UPPER: if not '.' in host: # use UPPER only for names without domain LOGGER.debug(f'{self.backend} Change neighbour to upper case: {host} -> {host.upper()}') possible_hosts.add(host.upper) - case Case.LOWER: + case L2Case.LOWER: LOGGER.debug(f'{self.backend} Change neighbour to lower case: {host} -> {host.lower()}') possible_hosts.add(host.lower()) - case Case.AUTO: + case L2Case.AUTO: possible_hosts.add(host) possible_hosts.add(host.lower()) possible_hosts.add(host_no_domain.lower()) possible_hosts.add(host_no_domain.upper()) - case Case.OFF | _: + case L2Case.OFF | _: possible_hosts.add(host) possible_hosts = [f'{self.prefix}{host}' for host in possible_hosts] @@ -381,7 +381,7 @@ class HostCache: LOGGER.debug(f'{self.backend} Matched neighbour to host: |{neighbour}| -> |{entry}|') return entry - if self.case in [Case.AUTO, Case.INSENSITIVE]: + if self.case in [L2Case.AUTO, L2Case.INSENSITIVE]: if not self.no_case_host_map: self.no_case_host_map = {host.lower():host for host in self.cache} for entry in possible_hosts: diff --git a/source/bin/nvdct/lib/constants.py b/source/bin/nvdct/lib/constants.py index 579eb0d75050c4dd8b280c62138deecfb5e4e401..2cac55c304fbf5a352847dfe99a59345b3781002 100755 --- a/source/bin/nvdct/lib/constants.py +++ b/source/bin/nvdct/lib/constants.py @@ -13,7 +13,7 @@ from os import environ from typing import Final # -NVDCT_VERSION: Final[str] = '0.9.8-20250107' +NVDCT_VERSION: Final[str] = '0.9.9-20250207' # OMD_ROOT: Final[str] = environ["OMD_ROOT"] # @@ -21,19 +21,25 @@ API_PORT_DEFAULT: Final[int] = 5001 CACHE_INTERFACES_DATA: Final[str] = 'interface_data' CMK_SITE_CONF: Final[str] = f'{OMD_ROOT}/etc/omd/site.conf' CONFIG_FILE: Final[str] = 'nvdct.toml' +CONFIG_PATH_DEFAULT: Final[str] = f'{OMD_ROOT}/local/bin/nvdct/conf' DATAPATH: Final[str] = f'{OMD_ROOT}/var/check_mk/topology/data' LOGGER: Logger = getLogger('root)') -LOG_FILE_DEFAULT: Final[str] = f'{OMD_ROOT}/var/log/nvdct.log' +LOG_PATH_DEFAULT: Final[str] = f'{OMD_ROOT}/var/log' +LOG_FILE_DEFAULT: Final[str] = f'{LOG_PATH_DEFAULT}/nvdct.log' MIN_CMK_VERSION_POST: Final[str] = '2.3.0p23' SCRIPT: Final[str] = '~/local/bin/nvdct/nvdct.py' -TIME_FORMAT_ARGPARSER: Final[str] = '%%Y-%%m-%%dT%%H:%%M:%%S.%%m' TIME_FORMAT_DEFAULT: Final[str] = '%Y-%m-%dT%H:%M:%S.%m' +TIME_FORMAT_ARGPARSER: Final[str] = TIME_FORMAT_DEFAULT.replace('%', '%%') + class EnumValue(Enum): def __get__(self, instance, owner): return self.value + @classmethod + def list(cls): + return list(map(lambda c: c.value, cls)) @unique class ExitCodes(EnumValue): @@ -72,7 +78,7 @@ class Backends(EnumValue): @unique -class Case(EnumValue): +class L2Case(EnumValue): AUTO: Final[str] = 'AUTO' INSENSITIVE: Final[str] = 'INSENSITIVE' LOWER: Final[str] = 'LOWER' @@ -81,11 +87,16 @@ class Case(EnumValue): @unique -class RemoveDomain(EnumValue): +class L2RemoveDomain(EnumValue): ON: Final[str] = 'ON' OFF: Final[str] = 'OFF' AUTO: Final[str] = 'AUTO' +@unique +class L2IgnoreMismatch(EnumValue): + DUPLEX: Final[str] = 'DUPLEX' + SPEED: Final[str] = 'SPEED' + VLAN: Final[str] = 'VLAN' @unique class CacheItems(EnumValue): @@ -107,6 +118,7 @@ class CliLong(EnumValue): L2_CASE: Final[str] = '--l2-case' L2_DISPLAY_NEIGHBOURS: Final[str] = '--l2-display-neighbours' L2_DISPLAY_PORTS: Final[str] = '--l2-display-ports' + L2_IGNORE_MISMATCH: Final[str] = '--l2-ignore-mismatch' L2_PREFIX: Final[str] = '--l2-prefix' L2_REMOVE_DOMAIN: Final[str] = '--l2-remove-domain' L2_SKIP_EXTERNAL: Final[str] = '--l2-skip-external' @@ -285,6 +297,7 @@ class TomlSettings(EnumValue): L2_CASE: Final[str] = cli_long_to_toml(CliLong.L2_CASE) L2_DISPLAY_NEIGHBOURS: Final[str] = cli_long_to_toml(CliLong.L2_DISPLAY_NEIGHBOURS) L2_DISPLAY_PORTS: Final[str] = cli_long_to_toml(CliLong.L2_DISPLAY_PORTS) + L2_IGNORE_MISMATCH: Final[str] = cli_long_to_toml(CliLong.L2_IGNORE_MISMATCH) L2_PREFIX: Final[str] = cli_long_to_toml(CliLong.L2_PREFIX) L2_REMOVE_DOMAIN: Final[str] = cli_long_to_toml(CliLong.L2_REMOVE_DOMAIN) L2_SKIP_EXTERNAL: Final[str] = cli_long_to_toml(CliLong.L2_SKIP_EXTERNAL) diff --git a/source/bin/nvdct/lib/settings.py b/source/bin/nvdct/lib/settings.py index e069f0ccddcaefac6174632fffcf6c9abef030fe..79576921727996d781dd1bc91fc16d801784ec4d 100755 --- a/source/bin/nvdct/lib/settings.py +++ b/source/bin/nvdct/lib/settings.py @@ -15,7 +15,6 @@ from collections.abc import Mapping from ipaddress import AddressValueError, NetmaskValueError, ip_address, ip_network from logging import CRITICAL, DEBUG, ERROR, FATAL, INFO, WARNING from pathlib import Path -from sys import exit as sys_exit from time import strftime from typing import Dict, List, NamedTuple, Set, Tuple @@ -23,16 +22,18 @@ from lib.constants import ( API_PORT_DEFAULT, Backends, CONFIG_FILE, - Case, + CONFIG_PATH_DEFAULT, EmblemNames, EmblemValues, - ExitCodes, IncludeExclude, + L2Case, + L2IgnoreMismatch, + L2RemoveDomain, + Layers, LOGGER, LOG_FILE_DEFAULT, + LOG_PATH_DEFAULT, LogLevels, - OMD_ROOT, - RemoveDomain, TIME_FORMAT_DEFAULT, TomlSections, TomlSettings, @@ -86,7 +87,7 @@ class Settings: TomlSettings.API_PORT: None, TomlSettings.BACKEND: Backends.MULTISITE, TomlSettings.CHECK_CONFIG: False, - TomlSettings.CONFIG: f'{OMD_ROOT}/local/bin/nvdct/conf/{CONFIG_FILE}', + TomlSettings.CONFIG: f'{CONFIG_PATH_DEFAULT}/{CONFIG_FILE}', TomlSettings.DEFAULT: False, TomlSettings.DONT_COMPARE: False, TomlSettings.FILTER_CUSTOMERS: None, @@ -94,6 +95,7 @@ class Settings: TomlSettings.KEEP_MAX_TOPOLOGIES: False, TomlSettings.L2_CASE: None, TomlSettings.L2_DISPLAY_PORTS: False, + TomlSettings.L2_IGNORE_MISMATCH: [], TomlSettings.L2_DISPLAY_NEIGHBOURS: False, TomlSettings.L2_PREFIX: None, TomlSettings.L2_REMOVE_DOMAIN: None, @@ -132,14 +134,6 @@ class Settings: self.__settings.update(self.__user_data.get(TomlSections.SETTINGS, {})) self.__settings.update(self.__args) - if self.layers: - layers = list(set(self.layers)) - if len(layers) != len(self.layers): - # logger not initialized here - # LOGGER.fatal('-l/--layers options must be unique. Don\'t use any layer more than once.') - print('-l/--layers options must be unique. Don\'t use any layer more than once.') - sys_exit(ExitCodes.BAD_OPTION_LIST) - self.__api_port: int | None = None # init user data with defaults @@ -150,6 +144,7 @@ class Settings: self.__filter_by_host_tag: Dict[str, Set[str]] | None = None self.__filter_by_site: List[str] | None = None self.__l2_drop_neighbours: List[str] | None = None + self.__l2_ignore_mismatch: List[str] | None = None self.__l2_neighbour_replace_regex: List[Tuple[str, str]] | None = None self.__l2_neighbour_to_host_map: Dict[str, str] | None = None self.__l2_seed_devices: List[str] | None = None @@ -158,6 +153,7 @@ class Settings: self.__l3_replace: Dict[str, str] | None = None self.__l3_summarize: List[ip_network] | None = None self.__l3v4_ignore_wildcard: List[Wildcard] | None = None + self.__layers: List[str] | None = None self.__map_speed_to_thickness: List[Thickness] | None = None self.__protected_topologies: List[str] | None = None self.__static_connections: List[StaticConnection] | None = None @@ -179,11 +175,7 @@ class Settings: @property # -b --backend def backend(self) -> str: - if str(self.__settings[TomlSettings.BACKEND]) in [ - Backends.LIVESTATUS, - Backends.MULTISITE, - Backends.RESTAPI - ]: + if str(self.__settings[TomlSettings.BACKEND]) in Backends.list(): return str(self.__settings[TomlSettings.BACKEND]) else: # fallback to defaukt -> exit ?? LOGGER.error( @@ -210,7 +202,7 @@ class Settings: @property # --filter-customers def filter_customers(self) -> str | None: - if self.__settings[TomlSettings.FILTER_CUSTOMERS] in [IncludeExclude.INCLUDE, IncludeExclude.EXCLUDE]: + if self.__settings[TomlSettings.FILTER_CUSTOMERS] in IncludeExclude.list(): return self.__settings[TomlSettings.FILTER_CUSTOMERS] elif self.__settings[TomlSettings.FILTER_CUSTOMERS] is not None: LOGGER.error( @@ -221,7 +213,7 @@ class Settings: @property # --filter-sites def filter_sites(self) -> str | None: - if self.__settings[TomlSettings.FILTER_SITES] in [IncludeExclude.INCLUDE, IncludeExclude.EXCLUDE]: + if self.__settings[TomlSettings.FILTER_SITES] in IncludeExclude.list(): return self.__settings[TomlSettings.FILTER_SITES] elif self.__settings[TomlSettings.FILTER_SITES] is not None: LOGGER.error( @@ -232,12 +224,12 @@ class Settings: @property # --l2-case def l2_case(self) -> str | None: - if self.__settings[TomlSettings.L2_CASE] in [Case.LOWER, Case.UPPER, Case.INSENSITIVE, Case.AUTO, Case.OFF]: + if self.__settings[TomlSettings.L2_CASE] in L2Case.list(): return self.__settings[TomlSettings.L2_CASE] elif self.__settings[TomlSettings.L2_CASE] is not None: LOGGER.error( f'Unknown case setting {self.__settings[TomlSettings.L2_CASE]}. ' - f'Accepted are {Case.LOWER}|{Case.UPPER}|{Case.INSENSITIVE}|{Case.AUTO}|{Case.OFF}. Falling back to "OFF" (no change).' + f'Accepted are {L2Case.LOWER}|{L2Case.UPPER}|{L2Case.INSENSITIVE}|{L2Case.AUTO}|{L2Case.OFF}. Falling back to "OFF" (no change).' ) return None @@ -249,6 +241,15 @@ class Settings: def l2_display_neighbours(self) -> bool: return bool(self.__settings[TomlSettings.L2_DISPLAY_NEIGHBOURS]) + @property # --l2-ignore-mismatch + def l2_ignore_mismatch(self) -> List[str]: + if self.__l2_ignore_mismatch is None: + self.__l2_ignore_mismatch = list(set(self.__settings[TomlSettings.L2_IGNORE_MISMATCH])) + self.__l2_ignore_mismatch = [entry for entry in self.__l2_ignore_mismatch if entry in L2IgnoreMismatch.list()] + if len(self.__layers) != len(self.__settings[TomlSettings.LAYERS]): + LOGGER.error(f'Wrong/duplicate l2_mismatch_ignore options ignored, {self.__settings[TomlSettings.L2_IGNORE_MISMATCH]}') + return self.__l2_ignore_mismatch + @property # --l2-prefix def l2_prefix(self) -> str: if self.__settings[TomlSettings.L2_PREFIX] is not None: @@ -257,11 +258,11 @@ class Settings: @property # --l2-remove-domain def l2_remove_domain(self) -> str: - if self.__settings[TomlSettings.L2_REMOVE_DOMAIN] in [RemoveDomain.ON, RemoveDomain.OFF, RemoveDomain.AUTO]: + if self.__settings[TomlSettings.L2_REMOVE_DOMAIN] in L2RemoveDomain.list(): return self.__settings[TomlSettings.L2_REMOVE_DOMAIN] else: - self.__settings[TomlSettings.L2_REMOVE_DOMAIN] = RemoveDomain.OFF - return RemoveDomain.OFF + self.__settings[TomlSettings.L2_REMOVE_DOMAIN] = L2RemoveDomain.OFF + return L2RemoveDomain.OFF @property # --l2-skip-external def l2_skip_external(self) -> bool: @@ -301,12 +302,17 @@ class Settings: @property # --layers def layers(self) -> List[str]: - return self.__settings[TomlSettings.LAYERS] + if self.__layers is None: + self.__layers = list(set(self.__settings[TomlSettings.LAYERS])) + self.__layers = [entry for entry in self.__layers if entry in Layers.list()] + if len(self.__layers) != len(self.__settings[TomlSettings.LAYERS]): + LOGGER.error(f'Wrong/duplicate layer(s) ignored, {self.__settings[TomlSettings.LAYERS]}.') + return self.__layers @property # --log-file def log_file(self) -> str: raw_log_file = str(Path(str(self.__settings[TomlSettings.LOG_FILE])).expanduser()) - if not raw_log_file.startswith(f'{OMD_ROOT}/var/log/'): + if not raw_log_file.startswith(f'{LOG_PATH_DEFAULT}/'): # logger not ready yet print(f'\nInvalid log file {raw_log_file}. Falling back to {LOG_FILE_DEFAULT}') return LOG_FILE_DEFAULT @@ -405,13 +411,13 @@ class Settings: return self.__filter_by_site @staticmethod - def parse_key_value_section(section: str, data: Mapping[str, str]) -> Dict[str, set[str]]: + def parse_key_value_section(section: str, data: Mapping[str, str]) -> Mapping[str, set[str]]: parsed = { IncludeExclude.INCLUDE: set(), IncludeExclude.EXCLUDE: set() } for key_value, mode in data.items(): - if mode not in [IncludeExclude.INCLUDE, IncludeExclude.EXCLUDE]: + if mode not in IncludeExclude.list(): LOGGER.error( f'Invalid mode in {section} found: {key_value}={mode} -> line ignored' ) @@ -438,7 +444,7 @@ class Settings: return parsed @property - def filter_by_folder(self) -> Dict[str, set[str]]: + def filter_by_folder(self) -> Mapping[str, set[str]]: if self.__filter_by_folder is None: self.__filter_by_folder = self.parse_key_value_section( section=TomlSections.FILTER_BY_FOLDER, diff --git a/source/bin/nvdct/lib/topologies.py b/source/bin/nvdct/lib/topologies.py index 7a4634f156a401fd898ecec2cb210fb06a0a5529..69a4b4d5a077acbe59900fdbeebf54fbfba96be5 100755 --- a/source/bin/nvdct/lib/topologies.py +++ b/source/bin/nvdct/lib/topologies.py @@ -29,6 +29,7 @@ from lib.constants import ( HostLabels, IPVersion, InvPaths, + L2IgnoreMismatch, L2InvColumns, L3InvColumns, LOGGER, @@ -382,6 +383,7 @@ class NvConnections: self, nv_objects: NvObjects, speed_map: Sequence[Thickness], + ignore_mismatch: Sequence[str], ): for connection in self.nv_connections: warning = False @@ -411,7 +413,7 @@ class NvConnections: # left_thickness = map_speed_to_thickness(left_speed, speed_map) metadata['line_config']['thickness'] = right_thickness - if right_speed != left_speed: + if right_speed != left_speed and not L2IgnoreMismatch.SPEED in ignore_mismatch: warning = True metadata = add_tooltip_html( metadata, 'Speed', left, left_speed_str, right, right_speed_str @@ -428,7 +430,7 @@ class NvConnections: # for duplex/native vlan it might be a good idea to change left/right # value as they are reported by CDP as neighbour states if left_duplex and right_duplex: - if left_duplex != right_duplex: + if left_duplex != right_duplex and not L2IgnoreMismatch.DUPLEX in ignore_mismatch: warning = True metadata = add_tooltip_html( @@ -439,7 +441,7 @@ class NvConnections: f'Connection duplex mismatch: {left} ({left_duplex})' f'<->{right} ({right_duplex})' ) - if left_native_vlan and right_native_vlan: + if left_native_vlan and right_native_vlan and not L2IgnoreMismatch.VLAN in ignore_mismatch: if left_native_vlan != '0' and right_native_vlan != '0': # ignore VLAN 0 (Native VLAN on routed ports) if left_native_vlan != right_native_vlan: warning = True @@ -710,7 +712,7 @@ class TopologyL2(Topology): self.hosts_to_go: MutableSet[str] = set(seed_devices) self.label: str = label self.neighbour_replace_regex: List[Tuple[str, str]] = neighbour_replace_regex - self.neighbour_to_host: MutableMapping[str, str] = {} + self.neighbour_to_host: MutableMapping[str, str | None] = {} self.path_in_inventory: str = path_in_inventory self.raw_neighbour_to_neighbour: Dict[str, str] = {} self.skip_external: bool = skip_external @@ -910,7 +912,7 @@ class TopologyL3(Topology): super().__init__( emblems=emblems, host_cache=host_cache, - topology=f'[L3 IPv{version}]' + topology=f'[L3 IPv{version}]', ) self.diplay_devices = display_devices self.ignore_hosts: Sequence[str] = ignore_hosts diff --git a/source/bin/nvdct/lib/update_config.py b/source/bin/nvdct/lib/update_config.py new file mode 100755 index 0000000000000000000000000000000000000000..b1f52fb4f33924560d091043526d5a514470f5d3 --- /dev/null +++ b/source/bin/nvdct/lib/update_config.py @@ -0,0 +1,402 @@ +#!/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 : 202-01-03 +# File : nvdct/lib/adjust_toml.py + + +from pathlib import Path +from re import findall as re_findall, sub as re_sub +from sys import exit as sys_exit + +from lib.constants import ( + API_PORT_DEFAULT, + Backends, + EmblemNames, + EmblemValues, + ExitCodes, + IncludeExclude, + L2Case, + L2IgnoreMismatch, + L2RemoveDomain, + Layers, + LogLevels, + TomlSections, + TomlSettings, +) + + +def update_config(toml_file: str): + fix_options = { + 'DROP_HOSTS': TomlSections.L2_DROP_NEIGHBOURS, + 'HOST_MAP': TomlSections.L2_NEIGHBOUR_TO_HOST_MAP, + 'L2_DROP_HOSTS': TomlSections.L2_DROP_NEIGHBOURS, + 'L2_HOST_MAP': TomlSections.L2_NEIGHBOUR_TO_HOST_MAP, + 'L3V4_IGNORE_HOSTS': TomlSections.L3_IGNORE_HOSTS, + 'L3V4_IGNORE_IP': TomlSections.L3_IGNORE_IP, + 'L3V4_IRNORE_WILDCARD': TomlSections.L3V4_IGNORE_WILDCARD, + 'L3V4_REPLACE': TomlSections.L3_REPLACE_NETWORKS, + 'L3V3_REPLACE': TomlSections.L3_REPLACE_NETWORKS, + 'L3_REPLACE': TomlSections.L3_REPLACE_NETWORKS, + 'L3V4_SUMMARIZE': TomlSections.L3_SUMMARIZE, + 'SEED_DEVICES': TomlSections.L2_SEED_DEVICES, + 'SITES': TomlSections.FILTER_BY_SITE, + 'CUSTOMERS': TomlSections.FILTER_BY_CUSTOMER, + 'icon_missinc': EmblemValues.ICON_ALERT_UNREACHABLE, + 'icon_missing': EmblemValues.ICON_ALERT_UNREACHABLE, + 'l3v4_replace': EmblemNames.L3_REPLACE, + 'l3v4_summarize': EmblemNames.L3_SUMMARIZE, + 'keep_domain = true': f'{TomlSettings.L2_REMOVE_DOMAIN} = "{L2RemoveDomain.OFF}"', + 'keep_domain = false': f'{TomlSettings.L2_REMOVE_DOMAIN} = "{L2RemoveDomain.ON}"', + 'case': TomlSettings.L2_CASE, + 'prefix': TomlSettings.L2_PREFIX, + 'remove_domain': TomlSettings.L2_REMOVE_DOMAIN, + 'keep': TomlSettings.KEEP_MAX_TOPOLOGIES, + 'min_age': TomlSettings.MIN_TOPOLOGY_AGE, + 'display_l2_neighbours': TomlSettings.L2_DISPLAY_NEIGHBOURS, + 'include_l3_hosts': TomlSettings.L3_INCLUDE_HOSTS, + 'include_l3_loopback': TomlSettings.L3_INCLUDE_LOOPBACK, + 'skip_l3_cidr_0': TomlSettings.L3_SKIP_CIDR_0, + 'skip_l3_cidr_32_128': TomlSettings.L3_SKIP_CIDR_32_128, + 'skip_l3_if': TomlSettings.L3_SKIP_IF, + 'skip_l3_ip': TomlSettings.L3_SKIP_IP, + 'skip_l3_public': TomlSettings.L3_SKIP_PUBLIC, + } + + fix_params = { + f'# {TomlSettings.L2_CASE} = "{L2Case.LOWER}" | "{L2Case.UPPER}"\n': f'# {TomlSettings.L2_CASE} = "{L2Case.LOWER}" | "{L2Case.UPPER}" | {L2Case.INSENSITIVE} | "{L2Case.AUTO}" | "{L2Case.OFF}"\n', + f'# {TomlSettings.L2_CASE} = "{L2Case.LOWER}\n"' : f'# {TomlSettings.L2_CASE} = "{L2Case.LOWER}" | "{L2Case.UPPER}" | {L2Case.INSENSITIVE} | "{L2Case.AUTO}" | "{L2Case.OFF}"\n', + f'# {TomlSettings.L2_CASE} = "{L2Case.UPPER}\n"': f'# {TomlSettings.L2_CASE} = "{L2Case.LOWER}" | "{L2Case.UPPER}" | {L2Case.INSENSITIVE} | "{L2Case.AUTO}" | "{L2Case.OFF}\n', + f'# {TomlSettings.L2_REMOVE_DOMAIN} = false | true\n': f'# {TomlSettings.L2_REMOVE_DOMAIN} = "{L2RemoveDomain.OFF}" | "{L2RemoveDomain.ON}" | "{L2RemoveDomain.AUTO}"\n', + f'# {TomlSettings.L2_REMOVE_DOMAIN} = false\n': f'# {TomlSettings.L2_REMOVE_DOMAIN} = "{L2RemoveDomain.OFF}" | "{L2RemoveDomain.ON}" | "{L2RemoveDomain.AUTO}"\n', + f'# {TomlSettings.L2_REMOVE_DOMAIN} = true\n': f'# {TomlSettings.L2_REMOVE_DOMAIN} = "{L2RemoveDomain.OFF}" | "{L2RemoveDomain.ON}" | "{L2RemoveDomain.AUTO}"\n', + f'{TomlSettings.L2_REMOVE_DOMAIN} = false\n': f'{TomlSettings.L2_REMOVE_DOMAIN} = "{L2RemoveDomain.OFF}"\n', + f'{TomlSettings.L2_REMOVE_DOMAIN} = true\n': f'{TomlSettings.L2_REMOVE_DOMAIN} = "{L2RemoveDomain.ON}"\n', + } + + old_options = { + 'CUSTOM_LAYERS': f'Can be removed (no longer supported).', + 'FILESYSTEM': f'Use {Backends.MULTISITE} instead.', + 'debug': f'Use "{TomlSettings.LOG_LEVEL} = {LogLevels.DEBUG}" instead.', + 'lowercase': f'Use "{TomlSettings.L2_CASE} = {L2Case.LOWER}" instead.', + 'uppercase': f'Use "{TomlSettings.L2_CASE} = {L2Case.UPPER}" instead.', + 'CUSTOM': f'Is no loger supported. remove it from layers please.' + } + # sorted in reverse + missing_settings = { + TomlSettings.TIME_FORMAT: '"%Y-%m-%dT%H:%M:%S.%m"', + TomlSettings.QUIET: 'false | true', + TomlSettings.PRE_FETCH: 'false | true', + TomlSettings.OUTPUT_DIRECTORY: '"nvdct" # remove to get date formated directory', + TomlSettings.MIN_TOPOLOGY_AGE: '1', + TomlSettings.LOG_TO_STDOUT: 'false | true', + TomlSettings.LOG_LEVEL: f'"{LogLevels.WARNING}" | "{LogLevels.DEBUG}" | "{LogLevels.INFO}" | "{LogLevels.ERROR}" | "{LogLevels.FATAL}" | {LogLevels.CRITICAL}" | "{LogLevels.OFF}"', + TomlSettings.LOG_FILE: '"~/var/log/nvdct.log"', + TomlSettings.LAYERS: f'["{Layers.CDP}", "{Layers.LLDP}", "{Layers.L3V4}", "{Layers.STATIC}"]', + TomlSettings.L3_SKIP_PUBLIC: 'false | true', + TomlSettings.L3_SKIP_IP: 'false | true', + TomlSettings.L3_SKIP_IF: 'false | true', + TomlSettings.L3_SKIP_CIDR_32_128: 'false | true', + TomlSettings.L3_SKIP_CIDR_0: 'false | true', + TomlSettings.L3_INCLUDE_LOOPBACK: 'false | true # most likely dropped from inventory (SNMP) before', + TomlSettings.L3_INCLUDE_HOSTS: 'false | true', + TomlSettings.L3_DISPLAY_DEVICES: 'false | true', + TomlSettings.L2_SKIP_EXTERNAL: 'false | true', + TomlSettings.L2_REMOVE_DOMAIN: f'"{L2RemoveDomain.OFF}" | "{L2RemoveDomain.ON}" | "{L2RemoveDomain.AUTO}"', + TomlSettings.L2_PREFIX: '""', + TomlSettings.L2_IGNORE_MISMATCH: f'["{L2IgnoreMismatch.DUPLEX}", "{L2IgnoreMismatch.SPEED}", "{L2IgnoreMismatch.VLAN}"]', + TomlSettings.L2_DISPLAY_NEIGHBOURS: 'false | true', + TomlSettings.L2_DISPLAY_PORTS: 'false | true', + TomlSettings.L2_CASE: f'"{L2Case.OFF}" | "{L2Case.LOWER}" | "{L2Case.UPPER}" | {L2Case.INSENSITIVE} | "{L2Case.AUTO}"', + TomlSettings.KEEP_MAX_TOPOLOGIES: '10', + TomlSettings.FILTER_SITES: f'"{IncludeExclude.INCLUDE}" | "{IncludeExclude.EXCLUDE}"', + TomlSettings.FILTER_CUSTOMERS: f'"{IncludeExclude.INCLUDE}" | "{IncludeExclude.EXCLUDE}"', + TomlSettings.DONT_COMPARE: 'false | true', + TomlSettings.DEFAULT: 'false | true', + TomlSettings.BACKEND: f'"{Backends.MULTISITE}" | "{Backends.RESTAPI}" | "{Backends.LIVESTATUS}"', + TomlSettings.API_PORT: API_PORT_DEFAULT, + } + + l2_neighbour_to_host_map = r''' +# map inventory CDP/LLDP neighbour name to Checkmk host name +# [0-9-a-zA-Z\.\_\-]{1,253} -> host + +# "inventory_neighbour1" = "cmk_host1" +# "inventory_neighbour2" = "cmk_host2" +# "inventory_neighbour3" = "cmk_host3" + ''' + + l2_neighbour_replace_regex = r''' +# modify CDP/LLDP neighbour name with regex before mapping to CMK host names + +# "regex string to replace" = "string to replace with" +# "^(([0-9a-fA-F]){2}[:.-]?){5}([0-9a-fA-F]){2}$" = "" +# "\\([0-9a-zA-Z]+\\)$" = "" +# "^Meraki.*\\s-\\s" = "" + ''' + + l3_replace_networks = r''' +# replace _network objects_ in L§ topologies (takes place after summarize) +# [0-9-a-zA-Z\.\_\-]{1,253} -> host + +# "10.193.172.0/24" = "MPLS" +# "10.194.8.0/23" = "MPLS" +# "10.194.12.0/24" = "MPLS" +# "10.194.115.0/24" = "MPLS" +# "fc00::/7" = "Unique-local" + ''' + + emblems = f''' +# can use misc icons from CMK or upload your own in the misc category +# for built-in icons use "icon_" as l2_prefix to the name from CMK +# max size 80x80px +# emblems will only be used for non CMK objects + +# "{EmblemNames.HOST_NODE}" = "{EmblemValues.ICON_ALERT_UNREACHABLE}" +# "{EmblemNames.IP_ADDRESS}" = "{EmblemValues.IP_ADDRESS_80}" +# "{EmblemNames.IP_NETWORK}" = "{EmblemValues.IP_NETWORK_80}" +# "{EmblemNames.L3_REPLACE}" = "{EmblemValues.ICON_PLUGINS_CLOUD}" +# "{EmblemNames.L3_SUMMARIZE}" = "{EmblemValues.ICON_AGGREGATION}" +# "{EmblemNames.SERVICE_NODE}" = "{EmblemValues.ICON_ALERT_UNREACHABLE}" + ''' + + map_speed_to_thickness = r''' +# must be sorted from slower to faster speed +# use only one/no entry to have all connections with the same thickness +# "bits per second" = thickness + +# "2000000" = 1 # 2 mbit +# "5000000" = 2 # 5 mbit +# "1e7" = 3 # 10 mbit +# "51e7" = 4 # 51 mbit +"1e8" = 1 # 100 mbit +"1e9" = 3 # 1 gbit +"1e10" = 5 # 10 gbit + ''' + + filter_by_folder = f''' +# "/folder1/subfolder1" = "{IncludeExclude.INCLUDE}" | "{IncludeExclude.EXCLUDE}" +# "/folder2/subfolder2" = "{IncludeExclude.INCLUDE}" | "{IncludeExclude.EXCLUDE}" +''' + + filter_host_by_label = f''' +# "hostlabel1:value" = "{IncludeExclude.INCLUDE}" | "{IncludeExclude.EXCLUDE}" +# "hostlabel2:value" = "{IncludeExclude.INCLUDE}" | "{IncludeExclude.EXCLUDE}" +''' + + filter_host_by_tag = f''' +# "host_tag1:value" = "{IncludeExclude.INCLUDE}" | "{IncludeExclude.EXCLUDE}" +# "host_tag2:value" = "{IncludeExclude.INCLUDE}" | "{IncludeExclude.EXCLUDE}" + ''' + + missing_tables = { + TomlSections.L2_NEIGHBOUR_TO_HOST_MAP: l2_neighbour_to_host_map, + TomlSections.L2_NEIGHBOUR_REPLACE_REGEX: l2_neighbour_replace_regex, + TomlSections.L3_REPLACE_NETWORKS: l3_replace_networks, + TomlSections.EMBLEMS: emblems, + TomlSections.MAP_SPEED_TO_THICKNESS: map_speed_to_thickness, + TomlSections.FILTER_BY_FOLDER: filter_by_folder, + TomlSections.FILTER_BY_HOST_LABEL: filter_host_by_label, + TomlSections.FILTER_BY_HOST_TAG: filter_host_by_tag, + } + + l2_seed_devices = r''' = [ + # list of CDP/LLDP seed devices (if empty, all CDP/LLDP devices will be used) + # [0-9-a-zA-Z\.\_\-]{1,253} -> host + + # "CORE01", + # "LOCATION01", + # "LOCATION02", +] + ''' + + l2_drop_neighbours = r''' = [ + # drop CDP/LLDP neighbours names + + # "not advertised", + # "a nother invalid name", +] + ''' + + l3_ignore_hosts = r''' = [ + # hosts will be ignored in L3 topologies + # [0-9-a-zA-Z\.\_\-]{1,253} -> host + + # "host1", + # "host2", +] + ''' + + l3_ignore_ip = r''' = [ + # drop IP address that matches ip/network + + # "192.168.100.231", + # "192.168.100.0/16", + # "192.168.150.0/255.255.255.0", + # "fd00::1" + # "fd00::/8" +] + ''' + + l3v4_ignore_wildcard = r''' = [ + # ignore IPs by wildcard + # if comparing an ip address: + # each 0 bit in the wildcard has to be exactly as in the pattern + # each 1 bit in the wildcard will be ignored + + # [ pattern , wildcard ] + # ["172.17.0.1", "0.0.255.0"], # ignore all IPs ending with 1 from 172.17.0.0/16 + # ["172.17.128.0", "0.0.127.3"], # ignore all IPs ending with 0-3 from 172.17.128.0/17 + # ["172.17.128.3", "0.0.127.0"], # ignore all IPs ending with 3 from 172.17.128.0/17 +] +''' + + l3_summarize = r''' = [ + # IP _networks_ to summarize + + # "10.193.172.0/24", + # "10.194.8.0/23", + # "10.194.12.0/24", + # "10.194.115.0/255.255.255.0", + # "fd00::/8" +] + ''' + + protected_topologies = r''' = [ + # topologies will not be deleted by "--keep_max_topologies" + + # "2023-10-17T14:08:05.10", + # "your_important_topology" +] + ''' + + static_connections = r''' = [ + # user defined static connections + # [0-9-a-zA-Z\.\_\-]{1,253} -> host + + # valid entry formats + # ["left_host", "left_service", "right_service", "right_host"], + # connection: "left_host"<->"left_service"<->"right_service"<->"right_host" + # ["left_host", "", "right_service", "right_host"], + # connection: "left_host"<->"right_service"<->"right_host" + # ["left_host", "left_service", "", "right_host"], + # connection: "left_host"<->"left_service"<->"right_host" + # ["left_host", "", "", "right_host"], + # connection: "left_host"<->"right_host" +] + ''' + + filter_by_customer = r''' = [ + # list customers to include/exclude, use with option --filter-costumers INCLUDE/EXCLUDE + # [0-9-a-zA-Z\.\_\-]{1,16} -> customer + + # "customer1", + # "customer2", + # "customer3" +] + ''' + + filter_by_site = r''' = [ + # list site to include/exclude, use with option --filter-sites INCLUDE/EXCLUDE + # [0-9-a-zA-Z\.\_\-]{1,16} -> site + + # "site1", + # "site2", + # "site3", +] + ''' + + missing_arrays = { + TomlSections.FILTER_BY_SITE: filter_by_site, + TomlSections.FILTER_BY_CUSTOMER: filter_by_customer, + TomlSections.STATIC_CONNECTIONS: static_connections, + TomlSections.PROTECTED_TOPOLOGIES: protected_topologies, + TomlSections.L3_SUMMARIZE: l3_summarize, + TomlSections.L3V4_IGNORE_WILDCARD: l3v4_ignore_wildcard, + TomlSections.L3_IGNORE_IP: l3_ignore_ip, + TomlSections.L3_IGNORE_HOSTS: l3_ignore_hosts, + TomlSections.L2_DROP_NEIGHBOURS: l2_drop_neighbours, + TomlSections.L2_SEED_DEVICES: l2_seed_devices, + } + + changed: bool = False + org_file = Path(toml_file) + if not org_file.exists(): + print(f'Config file {org_file.name} not found!') + sys_exit(ExitCodes.FILE_NOT_FOUND) + + print(f'Checking file.: {org_file.name}') + org_content: str = org_file.read_text() + content: str = org_content + for old, new in fix_options.items(): + re_pattern = f'\\b{old}\\b' + count = len(re_findall(re_pattern, content)) + if count > 0: + changed = True + content = re_sub(re_pattern, new, content) + print(f'Found value...: "{old}" {count} times, replaced by "{new}"') + + for old, new in fix_params.items(): + if old in content: + changed = True + content = content.replace(old, new) + new_line = '\n' + print(f'Found value...: "{old.replace(new_line, "")}", replaced by "{new.replace(new_line, "")}"') + + for missing_setting, value in missing_settings.items(): + re_pattern = f'\\b{missing_setting}\\b' + count = len(re_findall(re_pattern, content)) + if count == 0: + changed = True + content = re_sub( + f'\\[{TomlSections.SETTINGS}\\]\n', + f'[{TomlSections.SETTINGS}]\n# {missing_setting} = {value}\n', + content + ) + print(f'Added option..: "# {missing_setting} = {value}"') + + for table, value in missing_tables.items(): + re_pattern = f'\\[{table}\\]\n' + count = len(re_findall(re_pattern, content)) + if count == 0: + changed = True + content = re_sub( + f'\\[{TomlSections.SETTINGS}\\]\n', + f'[{table}]{value}\n[{TomlSections.SETTINGS}]\n', + content + ) + print(f'Added section.: "[{table}]"') + + for array, value in missing_arrays.items(): + re_pattern = f'\\b{array}\\b' + count = len(re_findall(re_pattern, content)) + if count == 0: + changed = True + content = content.replace(']\n\n#', f']\n\n#\n{array}{value}\n#', 1) + print(f'Added section.: "{array} = []"') + pass + + for old, new in old_options.items(): + re_pattern = f'\\b{old}\\b' + count = len(re_findall(re_pattern, org_content)) + if count > 0: + print(f'Obsolete......: "{old}", {new}') + + if changed: + backup_file = Path(f'{toml_file}.backup') + if not backup_file.exists(): + org_file.rename(backup_file) + print(f'Renamed TOML..: {backup_file.name}') + new_file = Path(toml_file) + new_file.open('w').write(content) + print(f'Written fixed.: {new_file.name}') + else: + print( + f'Can not create backup file {backup_file.name}, file exists. Aborting!\n' + f'Nothing has changed.' + ) + else: + print('Finished......: Nothing found to fix.') diff --git a/source/bin/nvdct/lib/utils.py b/source/bin/nvdct/lib/utils.py index 4eb62eeee370b7a302cf9b3812d6ec1f55a9ea69..98878f4ce5e833bd828737b2443398f4bb7644bb 100755 --- a/source/bin/nvdct/lib/utils.py +++ b/source/bin/nvdct/lib/utils.py @@ -237,7 +237,6 @@ def is_valid_output_directory(directory: str) -> bool: re_host_pattern = r'^[0-9a-z-A-Z\.\-\_\:]{1,30}$' if re_match(re_host_pattern, directory): return True - return True else: LOGGER.error(f'Invalid output directory name found: {directory}') return False diff --git a/source/bin/nvdct/nvdct.py b/source/bin/nvdct/nvdct.py index 48d47168211fa332708f76952c2b8c6ba03f47c3..2e59506abed001472a37e813158d7fe1e1c059d3 100755 --- a/source/bin/nvdct/nvdct.py +++ b/source/bin/nvdct/nvdct.py @@ -221,6 +221,7 @@ # fixed REST API query for interface services # 2025-01-24: added option --l2-display-ports, --l3-display-devices # 2025-02-05: added option "OFF" to --l2-case +# 2025-02-07: added option --l2-ignore-mismatch # # creating topology data json from inventory data # @@ -541,6 +542,7 @@ def main(): topology.nv_connections.add_meta_data_to_connections( nv_objects=topology.nv_objects, speed_map=settings.map_speed_to_thickness, + ignore_mismatch=settings.l2_ignore_mismatch, ) topology.save( diff --git a/source/packages/nvdct b/source/packages/nvdct index d277e55d94d95f7fc2f230b44f2e30ff6993ff98..5c37104728f9ef918cc9c7a1a7dc2a1f712da759 100644 --- a/source/packages/nvdct +++ b/source/packages/nvdct @@ -40,14 +40,15 @@ 'nvdct/lib/__init__.py', 'nvdct/conf/nvdct.toml', 'nvdct/lib/topologies.py', - 'nvdct/lib/constants.py'], + 'nvdct/lib/constants.py', + 'nvdct/lib/update_config.py'], 'web': ['htdocs/images/icons/cloud_80.png', 'htdocs/images/icons/ip-address_80.png', 'htdocs/images/icons/ip-network_80.png', 'htdocs/images/icons/location_80.png']}, 'name': 'nvdct', 'title': 'Network Visualization Data Creation Tool (NVDCT)', - 'version': '0.9.8-20250205', + 'version': '0.9.9-20250214', 'version.min_required': '2.3.0b1', 'version.packaged': 'cmk-mkp-tool 0.2.0', 'version.usable_until': '2.4.0p1'}