diff --git a/README.md b/README.md index 35905d2884fde49b99526e37d79bdee082f3981f..274747bb19997f5f115ef001f685e8636de72865 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[PACKAGE]: ../../raw/master/mkp/nvdct-0.9.3-20241209.mkp "nvdct-0.9.3-20241209.mkp" +[PACKAGE]: ../../raw/master/mkp/nvdct-0.9.4-20241210.mkp "nvdct-0.9.4-20241210.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.4-20241210.mkp b/mkp/nvdct-0.9.4-20241210.mkp new file mode 100644 index 0000000000000000000000000000000000000000..f1bbfad565be60d5a472b0fba691807e167707c0 Binary files /dev/null and b/mkp/nvdct-0.9.4-20241210.mkp differ diff --git a/source/bin/nvdct/conf/nvdct.toml b/source/bin/nvdct/conf/nvdct.toml index b5632b5aa521ef87764297fb97eafa9676ec3e06..414861aab4269dcd02d9d3255bb0158edfa258e5 100755 --- a/source/bin/nvdct/conf/nvdct.toml +++ b/source/bin/nvdct/conf/nvdct.toml @@ -13,6 +13,7 @@ # # list of (additional to -s/--seed-devices) seed devices +# [0-9-a-zA-Z\.\_\-]{1,253} -> host L2_SEED_DEVICES = [ # "CORE01", # "LOCATION01", @@ -26,6 +27,7 @@ L2_DROP_HOSTS = [ ] # hosts will be ignored in L3v4 topology +# [0-9-a-zA-Z\.\_\-]{1,253} -> host L3V4_IGNORE_HOSTS = [ # "host1", # "host2", @@ -64,29 +66,39 @@ PROTECTED_TOPOLOGIES = [ ] # user defined static connections -# these connections will be added from host to neighbour and in reverese -# hosts/neighbours in this section will be added to SEED_DEVICES +# [0-9-a-zA-Z\.\_\-]{1,253} -> host STATIC_CONNECTIONS = [ - # ["cmk_host1", "local-port1", "neighbour-port1", "neighbour1", "label"], - # ["cmk_host1", "local-port2", "neighbour-port2", "neighbour2", "label"], + # 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" ] -# optional custom layers use option -l/--layers CUSTOM to include this layers +# THIS OPTION IS DEPRECATED +# optional custom layers use option -l/--layers CUSTOM to include these layers # don't use --pre-fetch without a host_label that matches all host you want to add +# THIS OPTION IS DEPRECATED CUSTOM_LAYERS = [ # { path = "path,in,inventory", columns = "columns from inventory", label = "label for the layer", host_label = "CMK host label to find matching hosts" }, # { path = "networking,lldp_cache,neighbours", columns = "neighbour_name,local_port,neighbour_port", label = "custom_LLDP", host_label = "nvdct/has_lldp_neighbours" }, # { path = "networking,cdp_cache,neighbours", columns = "neighbour_name,local_port,neighbour_port", label = "custom_CDP", host_label = "nvdct/has_cdp_neighbours" }, ] -# list customers so include/excluse, use option --filter-costumers INCLUDE/EXCLUDE +# list customers to include/excluse, use option --filter-costumers INCLUDE/EXCLUDE +# [0-9-a-zA-Z\.\_\-]{1,16} -> customer CUSTOMERS = [ # "customer1", # "customer2", # "customer3", ] -# list site so include/excluse, use option --filter-sites INCLUDE/EXCLUDE +# list site to include/excluse, use option --filter-sites INCLUDE/EXCLUDE +# [0-9-a-zA-Z\.\_\-]{1,16} -> site SITES = [ # "site1", # "site2", @@ -94,6 +106,7 @@ SITES = [ ] # map inventory neighbour name to Checkmk host name +# [0-9-a-zA-Z\.\_\-]{1,253} -> host [L2_HOST_MAP] # inventory_neighbour1 = "cmk_host1" # inventory_neighbour2 = "cmk_host2" @@ -106,6 +119,7 @@ SITES = [ # "^Meraki.*\\s-\\s" = "" # replace network objects (takes place after summarize) +# [0-9-a-zA-Z\.\_\-]{1,253} -> host [L3V4_REPLACE] # "10.193.172.0/24" = "MPLS" # "10.194.8.0/23" = "MPLS" @@ -127,29 +141,30 @@ SITES = [ # must be sorted from slower to faster speed # use only one entry to have all conections 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 +# "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 [SETTINGS] -# api_port = 80 +# api_port = 5001 # backend = "MULTISITE" | "RESTAPI" | "LIVESTATUS" # case = "LOWER" | "UPPER" # default = false # dont_compare = false # filter_customers = "INCLUDE" |"EXCLUDE" # filter_sites = "INCLUDE" | "EXCLUDE" +# include_l3_hosts = false # keep = 0 -# layers = ["LLDP", "CDP", "STATIC", "CUSTOM", "L3v4"] +# layers = ["LLDP", "CDP", L3v4, "STATIC", "CUSTOM"] # log_file = "~/var/log/nvdct.log" # log_level = "WARNING" # log_to_stdout = false # min_age = 0 -# output_directory = '' +# output_directory = '' # # pre_fetch = false # prefix = "" # quiet = true diff --git a/source/bin/nvdct/lib/args.py b/source/bin/nvdct/lib/args.py index d113171964dc8cc45782ce1d157bbb61dc518a1d..cf77c81bea79ee7e545e426450ad836b9469159c 100755 --- a/source/bin/nvdct/lib/args.py +++ b/source/bin/nvdct/lib/args.py @@ -23,6 +23,7 @@ # --dont-compare # --filter-customers # --filter-sites +# --include-l3-hosts # --keep # --log-file # --log-level @@ -43,18 +44,19 @@ from argparse import ( ) from pathlib import Path -from lib.utils import ( - ExitCodes, +from lib.constants import ( HOME_URL, MIN_CDP_VERSION, + MIN_LINUX_IP_ADDRESSES, + MIN_SNMP_IP_ADDRESSES, + MIN_WINDOWS_IP_ADDRESSES, MIN_LLDP_VERSION, - MIN_IP_ADDRESSES, NVDCT_VERSION, - # SAMPLE_SEEDS, SCRIPT, TIME_FORMAT_ARGPARSER, USER_DATA_FILE, ) +from lib.utils import ExitCodes def parse_arguments() -> arg_Namespace: @@ -83,7 +85,7 @@ def parse_arguments() -> arg_Namespace: f' {ExitCodes.BACKEND_NOT_IMPLEMENTED.value} - Backend not implemented\n' f' {ExitCodes.AUTOMATION_SECRET_NOT_FOUND.value} - Automation secret not found\n' '\nUsage:\n' - f'{SCRIPT} -u ~/local/bin/nvdct/conf/{USER_DATA_FILE} \n\n' + f'{SCRIPT} -u ~/local/bin/nvdct/conf/my_{USER_DATA_FILE} \n\n' ) parser.add_argument( @@ -122,12 +124,13 @@ def parse_arguments() -> arg_Namespace: choices=['CDP', 'CUSTOM', 'LLDP', 'STATIC', 'L3v4'], # default=['CDP'], help=( - f' - CDP : needs inv_cdp_cache package at least in version {MIN_CDP_VERSION}\n' - f' - LLDP : needs inv_lldp_cache package at least in version {MIN_LLDP_VERSION}\n' - f' - L3v4 : needs inv_ip_address package at least in version {MIN_IP_ADDRESSES}\n' - f' adds, layer 3 topology fpr IPv4\n' - f' - STATIC (deprecated)\n' - f' - CUSTOM (deprecated)\n' + f' - CDP : needs inv_cdp_cache package at least in version {MIN_CDP_VERSION}\n' + f' - LLDP : needs inv_lldp_cache package at least in version {MIN_LLDP_VERSION}\n' + f' - L3v4 : needs inv_ip_address package at least in version {MIN_SNMP_IP_ADDRESSES} for SNMP based hosts\n' + f' for Linux based hosts inv_lnx_ip_if in version {MIN_LINUX_IP_ADDRESSES}\n' + f' for Windows based hosts inv_win_ip_if in version {MIN_WINDOWS_IP_ADDRESSES}\n' + f' - STATIC: creates a topology base on the "STATIC_CONNECTIONS" in the toml file\n' + f' - CUSTOM: (deprecated)\n' ) ) parser.add_argument( @@ -192,6 +195,10 @@ def parse_arguments() -> arg_Namespace: help='INCLUDE/EXCLUDE site list from config file.\n' 'Note: MULTISITE backend only.', ) + parser.add_argument( + '--include-l3-hosts', action='store_const', const=True, # default=False, + help='Include hosts (single IP objects) in layer 3 topology', + ) parser.add_argument( '--remove-domain', action='store_const', const=True, # default=False, help='Remove the domain name from the neighbor name', diff --git a/source/bin/nvdct/lib/backends.py b/source/bin/nvdct/lib/backends.py index 611b7a6332f5aeed116bdbb4f87d5a1fd6e171f5..64cce3ae7b7075834fdc26052821ba65e4c60754 100755 --- a/source/bin/nvdct/lib/backends.py +++ b/source/bin/nvdct/lib/backends.py @@ -23,14 +23,17 @@ from sys import exit as sys_exit from livestatus import MultiSiteConnection, SiteConfigurations, SiteId -from lib.utils import ( +from lib.constants import ( CACHE_INTERFACES_DATA, + OMD_ROOT, + PATH_INTERFACES, +) +from lib.utils import ( ExitCodes, get_data_form_live_status, get_table_from_inventory, LOGGER, - OMD_ROOT, - PATH_INTERFACES, + ) @@ -410,8 +413,8 @@ class HostCacheMultiSite(HostCacheLiveStatus): 'local site only. Try -b RESTAPI if you have a distributed environment.' ) - def filter_sites(self, filter: str| None, sites:List[str]): - match filter: + def filter_sites(self, filter_: str | None, sites: List[str]): + match filter_: case 'INCLUDE': self.sites = {site: data for site, data in self.sites.items() if site in sites} case 'EXCLUDE': @@ -419,8 +422,8 @@ class HostCacheMultiSite(HostCacheLiveStatus): case _: return - def filter_costumers(self, filter: str | None, costumers:List[str]): - match filter: + def filter_costumers(self, filter_: str | None, costumers: List[str]): + match filter_: case 'INCLUDE': self.sites = { site: data for site, data in self.sites.items() if data.get('customer') in costumers @@ -437,10 +440,17 @@ class HostCacheMultiSite(HostCacheLiveStatus): class HostCacheRestApi(HostCache): - def __init__(self, pre_fetch: bool, api_port: int): + def __init__( + self, + pre_fetch: bool, + api_port: int, + filter_sites: str | None = None, + sites: List[str] = [], + ): super().__init__(pre_fetch, '[RESTAPI]') LOGGER.debug(f'{self.backend} init backend') self._api_port = api_port + self.sites = [] try: self.__secret = Path( f'{OMD_ROOT}/var/check_mk/web/automation/automation.secret' @@ -458,9 +468,33 @@ class HostCacheRestApi(HostCache): self.__session.headers['Authorization'] = f"Bearer {self.__user} {self.__secret}" self.__session.headers['Accept'] = 'application/json' + self.get_sites() + self.filter_sites(filter_=filter_sites, sites=sites) + LOGGER.info(f'{self.backend} filtered sites : {self.sites}') + if self.pre_fetch: self.pre_fetch_hosts() + def get_sites(self): + LOGGER.debug(f'{self.backend} get_sites') + resp = self.__session.get(url=f"{self.__api_url}/domain-types/site_connection/collections/all") + if resp.status_code == 200: + sites = resp.json().get("value") + self.sites = [site.get('id') for site in sites] + LOGGER.debug(f'{self.backend} sites : {self.sites}') + else: + LOGGER.warning(f'{self.backend} got no site information! status code {resp.status_code}') + LOGGER.debug(f'{self.backend} response text: {resp.text}') + + def filter_sites(self, filter_: str | None, sites: List[str]): + match filter_: + case 'INCLUDE': + self.sites = [site for site in self.sites if site in sites] + case 'EXCLUDE': + self.sites = [site for site in self.sites if site not in sites] + case _: + return + def get_inventory_data(self, hosts: List[str]) -> Dict[str, Dict | None]: LOGGER.debug(f'{self.backend} get_inventory_data {hosts}') host_data: Dict[str, Dict | None] = {} @@ -475,8 +509,9 @@ class HostCacheRestApi(HostCache): resp = self.__session.get( url=f"{self.__api_url}/domain-types/host/collections/all", params={ - "query": query, - "columns": ['name', 'mk_inventory'], + 'query': query, + 'columns': ['name', 'mk_inventory'], + 'sites': self.sites, }, ) if resp.status_code == 200: @@ -487,7 +522,7 @@ class HostCacheRestApi(HostCache): if host: host_data[host] = raw_host['extensions'].get('mk_inventory') else: - LOGGER.info( + LOGGER.warning( f'{self.backend} got no inventory data found!, status code {resp.status_code}' ) LOGGER.debug(f'{self.backend} response query: {query}') @@ -511,10 +546,11 @@ class HostCacheRestApi(HostCache): query = f'{{"op": "and", "expr": [{query_item},{query_host}]}}' resp = self.__session.get( - url=f"{self.__api_url}/domain-types/service/collections/all", + url=f'{self.__api_url}/domain-types/service/collections/all', params={ - "query": query, - "columns": ['host_name', 'description', 'long_plugin_output'], + 'query': query, + 'columns': ['host_name', 'description', 'long_plugin_output'], + 'sites': self.sites, }, ) @@ -556,10 +592,11 @@ class HostCacheRestApi(HostCache): # return False query = '{"op": "=", "left": "name", "right": "' + host + '"}' resp = self.__session.get( - url=f"{self.__api_url}/domain-types/host/collections/all", + url=f'{self.__api_url}/domain-types/host/collections/all', params={ - "query": query, - "columns": ['name'], + 'query': query, + 'columns': ['name'], + 'sites': self.sites, }, ) if resp.status_code == 200: @@ -579,14 +616,14 @@ class HostCacheRestApi(HostCache): def get_hosts_by_label(self, label: str) -> List[str] | None: LOGGER.debug(f'{self.backend} get_hosts_by_label {label}') - # query = '{"op": "=", "left": "label_names", "right": "' + label + '"}' query = '{"op": "=", "left": "labels", "right": "' + label + '"}' resp = self.__session.get( - url=f"{self.__api_url}/domain-types/host/collections/all", + url=f'{self.__api_url}/domain-types/host/collections/all', params={ - "query": query, - "columns": ['name', 'labels'], + 'columns': ['name', 'labels'], + 'query': query, + 'sites': self.sites, }, ) if resp.status_code == 200: @@ -608,11 +645,12 @@ class HostCacheRestApi(HostCache): def pre_fetch_hosts(self): LOGGER.debug(f'{self.backend} pre_fetch_hosts') - + LOGGER.critical(f'{self.backend} pre_fetch_hosts sites {self.sites}') resp = self.__session.get( - url=f"{self.__api_url}/domain-types/host/collections/all", + url=f'{self.__api_url}/domain-types/host/collections/all', params={ - "columns": ['name'], + 'columns': ['name'], + 'sites': self.sites, }, ) if resp.status_code == 200: diff --git a/source/bin/nvdct/lib/constants.py b/source/bin/nvdct/lib/constants.py new file mode 100755 index 0000000000000000000000000000000000000000..076f6490754ace384898101ddf1a4f9272f10363 --- /dev/null +++ b/source/bin/nvdct/lib/constants.py @@ -0,0 +1,48 @@ +#!/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 : 2024-12-11 +# File : nvdct/lib/constants.py + + +from logging import getLogger +from os import environ +from typing import Final + +NVDCT_VERSION: Final[str] = '0.9.4-20241210' +# +OMD_ROOT: Final[str] = environ["OMD_ROOT"] +# +API_PORT: Final[int] = 5001 +CACHE_INTERFACES_DATA: Final[str] = 'interface_data' +CMK_SITE_CONF: Final[str] = f'{OMD_ROOT}/etc/omd/site.conf' +COLUMNS_CDP: Final[str] = 'neighbour_name,local_port,neighbour_port' +COLUMNS_L3v4: Final[str] = 'address,device,cidr,network,type' +COLUMNS_LLDP: Final[str] = 'neighbour_name,local_port,neighbour_port' +DATAPATH: Final[str] = f'{OMD_ROOT}/var/check_mk/topology/data' +HOME_URL: Final[str] = 'https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/nvdct' +HOST_LABEL_CDP: Final[str] = "'nvdct/has_cdp_neighbours' 'yes'" +HOST_LABEL_L3V4_HOSTS: Final[str] = "'nvdct/l3v4_topology' 'host'" +HOST_LABEL_L3V4_ROUTER: Final[str] = "'nvdct/l3v4_topology' 'router'" +HOST_LABEL_LLDP: Final[str] = "'nvdct/has_lldp_neighbours' 'yes'" +LABEL_CDP: Final[str] = 'CDP' +LABEL_L3v4: Final[str] = 'LAYER3v4' +LABEL_LLDP: Final[str] = 'LLDP' +LOGGER: Final[str] = getLogger('root)') +LOG_FILE: Final[str] = f'{OMD_ROOT}/var/log/nvdct.log' +MIN_CDP_VERSION: Final[str] = '0.7.1-20240320' +MIN_LINUX_IP_ADDRESSES: Final[str] = '0.0.4-20241210' +MIN_SNMP_IP_ADDRESSES: Final[str] = '0.0.6-20241210' +MIN_WINDOWS_IP_ADDRESSES: Final[str] = '0.0.3-20241210' +MIN_LLDP_VERSION: Final[str] = '0.9.3-20240320' +PATH_CDP: Final[str] = 'networking,cdp_cache,neighbours' +PATH_INTERFACES: Final[str] = 'networking,interfaces' +PATH_L3v4: Final[str] = 'networking,addresses' +PATH_LLDP: Final[str] = 'networking,lldp_cache,neighbours' +SCRIPT: Final[str] = '~/local/bin/nvdct/nvdct.py' +TIME_FORMAT: Final[str] = '%Y-%m-%dT%H:%M:%S.%m' +TIME_FORMAT_ARGPARSER: Final[str] = '%%Y-%%m-%%dT%%H:%%M:%%S.%%m' +USER_DATA_FILE: Final[str] = 'nvdct.toml' diff --git a/source/bin/nvdct/lib/settings.py b/source/bin/nvdct/lib/settings.py index f8f4bc87e379266536e477fc4116b147c8addba8..d40af5356ec8670702c24417f0aed896f32dcf6c 100755 --- a/source/bin/nvdct/lib/settings.py +++ b/source/bin/nvdct/lib/settings.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # # License: GNU General Public License v2 - +from cmk_addons.plugins.bgp_topology.lib.utils import OMD_ROOT # Author: thl-cmk[at]outlook[dot]com # URL : https://thl-cmk.hopto.org # Date : 2023-10-12 @@ -13,20 +13,29 @@ from collections.abc import Mapping from ipaddress import AddressValueError, IPv4Address, IPv4Network, NetmaskValueError from logging import CRITICAL, FATAL, ERROR, WARNING, INFO, DEBUG -from os import environ from sys import exit as sys_exit from time import strftime from typing import Dict, List, NamedTuple, Tuple +from pathlib import Path +from lib.constants import ( + API_PORT, + LOGGER, + LOG_FILE, + OMD_ROOT, + TIME_FORMAT, + USER_DATA_FILE, +) from lib.utils import ( ExitCodes, + Layer, get_data_from_toml, get_local_cmk_api_port, - # get_local_cmk_version, - LOGGER, - TIME_FORMAT, - USER_DATA_FILE, - Layer + is_valid_customer_name, + is_valid_hostname, + is_valid_log_file, + is_valid_output_directory, + is_valid_site_name, ) @@ -45,11 +54,10 @@ class Thickness(NamedTuple): class StaticConnection(NamedTuple): - host: str - local_port: str - neighbour_port: str - neighbour: str - label: str + right_host: str + right_service: str + left_service: str + left_host: str class Wildcard(NamedTuple): @@ -65,11 +73,6 @@ class Settings: self, cli_args: Mapping[str, object], ): - self.__omd_root = environ['OMD_ROOT'] - self.__path_to_if_table = 'networking,interfaces' - self.__topology_file_name = 'network_data.json' - # self.__topology_save_path = 'var/topology_data' - self.__topology_save_path_cmk_2_3 = 'var/check_mk/topology/data' # cli defaults self.__settings = { # 'api_port': 80, @@ -80,10 +83,11 @@ class Settings: 'dont_compare': False, 'filter_customers': None, 'filter_sites': None, + 'include_l3_hosts': False, 'keep': False, 'remove_domain': False, - 'layers': ['CDP'], - 'log_file': f'{self.omd_root}/var/log/nvdct.log', + 'layers': [], + 'log_file': LOG_FILE, 'log_level': 'WARNING', 'log_to_stdout': False, 'min_age': 0, @@ -91,11 +95,10 @@ class Settings: 'prefix': None, 'quiet': False, 'pre_fetch': False, - # 'seed_devices': [], 'skip_l3_if': False, 'skip_l3_ip': False, 'time_format': TIME_FORMAT, - 'user_data_file': f'{self.omd_root}/local/bin/nvdct/conf/{USER_DATA_FILE}', + 'user_data_file': f'{OMD_ROOT}/local/bin/nvdct/conf/{USER_DATA_FILE}', } # args in the form {'s, __seed_devices': 'CORE01', 'p, __path_in_inventory': None, ... }} # we will remove 's, __' @@ -128,7 +131,6 @@ class Settings: self.__api_port: int | None = None # init user data with defaults - # self.__drop_host_regex: List[str] | None = None self.__custom_layers: List[StaticConnection] | None = None self.__customers: List[str] | None = None self.__emblems: Emblems | None = None @@ -157,7 +159,7 @@ class Settings: else: self.__api_port = get_local_cmk_api_port() if self.__api_port is None: - self.__api_port = 80 + self.__api_port = API_PORT return self.__api_port @@ -217,6 +219,10 @@ class Settings: ) return None + @property # --include-l3-hosts + def include_l3_hosts(self) -> bool: + return bool(self.__settings['include_l3_hosts']) + @property # --keep def keep(self) -> int | None: if isinstance(self.__settings['keep'], int): @@ -233,27 +239,25 @@ class Settings: @property # --log-file def log_file(self) -> str: - return str(self.__settings['log_file']) + raw_log_file = str(Path(str(self.__settings['log_file'])).expanduser()) + if is_valid_log_file(raw_log_file): + return raw_log_file + else: + LOGGER.error(f'Falling back to {LOG_FILE}') + return LOG_FILE @property # --log-level def loglevel(self) -> int: - match self.__settings['log_level']: - case 'DEBUG': - return DEBUG - case 'INFO': - return INFO - case 'WARNING': - return WARNING - case 'ERROR': - return ERROR - case 'FATAL': - return FATAL - case 'CRITICAL': - return CRITICAL - case 'OFF': - return -1 - case _: - return WARNING + log_levels = { + 'DEBUG': DEBUG, + 'INFO': INFO, + 'WARNING': WARNING, + 'ERROR': ERROR, + 'FATAL': FATAL, + 'CRITICAL': CRITICAL, + 'OFF': -1, + } + return log_levels.get(self.__settings['log_level'], WARNING) @property # --log-to-stdout def log_to_stdtout(self) -> bool: @@ -271,7 +275,11 @@ class Settings: # init output directory with current time if not set if not self.__settings['output_directory']: self.__settings['output_directory'] = f'{strftime(self.__settings["time_format"])}' - return str(self.__settings['output_directory']) + if is_valid_output_directory(str(self.__settings['output_directory'])): + return str(self.__settings['output_directory']) + else: + LOGGER.error('Falling back to "nvdct"') + return 'nvdct' @property # --prefix def prefix(self) -> str | None: @@ -311,7 +319,7 @@ class Settings: if self.__customers is None: self.__customers = [ str(customer) for customer in set(self.__user_data.get('CUSTOMERS', [])) - ] + if is_valid_customer_name(customer)] LOGGER.info(f'Found {len(self.__customers)} to filter on') return self.__customers @@ -362,9 +370,7 @@ class Settings: def l2_seed_devices(self) -> List[str]: if self.__l2_seed_devices is None: self.__l2_seed_devices = list(set(str(host) for host in ( - # self.__user_data.get('L2_SEED_DEVICES', []) + self.__settings.get('seed_devices', []) - self.__user_data.get('L2_SEED_DEVICES', []) - ))) + self.__user_data.get('L2_SEED_DEVICES', [])) if is_valid_hostname(host))) return self.__l2_seed_devices @property @@ -373,7 +379,7 @@ class Settings: self.__l2_host_map = { str(host): str(replace_host) for host, replace_host in self.__user_data.get( 'L2_HOST_MAP', {} - ).items() + ).items() if is_valid_hostname(host) } return self.__l2_host_map @@ -392,7 +398,7 @@ class Settings: if self.__l3v4_ignore_hosts is None: self.__l3v4_ignore_hosts = [str(host) for host in set(self.__user_data.get( 'L3V4_IGNORE_HOSTS', [] - ))] + )) if is_valid_hostname(host)] return self.__l3v4_ignore_hosts @property @@ -460,9 +466,12 @@ class Settings: _ip_network = IPv4Network(ip_network) # noqa: F841 except (AddressValueError, NetmaskValueError): LOGGER.error( - f'Invalid entry in L3V4_REPLACE found: {ip_network} -> ignored' + f'Invalid entry in L3V4_REPLACE found: {ip_network} -> line ignored' ) continue + if not is_valid_hostname(node): + LOGGER.error(f'Invalid node name found: {node} -> line ignored ') + continue self.__l3v4_replace[ip_network] = str(node) LOGGER.info( f'Valid entries in L3V4_REPLACE found: {len(self.__l3v4_replace)}/' @@ -526,18 +535,22 @@ class Settings: self.__static_connections = [] for connection in self.__user_data.get('STATIC_CONNECTIONS', []): try: - host, local_port, neighbour_port, neighbour, label = connection + left_host, left_service, right_service, right_host = connection except ValueError: LOGGER.error( - f'Worng entry in STATIC_CONNECTIONS -> {connection} -> ignored' + f'Wrong entry in STATIC_CONNECTIONS -> {connection} -> ignored' ) continue + if not right_host or not left_host: + LOGGER.warning(f'Both hosts must be set, got {connection}') + continue + if not is_valid_hostname(right_host) or not is_valid_hostname(left_host): + continue self.__static_connections.append(StaticConnection( - host=str(host), - local_port=str(local_port), - neighbour_port=str(neighbour_port), - neighbour=str(neighbour), - label=str(label), + right_host=str(right_host), + right_service=str(right_service), + left_service=str(left_service), + left_host=str(left_host), )) LOGGER.info( f'Valid entries in STATIC_CONNECTIONS found: {len(self.__static_connections)}/' @@ -548,21 +561,6 @@ class Settings: @property def sites(self) -> List[str]: if self.__sites is None: - self.__sites = [str(site) for site in set(self.__user_data.get('SITES', []))] - LOGGER.info(f'fFound {len(self.__sites)} to filter on') + self.__sites = [str(site) for site in set(self.__user_data.get('SITES', [])) if is_valid_site_name(site)] + LOGGER.info(f'Found {len(self.__sites)} to filter on') return self.__sites - - # - # all other settings - # - @property - def omd_root(self) -> str: - return self.__omd_root - - @property - def topology_save_path(self) -> str: - return self.__topology_save_path_cmk_2_3 - - @property - def topology_file_name(self) -> str: - return self.__topology_file_name diff --git a/source/bin/nvdct/lib/topologies.py b/source/bin/nvdct/lib/topologies.py index 8c35a45b2a6062d07f37c1198a2f994e6b748a7f..026b6fc46b0348f35404e812aac9b83a018887a2 100755 --- a/source/bin/nvdct/lib/topologies.py +++ b/source/bin/nvdct/lib/topologies.py @@ -10,11 +10,32 @@ from collections.abc import Mapping, Sequence from ipaddress import IPv4Address, IPv4Network -from typing import Dict, List - -from lib.backends import CACHE_INTERFACES_DATA, CacheItems, HostCache, PATH_INTERFACES -from lib.settings import Thickness, Wildcard -from lib.utils import LOGGER +from typing import Dict, List, Tuple +from re import sub as re_sub + +from lib.backends import ( + CacheItems, + HostCache, +) +from lib.constants import ( + CACHE_INTERFACES_DATA, + HOST_LABEL_L3V4_HOSTS, + HOST_LABEL_L3V4_ROUTER, + LOGGER, + PATH_INTERFACES, + PATH_L3v4, +) +from lib.settings import ( + Emblems, + StaticConnection, + Thickness, + Wildcard, +) +from lib.utils import ( + InventoryColumns, + Ipv4Info, + is_valid_hostname, +) class NvObjects: @@ -23,12 +44,15 @@ class NvObjects: self.host_count: int = 0 self.host_list: List[str] = [] - def add_host_object( + def add_host( self, host: str, host_cache: HostCache, emblem: str | None = None ) -> None: + if not is_valid_hostname(host): + LOGGER.error(f'host not added! Invalid name {host}') + return if host not in self.nv_objects: self.host_count += 1 self.host_list.append(host) @@ -57,7 +81,45 @@ class NvObjects: } LOGGER.debug(f'host: {host}, link: {link}, metadata: {metadata}') - def add_service_object( + def add_service( + self, + host: str, + service: str, + host_cache: HostCache, + emblem: str | None = None, + metadata: Dict | None = None, + name: str | None = None, + ) -> None: + if metadata is None: + metadata = {} + if name is None: + name = service + + self.add_host(host=host, host_cache=host_cache) + service_object = f'{service}@{host}' + if service_object not in self.nv_objects: + link: Dict = {} + if host_cache.host_exists(host=host): + link = {'core': [host, service]} + elif emblem is not None: + metadata.update({ + 'images': { + 'emblem': emblem, # node image + # 'icon': 'icon_tick', # status image, top left from emblem + }, + **({'tooltip': {'quickinfo': [{'name': 'Service node', 'value': service}]}} if not link else {}) + }) + + self.nv_objects[service_object] = { + 'name': name, + 'link': link, + 'metadata': metadata, + } + elif metadata is not {}: + self.nv_objects[service_object]['metadata'].update(metadata) + + + def add_interface( self, host: str, service: str, @@ -73,7 +135,7 @@ class NvObjects: name = service speed = None - self.add_host_object(host=host, host_cache=host_cache) + self.add_host(host=host, host_cache=host_cache) service_object = f'{service}@{host}' if service_object not in self.nv_objects: link: Dict = {} @@ -106,7 +168,7 @@ class NvObjects: elif metadata is not {}: self.nv_objects[service_object]['metadata'].update(metadata) - def add_ipv4_address_object( + def add_ipv4_address( self, host: str, ipv4_address: str, @@ -135,7 +197,7 @@ class NvObjects: } } - def add_ipv4_network_object(self, network: str, emblem: str, ) -> None: + def add_ipv4_network(self, network: str, emblem: str, ) -> None: if network not in self.nv_objects: self.nv_objects[network] = { 'name': network, @@ -339,19 +401,19 @@ def get_service_by_interface(host: str, interface: str, host_cache: HostCache) - # 'management': 'Ma', } - def _get_short_if_name(interface: str) -> str: + def _get_short_if_name(interface_: str) -> str: """ returns short interface name from long interface name interface: is the long interface name - :type interface: str + :type interface_: str """ - if not interface: - return interface + if not interface_: + return interface_ for interface_prefix in _alternate_if_name.keys(): - if interface.lower().startswith(interface_prefix.lower()): + if interface_.lower().startswith(interface_prefix.lower()): interface_short = _alternate_if_name[interface_prefix] - return interface.lower().replace(interface_prefix.lower(), interface_short, 1) - return interface + return interface_.lower().replace(interface_prefix.lower(), interface_short, 1) + return interface_ # try to find the item for an interface def _match_entry_with_item(_entry: Mapping[str, str], services: Sequence[str]) -> str | None: @@ -552,3 +614,387 @@ def get_list_of_devices(data: Mapping) -> List[str]: for connection in data.values(): devices.append(connection[0]) return list(set(devices)) + + +def create_static_connections( + connections_: Sequence[StaticConnection], + emblems: Emblems, + host_cache: HostCache, + nv_connections: NvConnections, + nv_objects: NvObjects, +): + for connection in connections_: + LOGGER.info(msg=f'connection: {connection}') + nv_objects.add_host( + host=connection.right_host, + host_cache=host_cache, + emblem=emblems.host_node + ) + nv_objects.add_host( + host=connection.left_host, + host_cache=host_cache, + emblem=emblems.host_node + ) + if connection.right_service: + nv_objects.add_service( + host=connection.right_host, + host_cache=host_cache, + emblem=emblems.service_node, + service=connection.right_service + ) + nv_connections.add_connection( + left=connection.right_host, + right=f'{connection.right_service}@{connection.right_host}', + ) + + if connection.left_service: + nv_objects.add_service( + host=connection.left_host, + host_cache=host_cache, + emblem=emblems.service_node, + service=connection.left_service + ) + nv_connections.add_connection( + left=connection.left_host, + right=f'{connection.left_service}@{connection.left_host}', + ) + + if connection.right_service and connection.left_service: + nv_connections.add_connection( + left=f'{connection.right_service}@{connection.right_host}', + right=f'{connection.left_service}@{connection.left_host}', + ) + elif connection.right_service: # connect right_service with left_host + nv_connections.add_connection( + left=f'{connection.right_service}@{connection.right_host}', + right=f'{connection.left_host}', + ) + elif connection.left_service: # connect left_service with right_host + nv_connections.add_connection( + left=f'{connection.right_host}', + right=f'{connection.left_service}@{connection.left_host}', + ) + else: # connect right_host with left_host + nv_connections.add_connection( + left=f'{connection.right_host}', + right=f'{connection.left_host}', + ) + + +def create_l2_device_from_inv( + case: str, + host: str, + host_cache: HostCache, + inv_columns: InventoryColumns, + inv_data: Sequence[Mapping[str, str]], + l2_drop_hosts: List, + l2_host_map: Dict[str, str], + l2_neighbour_replace_regex: List[Tuple[str, str]] | None, + nv_connections: NvConnections, + nv_objects: NvObjects, + prefix: str, + remove_domain: bool, +) -> None: + for topo_neighbour in inv_data: + # check if required data are not empty + if not (neighbour := topo_neighbour.get(inv_columns.neighbour)): + LOGGER.warning(f'incomplete data, neighbour missing {topo_neighbour}') + continue + if not (raw_local_port := topo_neighbour.get(inv_columns.local_port)): + LOGGER.warning(f'incomplete data, local port missing {topo_neighbour}') + continue + if not (raw_neighbour_port := topo_neighbour.get(inv_columns.neighbour_port)): + LOGGER.warning(f'incomplete data, neighbour port missing {topo_neighbour}') + continue + + # drop neighbour before domain split + if neighbour in l2_drop_hosts: + LOGGER.info(msg=f'drop neighbour: {neighbour}') + continue + + if l2_neighbour_replace_regex: + for re_str, replace_str in l2_neighbour_replace_regex: + re_neighbour = re_sub(re_str, replace_str, neighbour) + if re_neighbour != neighbour: + LOGGER.info(f'regex changed Neighbor |{neighbour}| to |{re_neighbour}|') + neighbour = re_neighbour + if not neighbour: + LOGGER.info(f'Neighbour removed by regex (|{neighbour}|, |{re_str}|, |{replace_str}|)') + break + if not neighbour: + continue + + if remove_domain: + neighbour = neighbour.split('.')[0] + + # drop neighbour after domain split + if neighbour in l2_drop_hosts: + LOGGER.info(msg=f'drop neighbour: {neighbour}') + continue + + if case == 'UPPER': + neighbour = neighbour.upper() + LOGGER.debug(f'Changed neighbour to upper case: {neighbour}') + elif case == 'LOWER': + neighbour = neighbour.lower() + LOGGER.debug(f'Changed neighbour to lower case: {neighbour}') + + if prefix: + neighbour = f'{prefix}{neighbour}' + # rewrite neighbour if inventory neighbour and checkmk host don't match + if neighbour in l2_host_map.keys(): + neighbour = l2_host_map[neighbour] + + # getting/checking interfaces + local_port = get_service_by_interface(host, raw_local_port, host_cache) + if not local_port: + local_port = raw_local_port + LOGGER.warning(msg=f'service not found: host: {host}, raw_local_port: {raw_local_port}') + elif local_port != raw_local_port: + # local_port = raw_local_port # don't reset local_port + LOGGER.info( + msg=f'host: {host}, raw_local_port: {raw_local_port} -> local_port: {local_port}' + ) + + neighbour_port = get_service_by_interface(neighbour, raw_neighbour_port, host_cache) + if not neighbour_port: + neighbour_port = raw_neighbour_port + LOGGER.warning( + msg=f'service not found: neighbour: {neighbour}, ' + f'raw_neighbour_port: {raw_neighbour_port}' + ) + elif neighbour_port != raw_neighbour_port: + # neighbour_port = raw_neighbour_port # don't reset neighbour_port + LOGGER.info( + msg=f'neighbour: {neighbour}, raw_neighbour_port {raw_neighbour_port} ' + f'-> neighbour_port {neighbour_port}' + ) + + metadata = { + 'duplex': topo_neighbour.get('duplex'), + 'native_vlan': topo_neighbour.get('native_vlan'), + } + + nv_objects.add_host(host=host, host_cache=host_cache) + nv_objects.add_host(host=neighbour, host_cache=host_cache) + nv_objects.add_interface( + host=host, + service=local_port, + host_cache=host_cache, + metadata=metadata, + name=raw_local_port, + item=local_port + ) + nv_objects.add_interface( + host=neighbour, + service=neighbour_port, + host_cache=host_cache, + name=raw_neighbour_port, + item=neighbour_port + ) + nv_connections.add_connection( + left=host, + right=f'{local_port}@{host}', + ) + nv_connections.add_connection( + left=neighbour, + right=f'{neighbour_port}@{neighbour}', + ) + nv_connections.add_connection( + left=f'{local_port}@{host}', + right=f'{neighbour_port}@{neighbour}', + ) + + +def create_l2_topology( + case: str, + host_cache: HostCache, + inv_columns: InventoryColumns, + l2_drop_hosts: List[str], + l2_host_map: Dict[str, str], + l2_neighbour_replace_regex: List[Tuple[str, str]], + label_: str, + nv_connections: NvConnections, + nv_objects: NvObjects, + path_in_inventory: str, + prefix: str, + remove_domain: bool, + seed_devices: Sequence[str], +) -> None: + devices_to_go = list(set(seed_devices)) # remove duplicates + devices_done = [] + + while devices_to_go: + device = devices_to_go[0] + + if device in l2_host_map.keys(): + try: + devices_to_go.remove(device) + except ValueError: + pass + device = l2_host_map[device] + if device in devices_done: + continue + + topo_data = host_cache.get_data( + host=device, item=CacheItems.inventory, path=path_in_inventory + ) + if topo_data: + create_l2_device_from_inv( + host=device, + inv_data=topo_data, + inv_columns=inv_columns, + l2_host_map=l2_host_map, + l2_drop_hosts=l2_drop_hosts, + l2_neighbour_replace_regex=l2_neighbour_replace_regex, + host_cache=host_cache, + nv_objects=nv_objects, + nv_connections=nv_connections, + case=case, + prefix=prefix, + remove_domain=remove_domain + ) + + for _entry in nv_objects.host_list: + if _entry not in devices_done: + devices_to_go.append(_entry) + + devices_to_go = list(set(devices_to_go)) + devices_done.append(device) + devices_to_go.remove(device) + LOGGER.info(msg=f'Device done: {device}, source: {label_}') + + +def create_l3v4_topology( + emblems: Emblems, + host_cache: HostCache, + ignore_hosts: Sequence[str], + ignore_ips: Sequence[IPv4Network], + ignore_wildcard: Sequence[Wildcard], + include_hosts: bool, + nv_connections: NvConnections, + nv_objects: NvObjects, + replace: Mapping[str, str], + skip_if: bool, + skip_ip: bool, + summarize: Sequence[IPv4Network], +) -> None: + host_list: Sequence[str] = host_cache.get_hosts_by_label(HOST_LABEL_L3V4_ROUTER) + + if include_hosts: + host_list += host_cache.get_hosts_by_label(HOST_LABEL_L3V4_HOSTS) + + LOGGER.debug(f'host list: {host_list}') + if not host_list: + LOGGER.warning( + msg='No (routing capable) host found. Check if "inv_ip_addresses.mkp" ' + 'added/enabled and inventory and host label discovery has run.' + ) + return + + LOGGER.debug(f'L3v4 ignore hosts: {ignore_hosts}') + for raw_host in host_list: + host = raw_host + if host in ignore_hosts: + LOGGER.info(f'L3v4 host {host} ignored') + continue + if not (ipv4_addresses := host_cache.get_data( + host=host, item=CacheItems.inventory, path=PATH_L3v4) + ): + LOGGER.warning(f'No IPv4 address inventory found for host: {host}') + continue + + nv_objects.add_host(host=host, host_cache=host_cache) + for _entry in ipv4_addresses: + emblem = emblems.ip_network + try: + ipv4_info = Ipv4Info(**_entry) + except TypeError: # as e + LOGGER.warning(f'Drop IPv4 address data for host: {host}, data: {_entry}') + continue + + if ipv4_info.address.startswith('127.'): # drop loopback addresses + LOGGER.info(f'host: {host} dropped loopback address: {ipv4_info.address}') + continue + + if ipv4_info.cidr == 32: # drop host addresses + LOGGER.info( + f'host: {host} dropped host address: {ipv4_info.address}/{ipv4_info.cidr}' + ) + continue + + if ipv4_info.type.lower() != 'ipv4': # drop if not ipv4 + LOGGER.warning( + f'host: {host} dropped non ipv4 address: {ipv4_info.address},' + ' type: {ipv4_info.type}' + ) + continue + + if is_ignore_ipv4(ipv4_info.address, ignore_ips): + LOGGER.info(f'host: {host} dropped ignore address: {ipv4_info.address}') + continue + + if is_ignore_wildcard(ipv4_info.address, ignore_wildcard): + LOGGER.info(f'host: {host} dropped wildcard address: {ipv4_info.address}') + continue + + if network := get_network_summary( + ipv4_address=ipv4_info.address, + summarize=summarize, + ): + emblem = emblems.l3v4_summarize + LOGGER.info( + f'Network summarized: {ipv4_info.network}/{ipv4_info.cidr} -> {network}' + ) + else: + network = f'{ipv4_info.network}/{ipv4_info.cidr}' + + if network in replace.keys(): + LOGGER.info(f'Replaced network {network} with {replace[network]}') + network = replace[network] + emblem = emblems.l3v4_replace + + nv_objects.add_ipv4_network(network=network, emblem=emblem) + + if skip_if is True and skip_ip is True: + nv_connections.add_connection(left=host, right=network) + elif skip_if is True and skip_ip is False: + nv_objects.add_ipv4_address( + host=host, + interface=None, + ipv4_address=ipv4_info.address, + emblem=emblems.ip_address, + ) + nv_objects.add_tooltip_quickinfo( + '{ipv4_info.address}@{host}', 'Interface', ipv4_info.device + ) + nv_connections.add_connection(left=f'{host}', right=f'{ipv4_info.address}@{host}') + nv_connections.add_connection(left=network, right=f'{ipv4_info.address}@{host}') + elif skip_if is False and skip_ip is True: + nv_objects.add_interface( + host=host, service=ipv4_info.device, host_cache=host_cache + ) + nv_objects.add_tooltip_quickinfo( + f'{ipv4_info.device}@{host}', 'IP-address', ipv4_info.address + ) + nv_connections.add_connection(left=f'{host}', right=f'{ipv4_info.device}@{host}') + nv_connections.add_connection(left=network, right=f'{ipv4_info.device}@{host}') + else: + nv_objects.add_ipv4_address( + host=host, + interface=ipv4_info.device, + ipv4_address=ipv4_info.address, + emblem=emblems.ip_address, + ) + nv_objects.add_interface( + host=host, service=ipv4_info.device, host_cache=host_cache, + ) + nv_connections.add_connection( + left=host, right=f'{ipv4_info.device}@{host}') + nv_connections.add_connection( + left=f'{ipv4_info.device}@{host}', + right=f'{ipv4_info.address}@{ipv4_info.device}@{host}', + ) + nv_connections.add_connection( + left=network, right=f'{ipv4_info.address}@{ipv4_info.device}@{host}', + ) diff --git a/source/bin/nvdct/lib/utils.py b/source/bin/nvdct/lib/utils.py index 1857bce35e32bca12b32cdbcd778a634646b638f..4bd7f573474d8f75a4463ad77e5e089fa1a2c6ba 100755 --- a/source/bin/nvdct/lib/utils.py +++ b/source/bin/nvdct/lib/utils.py @@ -14,7 +14,6 @@ from enum import Enum, unique from json import dumps from logging import disable as log_off, Formatter, getLogger, StreamHandler from logging.handlers import RotatingFileHandler -from os import environ from pathlib import Path from re import match as re_match from socket import socket, AF_UNIX, AF_INET, SOCK_STREAM, SHUT_WR @@ -23,8 +22,23 @@ from time import time as now_time from tomllib import loads as toml_loads, TOMLDecodeError from typing import List, Dict, TextIO -NVDCT_VERSION = '0.9.3-20241209' - +from lib.constants import ( + CMK_SITE_CONF, + COLUMNS_CDP, + COLUMNS_LLDP, + HOST_LABEL_CDP, + HOST_LABEL_L3V4_ROUTER, + HOST_LABEL_LLDP, + LABEL_CDP, + LABEL_L3v4, + LABEL_LLDP, + LOGGER, + OMD_ROOT, + PATH_CDP, + PATH_L3v4, + PATH_LLDP, + DATAPATH, +) @unique class ExitCodes(Enum): @@ -33,15 +47,7 @@ class ExitCodes(Enum): BAD_TOML_FORMAT = 2 BACKEND_NOT_IMPLEMENTED = 3 AUTOMATION_SECRET_NOT_FOUND = 4 - - -@dataclass(frozen=True) -class Layer: - path: str - columns: str - label: str - host_label: str - + NO_LAYER_CONFIGURED = 5 @dataclass(frozen=True) class Ipv4Info: @@ -53,44 +59,18 @@ class Ipv4Info: network: str type: str - @dataclass(frozen=True) class InventoryColumns: neighbour: str local_port: str neighbour_port: str - -# constants -OMD_ROOT = environ["OMD_ROOT"] - -CACHE_INTERFACES_DATA = 'interface_data' -CMK_SITE_CONF = f'{OMD_ROOT}/etc/omd/site.conf' -COLUMNS_CDP = 'neighbour_name,local_port,neighbour_port' -COLUMNS_L3v4 = 'address,device,cidr,network,type' -COLUMNS_LLDP = 'neighbour_name,local_port,neighbour_port' -HOME_URL = 'https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/nvdct' -HOST_LABEL_CDP = "'nvdct/has_cdp_neighbours' 'yes'" -HOST_LABEL_L3V4 = "'nvdct/l3v4_topology' 'router'" -HOST_LABEL_LLDP = "'nvdct/has_lldp_neighbours' 'yes'" -LABEL_CDP = 'CDP' -LABEL_L3v4 = 'LAYER3v4' -LABEL_LLDP = 'LLDP' -LOG_FILE = f'{OMD_ROOT}/var/log/nvdct.log' -LOGGER = getLogger('root)') -MIN_CDP_VERSION = '0.7.1-20240320' -MIN_IP_ADDRESSES = '0.0.5-2024120' -MIN_LLDP_VERSION = '0.9.3-20240320' -PATH_CDP = 'networking,cdp_cache,neighbours' -PATH_INTERFACES = 'networking,interfaces' -PATH_L3v4 = 'networking,addresses' -PATH_LLDP = 'networking,lldp_cache,neighbours' -SAMPLE_SEEDS = 'Core01 Core02' -SCRIPT = '~/local/bin/nvdct/nvdct.py' -TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%m' -TIME_FORMAT_ARGPARSER = '%%Y-%%m-%%dT%%H:%%M:%%S.%%m' -USER_DATA_FILE = 'nvdct.toml' - +@dataclass(frozen=True) +class Layer: + path: str + columns: str + label: str + host_label: str LAYERS = { 'CDP': Layer( @@ -109,7 +89,7 @@ LAYERS = { path=PATH_L3v4, columns='', label=LABEL_L3v4, - host_label=HOST_LABEL_L3V4, + host_label=HOST_LABEL_L3V4_ROUTER, ), } @@ -164,9 +144,7 @@ def get_data_from_toml(file: str) -> Dict: def rm_tree(root: Path) -> None: # safety - if not str(root).startswith( - f'{OMD_ROOT}/var/topology_data' - ) and not str(root).startswith(f'{OMD_ROOT}/var/check_mk/topology'): + if not str(root).startswith(DATAPATH): LOGGER.warning(msg=f"WARNING: bad path to remove, {str(root)}, don\'t delete it.") return for p in root.iterdir(): @@ -323,6 +301,47 @@ def is_list_of_str_equal(list1: List[str], list2: List[str]) -> bool: return tmp_list1 == tmp_list2 +def is_valid_hostname(host: str) -> bool: + re_host_pattern = r'^[0-9a-z-A-Z\.\-\_]{1,253}$' + if re_match(re_host_pattern, host): + return True + else: + LOGGER.error(f'Invalid hostname found: {host}') + return False + +def is_valid_site_name(site: str) -> bool: + re_host_pattern = r'^[0-9a-z-A-Z\.\-\_]{1,16}$' + if re_match(re_host_pattern, site): + return True + else: + LOGGER.error(f'Invalid site name found: {site}') + return False + +def is_valid_customer_name(customer: str) -> bool: + re_host_pattern = r'^[0-9a-z-A-Z\.\-\_]{1,16}$' + if re_match(re_host_pattern, customer): + return True + else: + LOGGER.error(f'Invalid customer name found: {customer}') + return False + + +def is_valid_output_directory(directory: str) -> bool: + # 2024-12-11T17:35:08.12 + re_host_pattern = r'^[0-9a-z-A-Z\.\-\_\:]{1,30}$' + if re_match(re_host_pattern, directory): + return True + else: + LOGGER.error(f'Invalid output directory name found: {directory}') + return False + +def is_valid_log_file(log_file: str) -> bool: + if not log_file.startswith(f'{OMD_ROOT}/var/log/'): + LOGGER.error(f'Logg file needs to be under "{OMD_ROOT}/var/log/"! Got {Path(log_file).absolute()}') + return False + return True + + # not used in cmk 2.3.x format def merge_topologies(topo_pri: Dict, topo_sec: Dict) -> Dict: """ @@ -410,8 +429,7 @@ def configure_logger(log_file: str, log_level: int, log_to_console: bool) -> Non log = getLogger() log_formatter = Formatter( - fmt='%(asctime)s :: %(levelname)s :: %(module)s :' - ': %(funcName)s() :: %(lineno)s :: %(message)s', + fmt='%(asctime)s :: %(levelname)s :: %(module)s :: %(funcName)s() :: %(lineno)s :: %(message)s', ) log.setLevel(log_level) diff --git a/source/bin/nvdct/nvdct.py b/source/bin/nvdct/nvdct.py index 16254c3cf373818ccd19a779382cef9aadadd02b..80755bf9c7a35ba4a238f440198b4eb3d829572f 100755 --- a/source/bin/nvdct/nvdct.py +++ b/source/bin/nvdct/nvdct.py @@ -133,7 +133,14 @@ # incompatible changed the option keep-domain to remove-domain -> don't mess with neighbor names by default # 2024-12-08: incompatible: changed hostlabel for L3v4 topology to nvdct/l3v4_topology # needs at least inv_ip_address inv_ip_address-0.0.5-20241209.mkp - +# 2024-12-09: added option --include-l3-hosts +# added site filter for RESTAPI backend +# enabled customer filter for MULTISITE backend +# 2024-12-10: refactoring: moved topology code to topologies, removed all global variables, created main() function +# 2024-12-11: incompatible: changed default layers to None -> use the CLI option -l CDP or the configfile instead +# incompatible: reworked static topology -> can now be used for each service, host/service name has to be +# exactly like in CMK. See ~/local/bin/nvdct/conf/nfdct.toml +# moved string constants to lib/constants.py # creating topology data json from inventory data # @@ -147,7 +154,9 @@ # The inventory data could be created with my CDP/LLDP/IP Address/Interface nane inventory plugins: # CDP.....: https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_cdp_cache # LLDP....: https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_lldp_cache -# L3v4....: https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_ipv4_addresses +# L3v4....: https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_ip_addresses +# : https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_lnx_if_ip +# : https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_win_if_ip # IF Name.: https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_ifname # # USAGE: @@ -229,554 +238,233 @@ __data = { """ import sys -from collections.abc import Mapping, Sequence -from ipaddress import IPv4Network from logging import DEBUG -from re import sub as re_sub from time import strftime, time_ns -from typing import Dict, List, Tuple +from typing import List from lib.args import parse_arguments from lib.backends import ( - CacheItems, HostCache, HostCacheLiveStatus, HostCacheMultiSite, HostCacheRestApi, ) +from lib.constants import ( + HOME_URL, + LOGGER, + NVDCT_VERSION, + DATAPATH, +) +from lib.settings import Settings from lib.topologies import ( NvConnections, NvObjects, - # get_list_of_devices, - get_network_summary, - get_service_by_interface, - is_ignore_ipv4, - is_ignore_wildcard, + create_l2_topology, + create_l3v4_topology, + create_static_connections, ) from lib.utils import ( - configure_logger, ExitCodes, - HOME_URL, - HOST_LABEL_L3V4, InventoryColumns, - Ipv4Info, - Layer, LAYERS, - LOGGER, - # merge_topologies, - NVDCT_VERSION, - PATH_L3v4, + Layer, + StdoutQuiet, + configure_logger, remove_old_data, save_data_to_file, - # save_topology, - StdoutQuiet, -) -from lib.settings import ( - Emblems, - Settings, - StaticConnection, - Thickness, - Wildcard, ) -EMBLEMS: Emblems -HOST_CACHE: HostCache -L2_DROP_HOSTS: List[str] = [] -L2_HOST_MAP: Dict[str, str] = {} -L2_NEIGHBOUR_REPLACE_REGEX: List[Tuple[str, str]] | None = None -MAP_SPEED_TO_THICKNESS: List[Thickness] = [] -NV_CONNECTIONS = NvConnections() -NV_OBJECTS = NvObjects() -SETTINGS: Settings - - -def create_l2_device_from_inv( - host: str, - inv_data: Sequence[Mapping[str, str]], - inv_columns: InventoryColumns, - # label: str, -) -> None: - for topo_neighbour in inv_data: - # check if required data are not empty - if not (neighbour := topo_neighbour.get(inv_columns.neighbour)): - LOGGER.warning(f'incomplete data, neighbour missing {topo_neighbour}') - continue - if not (raw_local_port := topo_neighbour.get(inv_columns.local_port)): - LOGGER.warning(f'incomplete data, local port missing {topo_neighbour}') - continue - if not (raw_neighbour_port := topo_neighbour.get(inv_columns.neighbour_port)): - LOGGER.warning(f'incomplete data, neighbour port missing {topo_neighbour}') - continue - - # drop neighbour before domain split - if neighbour in L2_DROP_HOSTS: - LOGGER.info(msg=f'drop neighbour: {neighbour}') - continue - - if L2_NEIGHBOUR_REPLACE_REGEX: - for re_str, replace_str in L2_NEIGHBOUR_REPLACE_REGEX: - re_neighbour = re_sub(re_str, replace_str, neighbour) - if re_neighbour != neighbour: - LOGGER.info(f'regex changed Neighbor |{neighbour}| to |{re_neighbour}|') - neighbour = re_neighbour - if not neighbour: - LOGGER.info(f'Neighbour removed by regex (|{neighbour}|, |{re_str}|, |{replace_str}|)') - break - if not neighbour: - continue - - if SETTINGS.remove_domain: - neighbour = neighbour.split('.')[0] - - # drop neighbour after domain split - if neighbour in L2_DROP_HOSTS: - LOGGER.info(msg=f'drop neighbour: {neighbour}') - continue - - if SETTINGS.case == 'UPPER': - neighbour = neighbour.upper() - LOGGER.debug(f'Changed neighbour to upper case: {neighbour}') - elif SETTINGS.case == 'LOWER': - neighbour = neighbour.lower() - LOGGER.debug(f'Changed neighbour to lower case: {neighbour}') - - if SETTINGS.prefix: - neighbour = f'{SETTINGS.prefix}{neighbour}' - # rewrite neighbour if inventory neighbour and checkmk host don't match - if neighbour in L2_HOST_MAP.keys(): - neighbour = L2_HOST_MAP[neighbour] - - # getting/checking interfaces - local_port = get_service_by_interface(host, raw_local_port, HOST_CACHE) - if not local_port: - local_port = raw_local_port - LOGGER.warning(msg=f'service not found: host: {host}, raw_local_port: {raw_local_port}') - elif local_port != raw_local_port: - # local_port = raw_local_port # don't reset local_port - LOGGER.info( - msg=f'host: {host}, raw_local_port: {raw_local_port} -> local_port: {local_port}' - ) - - neighbour_port = get_service_by_interface(neighbour, raw_neighbour_port, HOST_CACHE) - if not neighbour_port: - neighbour_port = raw_neighbour_port - LOGGER.warning( - msg=f'service not found: neighbour: {neighbour}, ' - f'raw_neighbour_port: {raw_neighbour_port}' - ) - elif neighbour_port != raw_neighbour_port: - # neighbour_port = raw_neighbour_port # don't reset neighbour_port - LOGGER.info( - msg=f'neighbour: {neighbour}, raw_neighbour_port {raw_neighbour_port} ' - f'-> neighbour_port {neighbour_port}' - ) - - metadata = { - 'duplex': topo_neighbour.get('duplex'), - 'native_vlan': topo_neighbour.get('native_vlan'), - } - - NV_OBJECTS.add_host_object(host=host, host_cache=HOST_CACHE) - NV_OBJECTS.add_host_object(host=neighbour, host_cache=HOST_CACHE) - NV_OBJECTS.add_service_object( - host=host, - service=local_port, - host_cache=HOST_CACHE, - metadata=metadata, - name=raw_local_port, - item=local_port - ) - NV_OBJECTS.add_service_object( - host=neighbour, - service=neighbour_port, - host_cache=HOST_CACHE, - name=raw_neighbour_port, - item=neighbour_port - ) - NV_CONNECTIONS.add_connection( - left=host, - right=f'{local_port}@{host}', - ) - NV_CONNECTIONS.add_connection( - left=neighbour, - right=f'{neighbour_port}@{neighbour}', - ) - NV_CONNECTIONS.add_connection( - left=f'{local_port}@{host}', - right=f'{neighbour_port}@{neighbour}', - ) - - -def create_static_connections(connections: Sequence[StaticConnection]): - for connection in connections: - LOGGER.info(msg=f'connection: {connection}') - NV_OBJECTS.add_host_object( - host=connection.host, - host_cache=HOST_CACHE, - emblem=EMBLEMS.host_node - ) - NV_OBJECTS.add_host_object( - host=connection.neighbour, - host_cache=HOST_CACHE, - emblem=EMBLEMS.host_node - ) - NV_OBJECTS.add_service_object( - host=connection.host, - host_cache=HOST_CACHE, - emblem=EMBLEMS.service_node, - service=connection.local_port - ) - NV_OBJECTS.add_service_object( - host=connection.neighbour, - host_cache=HOST_CACHE, - emblem=EMBLEMS.service_node, - service=connection.neighbour_port - ) - NV_CONNECTIONS.add_connection( - left=connection.host, - right=f'{connection.local_port}@{connection.host}', - ) - NV_CONNECTIONS.add_connection( - left=connection.neighbour, - right=f'{connection.neighbour_port}@{connection.neighbour}', - ) - NV_CONNECTIONS.add_connection( - left=f'{connection.local_port}@{connection.host}', - right=f'{connection.neighbour_port}@{connection.neighbour}', - ) - - -def create_l2_topology( - seed_devices: Sequence[str], - path_in_inventory: str, - inv_columns: InventoryColumns, - label: str, -) -> None: - devices_to_go = list(set(seed_devices)) # remove duplicates - devices_done = [] - - while devices_to_go: - device = devices_to_go[0] - - if device in L2_HOST_MAP.keys(): - try: - devices_to_go.remove(device) - except ValueError: - pass - device = L2_HOST_MAP[device] - if device in devices_done: - continue - - topo_data = HOST_CACHE.get_data( - host=device, item=CacheItems.inventory, path=path_in_inventory - ) - if topo_data: - create_l2_device_from_inv( - host=device, - inv_data=topo_data, - inv_columns=inv_columns, - # label=label, - ) - - for _entry in NV_OBJECTS.host_list: - if _entry not in devices_done: - devices_to_go.append(_entry) - - devices_to_go = list(set(devices_to_go)) - devices_done.append(device) - devices_to_go.remove(device) - LOGGER.info(msg=f'Device done: {device}, source: {label}') - - -def create_l3v4_topology( - ignore_hosts: Sequence[str], - ignore_ips: Sequence[IPv4Network], - ignore_wildcard: Sequence[Wildcard], - summarize: Sequence[IPv4Network], - replace: Mapping[str, str], - skip_if: bool, - skip_ip: bool, -) -> None: - host_list: Sequence[str] = HOST_CACHE.get_hosts_by_label(HOST_LABEL_L3V4) - LOGGER.debug(f'host list: {host_list}') - if not host_list: - LOGGER.warning( - msg='No routing capable host found. Check if "inv_ipv4_addresses.mkp" ' - 'added/enabled and inventory has run.' - ) - return - - LOGGER.debug(f'L3v4 ignore hosts: {ignore_hosts}') - for raw_host in host_list: - host = raw_host - if host in ignore_hosts: - LOGGER.info(f'L3v4 host {host} ignored') - continue - if not (ipv4_addresses := HOST_CACHE.get_data( - host=host, item=CacheItems.inventory, path=PATH_L3v4) - ): - LOGGER.warning(f'No IPv4 address inventory found for host: {host}') - continue - - NV_OBJECTS.add_host_object(host=host, host_cache=HOST_CACHE) - for _entry in ipv4_addresses: - emblem = EMBLEMS.ip_network - try: - ipv4_info = Ipv4Info(**_entry) - except TypeError: # as e - LOGGER.warning(f'Drop IPv4 address data for host: {host}, data: {_entry}') - continue - - if ipv4_info.address.startswith('127.'): # drop loopback addresses - LOGGER.info(f'host: {host} dropped loopback address: {ipv4_info.address}') - continue - - if ipv4_info.cidr == 32: # drop host addresses - LOGGER.info( - f'host: {host} dropped host address: {ipv4_info.address}/{ipv4_info.cidr}' - ) - continue - - if ipv4_info.type.lower() != 'ipv4': # drop if not ipv4 - LOGGER.warning( - f'host: {host} dropped non ipv4 address: {ipv4_info.address},' - ' type: {ipv4_info.type}' - ) - continue - - if is_ignore_ipv4(ipv4_info.address, ignore_ips): - LOGGER.info(f'host: {host} dropped ignore address: {ipv4_info.address}') - continue - - if is_ignore_wildcard(ipv4_info.address, ignore_wildcard): - LOGGER.info(f'host: {host} dropped wildcard address: {ipv4_info.address}') - continue - - if network := get_network_summary( - ipv4_address=ipv4_info.address, - summarize=summarize, - ): - emblem = EMBLEMS.l3v4_summarize - LOGGER.info( - f'Network summarized: {ipv4_info.network}/{ipv4_info.cidr} -> {network}' - ) - else: - network = f'{ipv4_info.network}/{ipv4_info.cidr}' - - if network in replace.keys(): - LOGGER.info(f'Replaced network {network} with {replace[network]}') - network = replace[network] - emblem = EMBLEMS.l3v4_replace - - NV_OBJECTS.add_ipv4_network_object(network=network, emblem=emblem) - - if skip_if is True and skip_ip is True: - NV_CONNECTIONS.add_connection(left=host, right=network) - elif skip_if is True and skip_ip is False: - NV_OBJECTS.add_ipv4_address_object( - host=host, - interface=None, - ipv4_address=ipv4_info.address, - emblem=EMBLEMS.ip_address, - ) - NV_OBJECTS.add_tooltip_quickinfo( - '{ipv4_info.address}@{host}', 'Interface', ipv4_info.device - ) - NV_CONNECTIONS.add_connection(left=f'{host}', right=f'{ipv4_info.address}@{host}') - NV_CONNECTIONS.add_connection(left=network, right=f'{ipv4_info.address}@{host}') - elif skip_if is False and skip_ip is True: - NV_OBJECTS.add_service_object( - host=host, service=ipv4_info.device, host_cache=HOST_CACHE - ) - NV_OBJECTS.add_tooltip_quickinfo( - f'{ipv4_info.device}@{host}', 'IP-address', ipv4_info.address - ) - NV_CONNECTIONS.add_connection(left=f'{host}', right=f'{ipv4_info.device}@{host}') - NV_CONNECTIONS.add_connection(left=network, right=f'{ipv4_info.device}@{host}') - else: - NV_OBJECTS.add_ipv4_address_object( - host=host, - interface=ipv4_info.device, - ipv4_address=ipv4_info.address, - emblem=EMBLEMS.ip_address, - ) - NV_OBJECTS.add_service_object( - host=host, service=ipv4_info.device, host_cache=HOST_CACHE, - ) - NV_CONNECTIONS.add_connection( - left=host, right=f'{ipv4_info.device}@{host}') - NV_CONNECTIONS.add_connection( - left=f'{ipv4_info.device}@{host}', - right=f'{ipv4_info.address}@{ipv4_info.device}@{host}', - ) - NV_CONNECTIONS.add_connection( - left=network, right=f'{ipv4_info.address}@{ipv4_info.device}@{host}', - ) - - -if __name__ == '__main__': +def main(): start_time = time_ns() - SETTINGS = Settings(vars(parse_arguments())) - sys.stdout = StdoutQuiet(quiet=SETTINGS.quiet) + nv_connections = NvConnections() + nv_objects = NvObjects() + + settings: Settings = Settings(vars(parse_arguments())) + sys.stdout = StdoutQuiet(quiet=settings.quiet) configure_logger( - log_file=SETTINGS.log_file, - log_to_console=SETTINGS.log_to_stdtout, - log_level=SETTINGS.loglevel, + log_file=settings.log_file, + log_to_console=settings.log_to_stdtout, + log_level=settings.loglevel, ) - LOGGER.info(msg='Data creation started') + # always logg start and end of a session (except --log-level OFF) + LOGGER.critical(msg='Data creation started') - print() + print('') print( f'Network Visualisation Data Creation Tool (NVDCT)\n' f'by thl-cmk[at]outlook[dot]com, version {NVDCT_VERSION}\n' f'see {HOME_URL}' ) - print() - print(f'Start time....: {strftime(SETTINGS.time_format)}') + print('') + print(f'Start time....: {strftime(settings.time_format)}') - match SETTINGS.backend: + match settings.backend: case 'RESTAPI': - HOST_CACHE = HostCacheRestApi( - pre_fetch=SETTINGS.pre_fetch, - api_port=SETTINGS.api_port + host_cache: HostCache = HostCacheRestApi( + pre_fetch=settings.pre_fetch, + api_port=settings.api_port, + filter_sites=settings.filter_sites, + sites=settings.sites ) case 'MULTISITE': - HOST_CACHE = HostCacheMultiSite( - pre_fetch=SETTINGS.pre_fetch, - filter_sites=SETTINGS.filter_sites, - sites=SETTINGS.sites, + host_cache: HostCache = HostCacheMultiSite( + pre_fetch=settings.pre_fetch, + filter_sites=settings.filter_sites, + sites=settings.sites, + filter_customers=settings.filter_customers, + customers=settings.customers, ) case 'LIVESTATUS': - HOST_CACHE = HostCacheLiveStatus( - pre_fetch=SETTINGS.pre_fetch, + host_cache: HostCache = HostCacheLiveStatus( + pre_fetch=settings.pre_fetch, ) case _: - LOGGER.error(msg=f'Backend {SETTINGS.backend} not (yet) implemented') + LOGGER.error(msg=f'Backend {settings.backend} not (yet) implemented') + host_cache: HostCache | None = None # to keep linter happy sys.exit(ExitCodes.BACKEND_NOT_IMPLEMENTED.value) - EMBLEMS = SETTINGS.emblems - L2_DROP_HOSTS = SETTINGS.l2_drop_hosts - L2_HOST_MAP = SETTINGS.l2_host_map - L2_NEIGHBOUR_REPLACE_REGEX = SETTINGS.l2_neighbour_replace_regex - MAP_SPEED_TO_THICKNESS = SETTINGS.map_speed_to_thickness - jobs: List[Layer] = [] - final_topology: Dict = {} pre_fetch_layers: List[str] = [] pre_fetch_host_list: List[str] = [] - for layer in SETTINGS.layers: + for layer in settings.layers: if layer == 'STATIC': jobs.append(layer) if layer == 'L3v4': jobs.append(layer) - HOST_CACHE.add_inventory_prefetch_path(path=LAYERS[layer].path) + host_cache.add_inventory_prefetch_path(path=LAYERS[layer].path) pre_fetch_layers.append(LAYERS[layer].host_label) elif layer in LAYERS: jobs.append(LAYERS[layer]) - HOST_CACHE.add_inventory_prefetch_path(path=LAYERS[layer].path) + host_cache.add_inventory_prefetch_path(path=LAYERS[layer].path) pre_fetch_layers.append(LAYERS[layer].host_label) elif layer == 'CUSTOM': - for entry in SETTINGS.custom_layers: + for entry in settings.custom_layers: jobs.append(entry) - HOST_CACHE.add_inventory_prefetch_path(entry.path) + host_cache.add_inventory_prefetch_path(entry.path) + + if not jobs: + message = ('No layer to work on. Please configura at least one layer (i.e. CLI option "-l CDP")\n' + 'See ~/local/bin/nvdct/conf/nvdct.toml -> SETTINGS -> layers') + LOGGER.warning(message) + print(message) + sys.exit(ExitCodes.NO_LAYER_CONFIGURED.value) - if SETTINGS.pre_fetch: + if settings.pre_fetch: LOGGER.info('Pre fill cache...') for host_label in pre_fetch_layers: - if _host_list := HOST_CACHE.get_hosts_by_label(host_label): + if _host_list := host_cache.get_hosts_by_label(host_label): pre_fetch_host_list = list(set(pre_fetch_host_list + _host_list)) LOGGER.info(f'Fetching data for {len(pre_fetch_host_list)} hosts start') - print(f'Prefetch start: {strftime(SETTINGS.time_format)}') - print(f'Prefetch hosts: {len(pre_fetch_host_list)} of {len(HOST_CACHE.cache.keys())}') - HOST_CACHE.pre_fetch_cache(pre_fetch_host_list) + print(f'Prefetch start: {strftime(settings.time_format)}') + print(f'Prefetch hosts: {len(pre_fetch_host_list)} of {len(host_cache.cache.keys())}') + host_cache.pre_fetch_cache(pre_fetch_host_list) LOGGER.info(f'Fetching data for {len(pre_fetch_host_list)} hosts end') - print(f'Prefetch end..: {strftime(SETTINGS.time_format)}') + print(f'Prefetch end..: {strftime(settings.time_format)}') for job in jobs: match job: case 'STATIC': - label = 'static' + label = 'STATIC' create_static_connections( - connections=SETTINGS.static_connections + connections_=settings.static_connections, + emblems=settings.emblems, + host_cache=host_cache, + nv_objects=nv_objects, + nv_connections=nv_connections, ) case 'L3v4': - topology = None - label = 'l3v4' + label = 'L3v4' create_l3v4_topology( - ignore_hosts=SETTINGS.l3v4_ignore_hosts, - ignore_ips=SETTINGS.l3v4_ignore_ips, - ignore_wildcard=SETTINGS.l3v4_ignore_wildcard, - summarize=SETTINGS.l3v4_summarize, - replace=SETTINGS.l3v4_replace, - skip_if=SETTINGS.skip_l3_if, - skip_ip=SETTINGS.skip_l3_ip + ignore_hosts=settings.l3v4_ignore_hosts, + ignore_ips=settings.l3v4_ignore_ips, + ignore_wildcard=settings.l3v4_ignore_wildcard, + include_hosts=settings.include_l3_hosts, + replace=settings.l3v4_replace, + skip_if=settings.skip_l3_if, + skip_ip=settings.skip_l3_ip, + summarize=settings.l3v4_summarize, + emblems=settings.emblems, + host_cache=host_cache, + nv_objects=nv_objects, + nv_connections=nv_connections, ) case _: - label = job.label.lower() + label = job.label.upper() columns = job.columns.split(',') create_l2_topology( - seed_devices=SETTINGS.l2_seed_devices, + seed_devices=settings.l2_seed_devices, path_in_inventory=job.path, inv_columns=InventoryColumns( neighbour=columns[0], local_port=columns[1], neighbour_port=columns[2] ), - label=label, + label_=label, + l2_drop_hosts=settings.l2_drop_hosts, + l2_host_map=settings.l2_host_map, + l2_neighbour_replace_regex=settings.l2_neighbour_replace_regex, + host_cache=host_cache, + nv_objects=nv_objects, + nv_connections=nv_connections, + case=settings.case, + prefix=settings.prefix, + remove_domain=settings.remove_domain, ) - NV_CONNECTIONS.add_meta_data_to_connections( - nv_objects=NV_OBJECTS, - speed_map=MAP_SPEED_TO_THICKNESS, + nv_connections.add_meta_data_to_connections( + nv_objects=nv_objects, + speed_map=settings.map_speed_to_thickness, ) - if not SETTINGS.loglevel == DEBUG: - connections = NV_CONNECTIONS.nv_connections - else: - connections = sorted(NV_CONNECTIONS.nv_connections) + _data = { 'version': 1, 'name': label, - 'objects': NV_OBJECTS.nv_objects if not SETTINGS.loglevel == DEBUG else dict( - sorted(NV_OBJECTS.nv_objects.items()) + 'objects': nv_objects.nv_objects if not settings.loglevel == DEBUG else dict( + sorted(nv_objects.nv_objects.items()) ), - 'connections': connections + 'connections': nv_connections.nv_connections if not settings.loglevel == DEBUG else sorted( + nv_connections.nv_connections + ) } save_data_to_file( data=_data, path=( - f'{SETTINGS.omd_root}/{SETTINGS.topology_save_path}/' - f'{SETTINGS.output_directory}' + f'{DATAPATH}/{settings.output_directory}' ), file=f'data_{label}.json', - make_default=SETTINGS.default, + make_default=settings.default, ) message = ( - f'Source {label:.<7s}: Devices/Objects/Connections added {NV_OBJECTS.host_count}/' - f'{len(NV_OBJECTS.nv_objects)}/{len(NV_CONNECTIONS.nv_connections)}' + f'Layer {label:.<8s}: Devices/Objects/Connections added {nv_objects.host_count}/' + f'{len(nv_objects.nv_objects)}/{len(nv_connections.nv_connections)}' ) LOGGER.info(msg=message) print(message) - NV_OBJECTS = NvObjects() - NV_CONNECTIONS = NvConnections() + nv_objects = NvObjects() + nv_connections = NvConnections() - if SETTINGS.keep: + if settings.keep: remove_old_data( - keep=SETTINGS.keep, - min_age=SETTINGS.min_age, - raw_path=f'{SETTINGS.omd_root}/{SETTINGS.topology_save_path}', - protected=SETTINGS.protected_topologies, + keep=settings.keep, + min_age=settings.min_age, + raw_path=DATAPATH, + protected=settings.protected_topologies, ) print(f'Time taken....: {(time_ns() - start_time) / 1e9}/s') - print(f'End time......: {strftime(SETTINGS.time_format)}') - print() + print(f'End time......: {strftime(settings.time_format)}') + print('') + + LOGGER.critical('Data creation finished') - LOGGER.info('Data creation finished') + +if __name__ == '__main__': + main() diff --git a/source/packages/nvdct b/source/packages/nvdct index 73ff4a11a7b6a314b0e31712667391fb1921d100..d2a4e8fa6c18365c06bec03ee77a0007c1e043aa 100644 --- a/source/packages/nvdct +++ b/source/packages/nvdct @@ -17,8 +17,7 @@ 'environments\n' ' - Add custom connections (STATIC) for connections that are ' 'not in the inventory\n' - ' - Optimized for my CDP, LLDP and IPv4 inventory plugins\n' - ' - Can also be used with custom inventory plugins\n' + ' - Optimized for my CDP, LLDP and IP inventory plugins\n' '\n' 'For more information about the network visualization plugin ' 'see: \n' @@ -28,14 +27,7 @@ '\n' 'The inventory data could be created with my inventory ' 'plugins:\n' - 'CDP: ' - 'https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_cdp_cache\n' - 'LLDP: ' - 'https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_lldp_cache\n' - 'L3v4: ' - 'https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_ipv4_addresses\n' - 'IF_name: ' - 'https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_ifname\n' + 'https://thl-cmk.hopto.org/gitlab/explore/projects/topics/Network%20Visualization\n' '\n' 'For the latest version and documentation see:\n' 'https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/nvdct\n', @@ -47,14 +39,15 @@ 'nvdct/lib/utils.py', 'nvdct/lib/__init__.py', 'nvdct/conf/nvdct.toml', - 'nvdct/lib/topologies.py'], + 'nvdct/lib/topologies.py', + 'nvdct/lib/constants.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.3-20241209', + 'version': '0.9.4-20241210', 'version.min_required': '2.3.0b1', 'version.packaged': 'cmk-mkp-tool 0.2.0', 'version.usable_until': '2.4.0p1'}