Collection of CheckMK checks (see https://checkmk.com/). All checks and plugins are provided as is. Absolutely no warranty. Send any comments to thl-cmk[at]outlook[dot]com

Skip to content
Snippets Groups Projects
Commit fbefde6b authored by thl-cmk's avatar thl-cmk :flag_na:
Browse files

fixed MKP, mising update_config.py

 Please enter the commit message for your changes. Lines starting
parent 1a7ee52e
No related branches found
No related tags found
No related merge requests found
[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) # 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.\ This script creates the topology data file needed for the [Checkmk Exchange Network visualization](https://exchange.checkmk.com/p/network-visualization) plugin.\
......
File added
...@@ -168,6 +168,7 @@ FILTER_BY_SITE = [ ...@@ -168,6 +168,7 @@ FILTER_BY_SITE = [
# l2_case = "OFF" | "LOWER" | "UPPER" | "IGNORE" | "AUTO" # l2_case = "OFF" | "LOWER" | "UPPER" | "IGNORE" | "AUTO"
# l2_display_neighbours = false | true # l2_display_neighbours = false | true
# l2_display_ports = false | true # l2_display_ports = false | true
# l2_ignore_mismatch = ["DUPLEX", "SPEED", "VLAN"]
# l2_prefix = "" # l2_prefix = ""
# l2_remove_domain = "OFF" | "ON" | "AUTO" # l2_remove_domain = "OFF" | "ON" | "AUTO"
# l2_skip_external = false | true # l2_skip_external = false | true
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
# --l2-case # --l2-case
# --l2-display-ports # --l2-display-ports
# --l2-display-neighbours # --l2-display-neighbours
# --l2-ignore-mismatch
# --l2-prefix # --l2-prefix
# --l2-remove-domain # --l2-remove-domain
# --l2-skip-external # --l2-skip-external
...@@ -55,16 +56,17 @@ from pathlib import Path ...@@ -55,16 +56,17 @@ from pathlib import Path
from lib.constants import ( from lib.constants import (
Backends, Backends,
CONFIG_FILE, CONFIG_FILE,
Case,
CliLong, CliLong,
CliShort, CliShort,
ExitCodes, ExitCodes,
IncludeExclude, IncludeExclude,
L2Case,
L2IgnoreMismatch,
L2RemoveDomain,
Layers, Layers,
LogLevels, LogLevels,
MinVersions, MinVersions,
NVDCT_VERSION, NVDCT_VERSION,
RemoveDomain,
SCRIPT, SCRIPT,
TIME_FORMAT_ARGPARSER, TIME_FORMAT_ARGPARSER,
TomlSections, TomlSections,
...@@ -95,11 +97,11 @@ def parse_arguments() -> arg_Namespace: ...@@ -95,11 +97,11 @@ def parse_arguments() -> arg_Namespace:
f' {ExitCodes.AUTOMATION_SECRET_NOT_FOUND} - Automation secret not found\n' f' {ExitCodes.AUTOMATION_SECRET_NOT_FOUND} - Automation secret not found\n'
f' {ExitCodes.NO_LAYER_CONFIGURED} - No layer to work on\n' f' {ExitCodes.NO_LAYER_CONFIGURED} - No layer to work on\n'
'\nUsage:\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( parser.add_argument(
CliShort.BACKEND, CliLong.BACKEND, CliShort.BACKEND, CliLong.BACKEND,
choices=[Backends.LIVESTATUS, Backends.MULTISITE, Backends.RESTAPI], choices=Backends.list(),
# default='MULTISITE', # default='MULTISITE',
help='Backend used to retrieve the topology data\n' help='Backend used to retrieve the topology data\n'
f' - {Backends.LIVESTATUS} : fetches data via local Livestatus (local site only)\n' f' - {Backends.LIVESTATUS} : fetches data via local Livestatus (local site only)\n'
...@@ -119,13 +121,7 @@ def parse_arguments() -> arg_Namespace: ...@@ -119,13 +121,7 @@ def parse_arguments() -> arg_Namespace:
parser.add_argument( parser.add_argument(
CliShort.LAYERS, CliLong.LAYERS, CliShort.LAYERS, CliLong.LAYERS,
nargs='+', nargs='+',
choices=[ choices=Layers.list(),
Layers.CDP,
Layers.LLDP,
Layers.L3V4,
Layers.STATIC,
],
# default=['CDP'],
help=( help=(
f' - {Layers.CDP} : needs inv_cdp_cache package at least in version {MinVersions.CDP}\n' 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' f' - {Layers.LLDP} : needs inv_lldp_cache package at least in version {MinVersions.LLDP}\n'
...@@ -166,14 +162,14 @@ def parse_arguments() -> arg_Namespace: ...@@ -166,14 +162,14 @@ def parse_arguments() -> arg_Namespace:
) )
parser.add_argument( parser.add_argument(
CliLong.FILTER_CUSTOMERS, CliLong.FILTER_CUSTOMERS,
choices=[IncludeExclude.INCLUDE, IncludeExclude.EXCLUDE], choices=IncludeExclude.list(),
# default='INCLUDE', # default='INCLUDE',
help=f'{IncludeExclude.INCLUDE}/{IncludeExclude.EXCLUDE} customer list "[{TomlSections.FILTER_BY_CUSTOMER}]" from TOML file.' help=f'{IncludeExclude.INCLUDE}/{IncludeExclude.EXCLUDE} customer list "[{TomlSections.FILTER_BY_CUSTOMER}]" from TOML file.'
f'NOTE: {Backends.MULTISITE} backend only.', f'NOTE: {Backends.MULTISITE} backend only.',
) )
parser.add_argument( parser.add_argument(
CliLong.FILTER_SITES, CliLong.FILTER_SITES,
choices=[IncludeExclude.EXCLUDE, IncludeExclude.EXCLUDE], choices=IncludeExclude.list(),
# default='INCLUDE', # default='INCLUDE',
help=f'{IncludeExclude.INCLUDE}/{IncludeExclude.EXCLUDE} site list "[{TomlSections.FILTER_BY_SITE}]" from TOML file.' help=f'{IncludeExclude.INCLUDE}/{IncludeExclude.EXCLUDE} site list "[{TomlSections.FILTER_BY_SITE}]" from TOML file.'
) )
...@@ -185,13 +181,13 @@ def parse_arguments() -> arg_Namespace: ...@@ -185,13 +181,13 @@ def parse_arguments() -> arg_Namespace:
) )
parser.add_argument( parser.add_argument(
CliLong.L2_CASE, CliLong.L2_CASE,
choices=[Case.INSENSITIVE, Case.LOWER, Case.UPPER, Case.AUTO, Case.OFF], choices=L2Case.list(),
help='Change L2 neighbour name case before matching to Checkmk host.\n' 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'- {L2Case.OFF} : Do not change the case of the neighbour name\n'
f'- {Case.INSENSITIVE} : search for a matching host by ignoring the case of neighbour name and host name\n' f'- {L2Case.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'- {L2Case.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'- {L2Case.UPPER} : change to all upper case, without the domain part of the neighbour name\n'
f'- {Case.AUTO} : try all the above variants\n' f'- {L2Case.AUTO} : try all the above variants\n'
f'Default is let the case of the neighbour name untouched ("OFF").\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}"', f'Takes place after "{CliLong.L2_REMOVE_DOMAIN}" and before "{CliLong.L2_PREFIX}"',
) )
...@@ -203,6 +199,16 @@ def parse_arguments() -> arg_Namespace: ...@@ -203,6 +199,16 @@ def parse_arguments() -> arg_Namespace:
CliLong.L2_DISPLAY_NEIGHBOURS, action='store_const', const=True, # default=False, CliLong.L2_DISPLAY_NEIGHBOURS, action='store_const', const=True, # default=False,
help='Use L2 neighbour name as display name in L2 topologies', 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( parser.add_argument(
CliLong.L2_PREFIX, type=str, CliLong.L2_PREFIX, type=str,
help=f'Prepends each L2 neighbour name with the prefix before matching to a Checkmk host name.\n' 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: ...@@ -210,12 +216,12 @@ def parse_arguments() -> arg_Namespace:
) )
parser.add_argument( parser.add_argument(
CliLong.L2_REMOVE_DOMAIN, 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' 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'- {L2RemoveDomain.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'- {L2RemoveDomain.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'- {L2RemoveDomain.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'Default: "{L2RemoveDomain.OFF}". Takes place after "{CliLong.L2_REMOVE_DOMAIN}" and before "{CliLong.L2_PREFIX}"',
) )
parser.add_argument( parser.add_argument(
CliLong.L2_SKIP_EXTERNAL, action='store_const', const=True, # default=False, CliLong.L2_SKIP_EXTERNAL, action='store_const', const=True, # default=False,
...@@ -260,15 +266,7 @@ def parse_arguments() -> arg_Namespace: ...@@ -260,15 +266,7 @@ def parse_arguments() -> arg_Namespace:
parser.add_argument( parser.add_argument(
CliLong.LOG_LEVEL, CliLong.LOG_LEVEL,
# nargs='+', # nargs='+',
choices=[ choices=LogLevels.list(),
LogLevels.CRITICAL,
LogLevels.FATAL,
LogLevels.ERROR,
LogLevels.WARNING,
LogLevels.INFO,
LogLevels.DEBUG,
LogLevels.OFF
],
# default='WARNING', # default='WARNING',
help=f'Sets the log level. The default is "{LogLevels.WARNING}"' help=f'Sets the log level. The default is "{LogLevels.WARNING}"'
) )
...@@ -295,6 +293,6 @@ def parse_arguments() -> arg_Namespace: ...@@ -295,6 +293,6 @@ def parse_arguments() -> arg_Namespace:
) )
parser.add_argument( parser.add_argument(
CliLong.UPDATE_CONFIG, action='store_const', const=True, # default=False, 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() return parser.parse_args()
...@@ -31,16 +31,16 @@ from lib.constants import ( ...@@ -31,16 +31,16 @@ from lib.constants import (
Backends, Backends,
CACHE_INTERFACES_DATA, CACHE_INTERFACES_DATA,
CacheItems, CacheItems,
Case,
ExitCodes, ExitCodes,
HostFilter, HostFilter,
IncludeExclude, IncludeExclude,
InvPaths, InvPaths,
L2Case,
L2InvColumns, L2InvColumns,
L2RemoveDomain,
LiveStatusOperator, LiveStatusOperator,
MIN_CMK_VERSION_POST, MIN_CMK_VERSION_POST,
OMD_ROOT, OMD_ROOT,
RemoveDomain,
) )
from lib.utils import ( from lib.utils import (
LOGGER, LOGGER,
...@@ -62,7 +62,7 @@ class HostCache: ...@@ -62,7 +62,7 @@ class HostCache:
LOGGER.info(f'{backend} init HOST_CACHE') LOGGER.info(f'{backend} init HOST_CACHE')
self.cache: Dict = {} 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._inventory_pre_fetch_list: List[str] = [InvPaths.INTERFACES]
self.backend: str = str(backend) self.backend: str = str(backend)
...@@ -71,7 +71,7 @@ class HostCache: ...@@ -71,7 +71,7 @@ class HostCache:
self.l2_neighbour_replace_regex: List[Tuple[str, str]] = [] self.l2_neighbour_replace_regex: List[Tuple[str, str]] = []
self.pre_fetch: bool = bool(pre_fetch) self.pre_fetch: bool = bool(pre_fetch)
self.prefix: str = '' self.prefix: str = ''
self.remove_domain: str = RemoveDomain.OFF self.remove_domain: str = L2RemoveDomain.OFF
self.filter_include: MutableSequence[str] = [] self.filter_include: MutableSequence[str] = []
self.filter_exclude: MutableSequence[str] = [] self.filter_exclude: MutableSequence[str] = []
self.no_case_host_map: MutableMapping[str, str] = {} self.no_case_host_map: MutableMapping[str, str] = {}
...@@ -94,9 +94,9 @@ class HostCache: ...@@ -94,9 +94,9 @@ class HostCache:
def init_filter_lists( def init_filter_lists(
self, self,
filter_by_folder: Mapping[str, Sequence[str]], filter_by_folder: Mapping[str, set[str]],
filter_by_host_label: Mapping[str, Sequence[str]], filter_by_host_label: Mapping[str, set[str]],
filter_by_host_tag: Mapping[str, Sequence[str]], filter_by_host_tag: Mapping[str, set[str]],
): ):
for folder in filter_by_folder[IncludeExclude.INCLUDE]: for folder in filter_by_folder[IncludeExclude.INCLUDE]:
self.filter_include += self.query_hosts_by_filter(HostFilter.FOLDER, folder, LiveStatusOperator.SUPERSET) self.filter_include += self.query_hosts_by_filter(HostFilter.FOLDER, folder, LiveStatusOperator.SUPERSET)
...@@ -243,7 +243,7 @@ class HostCache: ...@@ -243,7 +243,7 @@ class HostCache:
Returns: None, the data is directly writen to self.cache 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: if inventory_of_hosts:
for host, inventory in inventory_of_hosts.items(): for host, inventory in inventory_of_hosts.items():
if host not in self.cache: if host not in self.cache:
...@@ -343,30 +343,30 @@ class HostCache: ...@@ -343,30 +343,30 @@ class HostCache:
host_no_domain: str = host.split('.')[0] host_no_domain: str = host.split('.')[0]
match self.remove_domain: match self.remove_domain:
case RemoveDomain.ON: case L2RemoveDomain.ON:
LOGGER.debug(f'{self.backend} Remove domain: {host} -> {host_no_domain}') LOGGER.debug(f'{self.backend} Remove domain: {host} -> {host_no_domain}')
host = host_no_domain host = host_no_domain
possible_hosts.add(host_no_domain) possible_hosts.add(host_no_domain)
case RemoveDomain.AUTO: case L2RemoveDomain.AUTO:
possible_hosts.add(host) possible_hosts.add(host)
possible_hosts.add(host_no_domain) possible_hosts.add(host_no_domain)
case RemoveDomain.OFF | _: case L2RemoveDomain.OFF | _:
possible_hosts.add(host) possible_hosts.add(host)
match self.case: match self.case:
case Case.UPPER: case L2Case.UPPER:
if not '.' in host: # use UPPER only for names without domain if not '.' in host: # use UPPER only for names without domain
LOGGER.debug(f'{self.backend} Change neighbour to upper case: {host} -> {host.upper()}') LOGGER.debug(f'{self.backend} Change neighbour to upper case: {host} -> {host.upper()}')
possible_hosts.add(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()}') LOGGER.debug(f'{self.backend} Change neighbour to lower case: {host} -> {host.lower()}')
possible_hosts.add(host.lower()) possible_hosts.add(host.lower())
case Case.AUTO: case L2Case.AUTO:
possible_hosts.add(host) possible_hosts.add(host)
possible_hosts.add(host.lower()) possible_hosts.add(host.lower())
possible_hosts.add(host_no_domain.lower()) possible_hosts.add(host_no_domain.lower())
possible_hosts.add(host_no_domain.upper()) possible_hosts.add(host_no_domain.upper())
case Case.OFF | _: case L2Case.OFF | _:
possible_hosts.add(host) possible_hosts.add(host)
possible_hosts = [f'{self.prefix}{host}' for host in possible_hosts] possible_hosts = [f'{self.prefix}{host}' for host in possible_hosts]
...@@ -381,7 +381,7 @@ class HostCache: ...@@ -381,7 +381,7 @@ class HostCache:
LOGGER.debug(f'{self.backend} Matched neighbour to host: |{neighbour}| -> |{entry}|') LOGGER.debug(f'{self.backend} Matched neighbour to host: |{neighbour}| -> |{entry}|')
return 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: if not self.no_case_host_map:
self.no_case_host_map = {host.lower():host for host in self.cache} self.no_case_host_map = {host.lower():host for host in self.cache}
for entry in possible_hosts: for entry in possible_hosts:
......
...@@ -13,7 +13,7 @@ from os import environ ...@@ -13,7 +13,7 @@ from os import environ
from typing import Final 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"] OMD_ROOT: Final[str] = environ["OMD_ROOT"]
# #
...@@ -21,19 +21,25 @@ API_PORT_DEFAULT: Final[int] = 5001 ...@@ -21,19 +21,25 @@ API_PORT_DEFAULT: Final[int] = 5001
CACHE_INTERFACES_DATA: Final[str] = 'interface_data' CACHE_INTERFACES_DATA: Final[str] = 'interface_data'
CMK_SITE_CONF: Final[str] = f'{OMD_ROOT}/etc/omd/site.conf' CMK_SITE_CONF: Final[str] = f'{OMD_ROOT}/etc/omd/site.conf'
CONFIG_FILE: Final[str] = 'nvdct.toml' 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' DATAPATH: Final[str] = f'{OMD_ROOT}/var/check_mk/topology/data'
LOGGER: Logger = getLogger('root)') 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' MIN_CMK_VERSION_POST: Final[str] = '2.3.0p23'
SCRIPT: Final[str] = '~/local/bin/nvdct/nvdct.py' 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_DEFAULT: Final[str] = '%Y-%m-%dT%H:%M:%S.%m'
TIME_FORMAT_ARGPARSER: Final[str] = TIME_FORMAT_DEFAULT.replace('%', '%%')
class EnumValue(Enum): class EnumValue(Enum):
def __get__(self, instance, owner): def __get__(self, instance, owner):
return self.value return self.value
@classmethod
def list(cls):
return list(map(lambda c: c.value, cls))
@unique @unique
class ExitCodes(EnumValue): class ExitCodes(EnumValue):
...@@ -72,7 +78,7 @@ class Backends(EnumValue): ...@@ -72,7 +78,7 @@ class Backends(EnumValue):
@unique @unique
class Case(EnumValue): class L2Case(EnumValue):
AUTO: Final[str] = 'AUTO' AUTO: Final[str] = 'AUTO'
INSENSITIVE: Final[str] = 'INSENSITIVE' INSENSITIVE: Final[str] = 'INSENSITIVE'
LOWER: Final[str] = 'LOWER' LOWER: Final[str] = 'LOWER'
...@@ -81,11 +87,16 @@ class Case(EnumValue): ...@@ -81,11 +87,16 @@ class Case(EnumValue):
@unique @unique
class RemoveDomain(EnumValue): class L2RemoveDomain(EnumValue):
ON: Final[str] = 'ON' ON: Final[str] = 'ON'
OFF: Final[str] = 'OFF' OFF: Final[str] = 'OFF'
AUTO: Final[str] = 'AUTO' AUTO: Final[str] = 'AUTO'
@unique
class L2IgnoreMismatch(EnumValue):
DUPLEX: Final[str] = 'DUPLEX'
SPEED: Final[str] = 'SPEED'
VLAN: Final[str] = 'VLAN'
@unique @unique
class CacheItems(EnumValue): class CacheItems(EnumValue):
...@@ -107,6 +118,7 @@ class CliLong(EnumValue): ...@@ -107,6 +118,7 @@ class CliLong(EnumValue):
L2_CASE: Final[str] = '--l2-case' L2_CASE: Final[str] = '--l2-case'
L2_DISPLAY_NEIGHBOURS: Final[str] = '--l2-display-neighbours' L2_DISPLAY_NEIGHBOURS: Final[str] = '--l2-display-neighbours'
L2_DISPLAY_PORTS: Final[str] = '--l2-display-ports' L2_DISPLAY_PORTS: Final[str] = '--l2-display-ports'
L2_IGNORE_MISMATCH: Final[str] = '--l2-ignore-mismatch'
L2_PREFIX: Final[str] = '--l2-prefix' L2_PREFIX: Final[str] = '--l2-prefix'
L2_REMOVE_DOMAIN: Final[str] = '--l2-remove-domain' L2_REMOVE_DOMAIN: Final[str] = '--l2-remove-domain'
L2_SKIP_EXTERNAL: Final[str] = '--l2-skip-external' L2_SKIP_EXTERNAL: Final[str] = '--l2-skip-external'
...@@ -285,6 +297,7 @@ class TomlSettings(EnumValue): ...@@ -285,6 +297,7 @@ class TomlSettings(EnumValue):
L2_CASE: Final[str] = cli_long_to_toml(CliLong.L2_CASE) 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_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_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_PREFIX: Final[str] = cli_long_to_toml(CliLong.L2_PREFIX)
L2_REMOVE_DOMAIN: Final[str] = cli_long_to_toml(CliLong.L2_REMOVE_DOMAIN) 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) L2_SKIP_EXTERNAL: Final[str] = cli_long_to_toml(CliLong.L2_SKIP_EXTERNAL)
......
...@@ -15,7 +15,6 @@ from collections.abc import Mapping ...@@ -15,7 +15,6 @@ from collections.abc import Mapping
from ipaddress import AddressValueError, NetmaskValueError, ip_address, ip_network from ipaddress import AddressValueError, NetmaskValueError, ip_address, ip_network
from logging import CRITICAL, DEBUG, ERROR, FATAL, INFO, WARNING from logging import CRITICAL, DEBUG, ERROR, FATAL, INFO, WARNING
from pathlib import Path from pathlib import Path
from sys import exit as sys_exit
from time import strftime from time import strftime
from typing import Dict, List, NamedTuple, Set, Tuple from typing import Dict, List, NamedTuple, Set, Tuple
...@@ -23,16 +22,18 @@ from lib.constants import ( ...@@ -23,16 +22,18 @@ from lib.constants import (
API_PORT_DEFAULT, API_PORT_DEFAULT,
Backends, Backends,
CONFIG_FILE, CONFIG_FILE,
Case, CONFIG_PATH_DEFAULT,
EmblemNames, EmblemNames,
EmblemValues, EmblemValues,
ExitCodes,
IncludeExclude, IncludeExclude,
L2Case,
L2IgnoreMismatch,
L2RemoveDomain,
Layers,
LOGGER, LOGGER,
LOG_FILE_DEFAULT, LOG_FILE_DEFAULT,
LOG_PATH_DEFAULT,
LogLevels, LogLevels,
OMD_ROOT,
RemoveDomain,
TIME_FORMAT_DEFAULT, TIME_FORMAT_DEFAULT,
TomlSections, TomlSections,
TomlSettings, TomlSettings,
...@@ -86,7 +87,7 @@ class Settings: ...@@ -86,7 +87,7 @@ class Settings:
TomlSettings.API_PORT: None, TomlSettings.API_PORT: None,
TomlSettings.BACKEND: Backends.MULTISITE, TomlSettings.BACKEND: Backends.MULTISITE,
TomlSettings.CHECK_CONFIG: False, 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.DEFAULT: False,
TomlSettings.DONT_COMPARE: False, TomlSettings.DONT_COMPARE: False,
TomlSettings.FILTER_CUSTOMERS: None, TomlSettings.FILTER_CUSTOMERS: None,
...@@ -94,6 +95,7 @@ class Settings: ...@@ -94,6 +95,7 @@ class Settings:
TomlSettings.KEEP_MAX_TOPOLOGIES: False, TomlSettings.KEEP_MAX_TOPOLOGIES: False,
TomlSettings.L2_CASE: None, TomlSettings.L2_CASE: None,
TomlSettings.L2_DISPLAY_PORTS: False, TomlSettings.L2_DISPLAY_PORTS: False,
TomlSettings.L2_IGNORE_MISMATCH: [],
TomlSettings.L2_DISPLAY_NEIGHBOURS: False, TomlSettings.L2_DISPLAY_NEIGHBOURS: False,
TomlSettings.L2_PREFIX: None, TomlSettings.L2_PREFIX: None,
TomlSettings.L2_REMOVE_DOMAIN: None, TomlSettings.L2_REMOVE_DOMAIN: None,
...@@ -132,14 +134,6 @@ class Settings: ...@@ -132,14 +134,6 @@ class Settings:
self.__settings.update(self.__user_data.get(TomlSections.SETTINGS, {})) self.__settings.update(self.__user_data.get(TomlSections.SETTINGS, {}))
self.__settings.update(self.__args) 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 self.__api_port: int | None = None
# init user data with defaults # init user data with defaults
...@@ -150,6 +144,7 @@ class Settings: ...@@ -150,6 +144,7 @@ class Settings:
self.__filter_by_host_tag: Dict[str, Set[str]] | None = None self.__filter_by_host_tag: Dict[str, Set[str]] | None = None
self.__filter_by_site: List[str] | None = None self.__filter_by_site: List[str] | None = None
self.__l2_drop_neighbours: 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_replace_regex: List[Tuple[str, str]] | None = None
self.__l2_neighbour_to_host_map: Dict[str, str] | None = None self.__l2_neighbour_to_host_map: Dict[str, str] | None = None
self.__l2_seed_devices: List[str] | None = None self.__l2_seed_devices: List[str] | None = None
...@@ -158,6 +153,7 @@ class Settings: ...@@ -158,6 +153,7 @@ class Settings:
self.__l3_replace: Dict[str, str] | None = None self.__l3_replace: Dict[str, str] | None = None
self.__l3_summarize: List[ip_network] | None = None self.__l3_summarize: List[ip_network] | None = None
self.__l3v4_ignore_wildcard: List[Wildcard] | 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.__map_speed_to_thickness: List[Thickness] | None = None
self.__protected_topologies: List[str] | None = None self.__protected_topologies: List[str] | None = None
self.__static_connections: List[StaticConnection] | None = None self.__static_connections: List[StaticConnection] | None = None
...@@ -179,11 +175,7 @@ class Settings: ...@@ -179,11 +175,7 @@ class Settings:
@property # -b --backend @property # -b --backend
def backend(self) -> str: def backend(self) -> str:
if str(self.__settings[TomlSettings.BACKEND]) in [ if str(self.__settings[TomlSettings.BACKEND]) in Backends.list():
Backends.LIVESTATUS,
Backends.MULTISITE,
Backends.RESTAPI
]:
return str(self.__settings[TomlSettings.BACKEND]) return str(self.__settings[TomlSettings.BACKEND])
else: # fallback to defaukt -> exit ?? else: # fallback to defaukt -> exit ??
LOGGER.error( LOGGER.error(
...@@ -210,7 +202,7 @@ class Settings: ...@@ -210,7 +202,7 @@ class Settings:
@property # --filter-customers @property # --filter-customers
def filter_customers(self) -> str | None: 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] return self.__settings[TomlSettings.FILTER_CUSTOMERS]
elif self.__settings[TomlSettings.FILTER_CUSTOMERS] is not None: elif self.__settings[TomlSettings.FILTER_CUSTOMERS] is not None:
LOGGER.error( LOGGER.error(
...@@ -221,7 +213,7 @@ class Settings: ...@@ -221,7 +213,7 @@ class Settings:
@property # --filter-sites @property # --filter-sites
def filter_sites(self) -> str | None: 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] return self.__settings[TomlSettings.FILTER_SITES]
elif self.__settings[TomlSettings.FILTER_SITES] is not None: elif self.__settings[TomlSettings.FILTER_SITES] is not None:
LOGGER.error( LOGGER.error(
...@@ -232,12 +224,12 @@ class Settings: ...@@ -232,12 +224,12 @@ class Settings:
@property # --l2-case @property # --l2-case
def l2_case(self) -> str | None: 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] return self.__settings[TomlSettings.L2_CASE]
elif self.__settings[TomlSettings.L2_CASE] is not None: elif self.__settings[TomlSettings.L2_CASE] is not None:
LOGGER.error( LOGGER.error(
f'Unknown case setting {self.__settings[TomlSettings.L2_CASE]}. ' 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 return None
...@@ -249,6 +241,15 @@ class Settings: ...@@ -249,6 +241,15 @@ class Settings:
def l2_display_neighbours(self) -> bool: def l2_display_neighbours(self) -> bool:
return bool(self.__settings[TomlSettings.L2_DISPLAY_NEIGHBOURS]) 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 @property # --l2-prefix
def l2_prefix(self) -> str: def l2_prefix(self) -> str:
if self.__settings[TomlSettings.L2_PREFIX] is not None: if self.__settings[TomlSettings.L2_PREFIX] is not None:
...@@ -257,11 +258,11 @@ class Settings: ...@@ -257,11 +258,11 @@ class Settings:
@property # --l2-remove-domain @property # --l2-remove-domain
def l2_remove_domain(self) -> str: 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] return self.__settings[TomlSettings.L2_REMOVE_DOMAIN]
else: else:
self.__settings[TomlSettings.L2_REMOVE_DOMAIN] = RemoveDomain.OFF self.__settings[TomlSettings.L2_REMOVE_DOMAIN] = L2RemoveDomain.OFF
return RemoveDomain.OFF return L2RemoveDomain.OFF
@property # --l2-skip-external @property # --l2-skip-external
def l2_skip_external(self) -> bool: def l2_skip_external(self) -> bool:
...@@ -301,12 +302,17 @@ class Settings: ...@@ -301,12 +302,17 @@ class Settings:
@property # --layers @property # --layers
def layers(self) -> List[str]: 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 @property # --log-file
def log_file(self) -> str: def log_file(self) -> str:
raw_log_file = str(Path(str(self.__settings[TomlSettings.LOG_FILE])).expanduser()) 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 # logger not ready yet
print(f'\nInvalid log file {raw_log_file}. Falling back to {LOG_FILE_DEFAULT}') print(f'\nInvalid log file {raw_log_file}. Falling back to {LOG_FILE_DEFAULT}')
return LOG_FILE_DEFAULT return LOG_FILE_DEFAULT
...@@ -405,13 +411,13 @@ class Settings: ...@@ -405,13 +411,13 @@ class Settings:
return self.__filter_by_site return self.__filter_by_site
@staticmethod @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 = { parsed = {
IncludeExclude.INCLUDE: set(), IncludeExclude.INCLUDE: set(),
IncludeExclude.EXCLUDE: set() IncludeExclude.EXCLUDE: set()
} }
for key_value, mode in data.items(): for key_value, mode in data.items():
if mode not in [IncludeExclude.INCLUDE, IncludeExclude.EXCLUDE]: if mode not in IncludeExclude.list():
LOGGER.error( LOGGER.error(
f'Invalid mode in {section} found: {key_value}={mode} -> line ignored' f'Invalid mode in {section} found: {key_value}={mode} -> line ignored'
) )
...@@ -438,7 +444,7 @@ class Settings: ...@@ -438,7 +444,7 @@ class Settings:
return parsed return parsed
@property @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: if self.__filter_by_folder is None:
self.__filter_by_folder = self.parse_key_value_section( self.__filter_by_folder = self.parse_key_value_section(
section=TomlSections.FILTER_BY_FOLDER, section=TomlSections.FILTER_BY_FOLDER,
......
...@@ -29,6 +29,7 @@ from lib.constants import ( ...@@ -29,6 +29,7 @@ from lib.constants import (
HostLabels, HostLabels,
IPVersion, IPVersion,
InvPaths, InvPaths,
L2IgnoreMismatch,
L2InvColumns, L2InvColumns,
L3InvColumns, L3InvColumns,
LOGGER, LOGGER,
...@@ -382,6 +383,7 @@ class NvConnections: ...@@ -382,6 +383,7 @@ class NvConnections:
self, self,
nv_objects: NvObjects, nv_objects: NvObjects,
speed_map: Sequence[Thickness], speed_map: Sequence[Thickness],
ignore_mismatch: Sequence[str],
): ):
for connection in self.nv_connections: for connection in self.nv_connections:
warning = False warning = False
...@@ -411,7 +413,7 @@ class NvConnections: ...@@ -411,7 +413,7 @@ class NvConnections:
# left_thickness = map_speed_to_thickness(left_speed, speed_map) # left_thickness = map_speed_to_thickness(left_speed, speed_map)
metadata['line_config']['thickness'] = right_thickness 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 warning = True
metadata = add_tooltip_html( metadata = add_tooltip_html(
metadata, 'Speed', left, left_speed_str, right, right_speed_str metadata, 'Speed', left, left_speed_str, right, right_speed_str
...@@ -428,7 +430,7 @@ class NvConnections: ...@@ -428,7 +430,7 @@ class NvConnections:
# for duplex/native vlan it might be a good idea to change left/right # for duplex/native vlan it might be a good idea to change left/right
# value as they are reported by CDP as neighbour states # value as they are reported by CDP as neighbour states
if left_duplex and right_duplex: 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 warning = True
metadata = add_tooltip_html( metadata = add_tooltip_html(
...@@ -439,7 +441,7 @@ class NvConnections: ...@@ -439,7 +441,7 @@ class NvConnections:
f'Connection duplex mismatch: {left} ({left_duplex})' f'Connection duplex mismatch: {left} ({left_duplex})'
f'<->{right} ({right_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 != '0' and right_native_vlan != '0': # ignore VLAN 0 (Native VLAN on routed ports)
if left_native_vlan != right_native_vlan: if left_native_vlan != right_native_vlan:
warning = True warning = True
...@@ -710,7 +712,7 @@ class TopologyL2(Topology): ...@@ -710,7 +712,7 @@ class TopologyL2(Topology):
self.hosts_to_go: MutableSet[str] = set(seed_devices) self.hosts_to_go: MutableSet[str] = set(seed_devices)
self.label: str = label self.label: str = label
self.neighbour_replace_regex: List[Tuple[str, str]] = neighbour_replace_regex 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.path_in_inventory: str = path_in_inventory
self.raw_neighbour_to_neighbour: Dict[str, str] = {} self.raw_neighbour_to_neighbour: Dict[str, str] = {}
self.skip_external: bool = skip_external self.skip_external: bool = skip_external
...@@ -910,7 +912,7 @@ class TopologyL3(Topology): ...@@ -910,7 +912,7 @@ class TopologyL3(Topology):
super().__init__( super().__init__(
emblems=emblems, emblems=emblems,
host_cache=host_cache, host_cache=host_cache,
topology=f'[L3 IPv{version}]' topology=f'[L3 IPv{version}]',
) )
self.diplay_devices = display_devices self.diplay_devices = display_devices
self.ignore_hosts: Sequence[str] = ignore_hosts self.ignore_hosts: Sequence[str] = ignore_hosts
......
#!/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.')
...@@ -237,7 +237,6 @@ def is_valid_output_directory(directory: str) -> bool: ...@@ -237,7 +237,6 @@ def is_valid_output_directory(directory: str) -> bool:
re_host_pattern = r'^[0-9a-z-A-Z\.\-\_\:]{1,30}$' re_host_pattern = r'^[0-9a-z-A-Z\.\-\_\:]{1,30}$'
if re_match(re_host_pattern, directory): if re_match(re_host_pattern, directory):
return True return True
return True
else: else:
LOGGER.error(f'Invalid output directory name found: {directory}') LOGGER.error(f'Invalid output directory name found: {directory}')
return False return False
......
...@@ -221,6 +221,7 @@ ...@@ -221,6 +221,7 @@
# fixed REST API query for interface services # fixed REST API query for interface services
# 2025-01-24: added option --l2-display-ports, --l3-display-devices # 2025-01-24: added option --l2-display-ports, --l3-display-devices
# 2025-02-05: added option "OFF" to --l2-case # 2025-02-05: added option "OFF" to --l2-case
# 2025-02-07: added option --l2-ignore-mismatch
# #
# creating topology data json from inventory data # creating topology data json from inventory data
# #
...@@ -541,6 +542,7 @@ def main(): ...@@ -541,6 +542,7 @@ def main():
topology.nv_connections.add_meta_data_to_connections( topology.nv_connections.add_meta_data_to_connections(
nv_objects=topology.nv_objects, nv_objects=topology.nv_objects,
speed_map=settings.map_speed_to_thickness, speed_map=settings.map_speed_to_thickness,
ignore_mismatch=settings.l2_ignore_mismatch,
) )
topology.save( topology.save(
......
...@@ -40,14 +40,15 @@ ...@@ -40,14 +40,15 @@
'nvdct/lib/__init__.py', 'nvdct/lib/__init__.py',
'nvdct/conf/nvdct.toml', 'nvdct/conf/nvdct.toml',
'nvdct/lib/topologies.py', 'nvdct/lib/topologies.py',
'nvdct/lib/constants.py'], 'nvdct/lib/constants.py',
'nvdct/lib/update_config.py'],
'web': ['htdocs/images/icons/cloud_80.png', 'web': ['htdocs/images/icons/cloud_80.png',
'htdocs/images/icons/ip-address_80.png', 'htdocs/images/icons/ip-address_80.png',
'htdocs/images/icons/ip-network_80.png', 'htdocs/images/icons/ip-network_80.png',
'htdocs/images/icons/location_80.png']}, 'htdocs/images/icons/location_80.png']},
'name': 'nvdct', 'name': 'nvdct',
'title': 'Network Visualization Data Creation Tool (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.min_required': '2.3.0b1',
'version.packaged': 'cmk-mkp-tool 0.2.0', 'version.packaged': 'cmk-mkp-tool 0.2.0',
'version.usable_until': '2.4.0p1'} 'version.usable_until': '2.4.0p1'}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment