From 6e851d9fc8667461253e75b5d77bde163f721142 Mon Sep 17 00:00:00 2001
From: "th.l" <thl-cmk@outlook.com>
Date: Wed, 25 Dec 2024 11:57:40 +0100
Subject: [PATCH] streamlined L3v4 in preparation for L3v6 topology

---
 README.md                          |   2 +-
 mkp/nvdct-0.9.6-20241222.mkp       | Bin 0 -> 44241 bytes
 source/bin/nvdct/conf/nvdct.toml   |  30 +-
 source/bin/nvdct/lib/args.py       |  36 +-
 source/bin/nvdct/lib/backends.py   | 665 ++++++++++----------
 source/bin/nvdct/lib/constants.py  |  89 ++-
 source/bin/nvdct/lib/settings.py   | 174 +++---
 source/bin/nvdct/lib/topologies.py | 944 ++++++++++++++++-------------
 source/bin/nvdct/lib/utils.py      |  60 +-
 source/bin/nvdct/nvdct.py          | 182 +++---
 source/packages/nvdct              |   2 +-
 11 files changed, 1170 insertions(+), 1014 deletions(-)
 create mode 100644 mkp/nvdct-0.9.6-20241222.mkp

diff --git a/README.md b/README.md
index 075d246..f4a9179 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[PACKAGE]: ../../raw/master/mkp/nvdct-0.9.5-20241217.mkp "nvdct-0.9.5-20241217.mkp"
+[PACKAGE]: ../../raw/master/mkp/nvdct-0.9.6-20241222.mkp "nvdct-0.9.6-20241222.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.6-20241222.mkp b/mkp/nvdct-0.9.6-20241222.mkp
new file mode 100644
index 0000000000000000000000000000000000000000..a245e33f052657a47f08c939ec5ed9bfe5ec7575
GIT binary patch
literal 44241
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

literal 0
HcmV?d00001

diff --git a/source/bin/nvdct/conf/nvdct.toml b/source/bin/nvdct/conf/nvdct.toml
index 414861a..e907197 100755
--- a/source/bin/nvdct/conf/nvdct.toml
+++ b/source/bin/nvdct/conf/nvdct.toml
@@ -26,25 +26,27 @@ L2_DROP_HOSTS = [
     #  "a nother invalid name",
 ]
 
-# hosts will be ignored in L3v4 topology
+# hosts will be ignored in L3 topologies
 # [0-9-a-zA-Z\.\_\-]{1,253} -> host
-L3V4_IGNORE_HOSTS = [
+L3_IGNORE_HOSTS = [
     # "host1",
     # "host2",
 ]
 
-# drop IP address that match network
-L3V4_IGNORE_IP = [
+# drop IP address that matches ip/network
+L3_IGNORE_IP = [
   # "192.168.100.231",
   # "192.168.100.0/16",
   # "192.168.150.0/255.255.255.0",
+  # "fd00::1"
+  # "fd00::/8"
 ]
 
 # ignore IPs by wildcard
 # if comparing an ip address:
 # each 0 bit in the wildcad has to be exacly as in the pattern
 # each 1 bit in the wildacrd will be ignored
-L3V4_IRNORE_WILDCARD = [
+L3V4_IGNORE_WILDCARD = [
     # [ pattern    ,  wildcard ]
     # ["172.17.0.1", "0.0.255.0"],  # ignore all IPs ending with 1 from 172.17.128.0/16
     # ["172.17.128.0", "0.0.127.3"],  # ignore all IPs ending with 0-3 from 172.17.128.0/17
@@ -52,11 +54,12 @@ L3V4_IRNORE_WILDCARD = [
 ]
 
 # networks to summarize
-L3V4_SUMMARIZE = [
+L3_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"
 ]
 
 # topologies will not be deleted by "--keep"
@@ -120,11 +123,12 @@ SITES = [
 
 # replace network objects (takes place after summarize)
 # [0-9-a-zA-Z\.\_\-]{1,253} -> host
-[L3V4_REPLACE]
+[L3_REPLACE]
 # "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]
 # can use misc icons from CMK or upload your own in the misc category
@@ -133,8 +137,8 @@ SITES = [
 # "host_node" = "icon_missinc"
 # "ip_address" = "ip-address_80"
 # "ip_network" = "ip-network_80"
-# "l3v4_replace" = "icon_plugins_cloud"
-# "l3v4_summarize" = "icon_aggr"
+# "l3_replace" = "icon_plugins_cloud"
+# "l3_summarize" = "icon_aggr"
 # "service_node" = "icon_missing"
 
 [MAP_SPEED_TO_THICKNESS]
@@ -158,13 +162,13 @@ SITES = [
 # filter_customers = "INCLUDE" |"EXCLUDE"
 # filter_sites = "INCLUDE" | "EXCLUDE"
 # include_l3_hosts = false
-# keep = 0
-# layers = ["LLDP", "CDP", L3v4, "STATIC", "CUSTOM"]
+keep = 10
+# layers = ["LLDP", "CDP", "L3v4", "STATIC", "CUSTOM"]
 # log_file = "~/var/log/nvdct.log"
 # log_level = "WARNING"
 # log_to_stdout = false
-# min_age = 0
-# output_directory = ''  #
+min_age = 1
+output_directory = 'nvdct'  # remove to get date formated directory
 # pre_fetch = false
 # prefix = ""
 # quiet = true
diff --git a/source/bin/nvdct/lib/args.py b/source/bin/nvdct/lib/args.py
index cf77c81..5ad06bd 100755
--- a/source/bin/nvdct/lib/args.py
+++ b/source/bin/nvdct/lib/args.py
@@ -45,18 +45,18 @@ from argparse import (
 from pathlib import Path
 
 from lib.constants import (
+    ExitCodes,
     HOME_URL,
     MIN_CDP_VERSION,
     MIN_LINUX_IP_ADDRESSES,
+    MIN_LLDP_VERSION,
     MIN_SNMP_IP_ADDRESSES,
     MIN_WINDOWS_IP_ADDRESSES,
-    MIN_LLDP_VERSION,
     NVDCT_VERSION,
     SCRIPT,
     TIME_FORMAT_ARGPARSER,
     USER_DATA_FILE,
 )
-from lib.utils import ExitCodes
 
 
 def parse_arguments() -> arg_Namespace:
@@ -80,10 +80,12 @@ def parse_arguments() -> arg_Namespace:
         ),
         formatter_class=RawTextHelpFormatter,
         epilog='Exit codes:\n'
-               f' {ExitCodes.OK.value} - No error\n'
-               f' {ExitCodes.BAD_OPTION_LIST.value} - Bad options list\n'
-               f' {ExitCodes.BACKEND_NOT_IMPLEMENTED.value} - Backend not implemented\n'
-               f' {ExitCodes.AUTOMATION_SECRET_NOT_FOUND.value} - Automation secret not found\n'
+               f' {ExitCodes.OK} - No error\n'
+               f' {ExitCodes.BAD_OPTION_LIST} - Bad options list\n'
+               f' {ExitCodes.BAD_TOML_FORMAT} - Bad TOML file format\n'
+               f' {ExitCodes.BACKEND_NOT_IMPLEMENTED} - Backend not implemented\n'
+               f' {ExitCodes.AUTOMATION_SECRET_NOT_FOUND} - Automation secret not found\n'
+               f' {ExitCodes.NO_LAYER_CONFIGURED} - No layer to work on\n'
                '\nUsage:\n'
                f'{SCRIPT} -u ~/local/bin/nvdct/conf/my_{USER_DATA_FILE} \n\n'
     )
@@ -121,16 +123,22 @@ def parse_arguments() -> arg_Namespace:
     parser.add_argument(
         '-l', '--layers',
         nargs='+',
-        choices=['CDP', 'CUSTOM', 'LLDP', 'STATIC', 'L3v4'],
+        choices=[
+            'CDP',
+            'CUSTOM',
+            'L3v4',
+            'LLDP',
+            'STATIC',
+        ],
         # 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_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'
+            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(
diff --git a/source/bin/nvdct/lib/backends.py b/source/bin/nvdct/lib/backends.py
index 64cce3a..78a7d4b 100755
--- a/source/bin/nvdct/lib/backends.py
+++ b/source/bin/nvdct/lib/backends.py
@@ -10,32 +10,33 @@
 
 # 2024-06-18: fixed host_exist returns always True if host was in host_cache, even with host=None
 # 2024-09-25: fixed crash on missing "customer" section in site config file
+# 2024-12-22: refactoring, leave only backend specific stuff in the backend
+#             removed not strictly needed properties, renamed functions to better understand what the do
 
-from collections.abc import Mapping, Sequence
 from abc import abstractmethod
 from ast import literal_eval
+from collections.abc import Mapping, MutableSequence, Sequence
 from enum import Enum, unique
 from pathlib import Path
 from requests import session
-from time import time_ns
-from typing import Dict, List, Tuple
 from sys import exit as sys_exit
+from typing import Dict, List, Tuple
 
 from livestatus import MultiSiteConnection, SiteConfigurations, SiteId
 
 from lib.constants import (
     CACHE_INTERFACES_DATA,
+    ExitCodes,
     OMD_ROOT,
     PATH_INTERFACES,
 )
 from lib.utils import (
-    ExitCodes,
+    LOGGER,
     get_data_form_live_status,
     get_table_from_inventory,
-    LOGGER,
-
 )
 
+HOST_EXIST: Dict = {'exists': True}
 
 def hosts_to_query(hosts: List[str]) -> Tuple[str, List[str]]:
     # WORKAROUND for: Apache HTTP Error 414: Request URI too long
@@ -89,6 +90,8 @@ class CacheItems(Enum):
     inventory = 'inventory'
     interfaces = 'interfaces'
 
+    def __get__(self, instance, owner):
+        return self.value
 
 class HostCache:
     def __init__(
@@ -96,247 +99,287 @@ class HostCache:
             pre_fetch: bool,
             backend: str,
     ):
-        self._cache: Dict = {}
+        LOGGER.info('init HOST_CACHE')
+
+        self.cache: Dict = {}
         self._inventory_pre_fetch_list: List[str] = [
             PATH_INTERFACES,
         ]
-        self._count: int = 0
-        self._pre_fetch = pre_fetch
-        self._backend = backend
-        LOGGER.info('init HOST_CACHE')
 
-    @abstractmethod
-    def get_inventory_data(self, hosts: Sequence[str]) -> Dict[str, Dict | None]:
+        self.pre_fetch: bool = bool(pre_fetch)
+        self.backend: str = str(backend)
+
+        if self.pre_fetch:
+            for host in self.query_all_hosts():
+                self.cache[host] = HOST_EXIST.copy()
+
+    def get_inventory_data(self, hosts: Sequence[str]) -> Dict[str, Dict]:
         """
+        Returns a dictionary of hosts and there inventory data.
         Args:
-            hosts: the host name to return the inventory data for
+            hosts: list of host names to return the inventory data for
 
         Returns:
             the inventory data as dictionary
         """
-        raise NotImplementedError()
 
-    @abstractmethod
+        inventory_data: Dict[str, Dict | None] = {}
+        # init inventory_data with None
+        for host in hosts:
+            inventory_data[host] = None
+
+        open_hosts = hosts.copy()
+        while open_hosts:
+            hosts_str, open_hosts = hosts_to_query(open_hosts)
+            for host, inventory in self.query_inventory_data(hosts_str).items():
+                inventory_data[host] = inventory
+
+        return inventory_data
+
     def get_interface_data(self, hosts: Sequence[str]) -> Dict[str, Dict | None]:
         """
-
+        Returns Dictionary of hosts and there interface services from CMK.
+        The interface information consists of the "Item", the "Description (summary)" and the service details
         Args:
             hosts:  lit of host names to return the interface data for
 
         Returns:
-            dictionary of the interface data with the item as key
+            dictionary of the interface data with the host -> item as key
         """
-        raise NotImplementedError()
+        host_data: Dict[str, any] = {}  # to make pylint happy
+        # init host_data with None
+        for host in hosts:
+            host_data[host] = None
+        open_hosts = hosts.copy()
+        while open_hosts:
+            hosts_str, open_hosts = hosts_to_query(open_hosts)
+            host_data.update(self.query_interface_data(hosts_str))
 
-    @abstractmethod
-    def host_exists(self, host: str) -> bool:
-        raise NotImplementedError()
+        return host_data
 
-    @abstractmethod
-    def get_hosts_by_label(self, label: str) -> List[str] | None:
-        raise NotImplementedError()
+    def host_exists(self, host: str) -> bool:
+        """
+        Returns True if host exists in CMK, else False
+        """
+        try:
+            return bool(self.cache[host])
+        except KeyError:
+            pass
 
-    @abstractmethod
-    def pre_fetch_hosts(self):
-        raise NotImplementedError()
+        # get host from CMK and init host in cache
+        if exists := self.query_host(host):
+            self.cache[host] = HOST_EXIST.copy()
+        else:
+            self.cache[host] = None
 
-    @property
-    def cache(self) -> Dict:
-        return self._cache
+        return exists
 
-    @property
-    def backend(self) -> str:
-        return self._backend
+    def get_hosts_by_label(self, label: str) -> Sequence[str]:
+        """
+        Returns list of hosts from CMK filtered by label
+        Args:
+            label: hostlabel to filter by
 
-    @property
-    def pre_fetch(self) -> bool:
-        return self._pre_fetch
+        Returns:
+            List of hosts
+        """
+        return self.query_hosts_by_label(label)
 
-    def stop_host(self, host: str) -> None:
-        if host not in self._cache:
-            self._cache[host] = None
+    def fill_cache(self, hosts: Sequence[str]) -> None:
+        """
+        Gets the host data from CMK and puts them in the host cache. Data collected:
+        - inventory
+        - interfaces
 
-    def pre_fetch_cache(self, hosts: Sequence[str]) -> None:
-        # pre fill inventory data
-        self._count += 1
-        _pre_query = time_ns()
+        Args:
+            hosts: List of hosts the get data from CMK
 
+        Returns: None, the data is directly writen to self.cache
+        """
         inventory_of_hosts: Mapping[str, Mapping | None] = self.get_inventory_data(hosts=hosts)
-        LOGGER.debug(f'{(time_ns() - _pre_query) / 1e9}|{self._count:0>4}|inventory|{hosts}')
-
         if inventory_of_hosts:
             for host, inventory in inventory_of_hosts.items():
-                if host not in self._cache:
-                    self._cache[host] = {}
-                self._cache[host][CacheItems.inventory.value] = {}
-                self._cache[host][CacheItems.inventory.value].update({
+                if host not in self.cache:
+                    self.cache[host] = HOST_EXIST.copy()
+                self.cache[host][CacheItems.inventory] = {}
+                self.cache[host][CacheItems.inventory].update({
                     entry: get_table_from_inventory(
                         inventory=inventory,
                         raw_path=entry
                     ) for entry in self._inventory_pre_fetch_list
                 })
 
-        _pre_query = time_ns()
-
         interfaces_of_hosts: Mapping[str, Mapping | None] = self.get_interface_data(hosts)
 
         for host, interfaces in interfaces_of_hosts.items():
-            if host not in self._cache:
-                self._cache[host] = {}
-            if not self._cache[host].get(CacheItems.interfaces.value):
-                self._cache[host][CacheItems.interfaces.value] = {}
-            self._cache[host][CacheItems.interfaces.value][CACHE_INTERFACES_DATA] = interfaces
-
-            LOGGER.debug(f'{(time_ns() - _pre_query) / 1e9}|{self._count:0>4}|items|{host}')
+            if host not in self.cache:
+                self.cache[host] = HOST_EXIST.copy()
+            if not self.cache[host].get(CacheItems.interfaces):
+                self.cache[host][CacheItems.interfaces] = {}
+            self.cache[host][CacheItems.interfaces][CACHE_INTERFACES_DATA] = interfaces
 
     def get_data(self, host: str, item: CacheItems, path: str) -> Dict[str, any] | None:
+        """
+        Returns data from self.cache. If the cache for "host" is empty, data will be fetched from CMK
+        Args:
+            host: host to get data from cache
+            item: item in cache (inventory/interface)
+            path: path in cache item
+
+        Returns:
+            the requested data or None
+        """
         if self.host_exists(host=host):
-            if host not in self._cache:
-                self._cache[host] = {}
-                LOGGER.info(f'Host not in cache: {host}')
-                self.pre_fetch_cache(hosts=[host])
+            if self.cache[host] == HOST_EXIST:
+                LOGGER.info(f'fetch data for: {host}')
+                self.fill_cache(hosts=[host])
             try:
-                return self._cache[host][item.value][path]
+                return self.cache[host][item][path]
             except (KeyError, TypeError):
                 return None
-        else:
-            self._cache[host] = None
+
         return None
 
-    def add_inventory_prefetch_path(self, path: str) -> None:
+    def add_inventory_path(self, path: str) -> None:
         self._inventory_pre_fetch_list = list(set(self._inventory_pre_fetch_list + [path]))
 
+    @abstractmethod
+    def query_host(self, host: str) -> bool:
+        """
+        Query Livestatus for "host"
+
+        Args:
+             host: CMK host name to query livestus for
+        Returns:
+            True: if host was found
+            False: if host is not found
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def query_all_hosts(self) -> Sequence[str]:
+        """
+        Queries Livestatus for a list of all hosts
+        Returns:
+            List of all hosts
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def query_hosts_by_label(self, label: str) -> Sequence[str]:
+        """
+        Queries Livestatus for a list of hosts filtered by a host label
+        Args:
+            label: Host label to filter list of host by
+
+        Returns: List of hosts
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def query_inventory_data(self, hosts: str) -> Dict[str, Dict]:
+        raise NotImplementedError
+
+    @abstractmethod
+    def query_interface_data(self, hosts: str) -> Dict[str, Dict]:
+        raise NotImplementedError
 
 class HostCacheLiveStatus(HostCache):
     def __init__(self, pre_fetch: bool, backend: str = '[LIVESTATUS]'):
         super().__init__(pre_fetch, backend)
-        if self.pre_fetch:
-            self.pre_fetch_hosts()
 
     def get_raw_data(self, query: str) -> any:
         return get_data_form_live_status(query=query)
 
-    def get_inventory_data(self, hosts: List[str]) -> Dict[str, Dict | None]:
-        host_data: Dict[str, Dict | None] = {}
-        # int host_data with None
-        for host in hosts:
-            host_data[host] = None
-        open_hosts = hosts.copy()
-        while open_hosts:
-            hosts_str, open_hosts = hosts_to_query(open_hosts)
-            query = (
-                'GET hosts\n'
-                'Columns: host_name mk_inventory\n'
-                'OutputFormat: python3\n'
-                f'Filter: host_name ~~ {hosts_str}\n'
-            )
-            data: Sequence[Tuple[str, bytes]] = self.get_raw_data(query=query)
-            LOGGER.debug(f'{self.backend} data for hosts {hosts}: {data}')
-            if data:
-                for host, inventory in data:
-                    if inventory == b'':
-                        LOGGER.warning(f'{self.backend} Device: {hosts}: no inventory data found!')
-                        continue
-                    try:
-                        host_data[host] = literal_eval(inventory.decode('utf-8'))
-                    except SyntaxError as e:
-                        LOGGER.exception(f'inventory: |{inventory!r}|')  # output raw data
-                        LOGGER.exception(f'type: {type(inventory)}')
-                        LOGGER.exception(f'exception: {e}')
-                        continue
-            else:
-                LOGGER.warning(f'{self.backend} Device: {hosts}: no inventory data found!')
-        return host_data
-
-    def get_interface_data(self, hosts: List[str]) -> Dict[str, Dict | None]:
-        # host_data: Dict[str, Dict[str, Dict[str, List[str]] ] | None] = {}
-        host_data: Dict[str, any] = {}  # to make pylint happy
-        # int host_data with None
-        for host in hosts:
-            host_data[host] = None
-        open_hosts = hosts.copy()
-        while open_hosts:
-            hosts_str, open_hosts = hosts_to_query(open_hosts)
-            query = (
-                'GET services\n'
-                'Columns: host_name description long_plugin_output\n'
-                'Filter: description ~ ^Interface\n'
-                f'Filter: host_name ~~ {hosts_str}\n'
-                'OutputFormat: python3\n'
-            )
-            data: List[Tuple[str, str, str]] = self.get_raw_data(query=query)
-            LOGGER.debug(f'{self.backend} data for host {hosts}: {data}')
-            if data:
-                for host, description, long_plugin_output in data:
-                    if host_data.get(host) is None:
-                        host_data[host] = {}
-                    host_data[host][description[10:]] = {  # remove 'Interface ' from description
-                        'long_plugin_output': long_plugin_output.split('\\n')
-                    }
-            else:
-                LOGGER.warning(f'{self.backend} No Interfaces items found for hosts {hosts}')
-
-        return host_data
-
-    def host_exists(self, host: str) -> bool:
+    def query_host(self, host: str) -> bool:
         query = (
             'GET hosts\n'
             'Columns: host_name\n'
             'OutputFormat: python3\n'
             f'Filter: host_name = {host}\n'
         )
-        if self.cache.get(host) is not None:
-            return True
-        elif host in self.cache:
-            return False
-        # if self.pre_fetch:
-        #     LOGGER.warning(f'{self.backend} pre_fetch host not found in cache {host}')
-        #     return False
         data: Sequence[Sequence[str]] = self.get_raw_data(query=query)
         LOGGER.debug(f'{self.backend} data for host {host}: {data}')
         if [host] in data:
             LOGGER.debug(f'{self.backend} Host {host} found in CMK')
             return True
-
         LOGGER.warning(f'{self.backend} Host {host} not found in CMK')
-        self.stop_host(host)
-
         return False
 
-    def get_hosts_by_label(self, label: str) -> List[str] | None:
+    def query_all_hosts(self) -> Sequence[str]:
+        query = (
+            'GET hosts\n'
+            'Columns: host_name\n'
+            'OutputFormat: python3\n'
+        )
+        data: Sequence[Sequence[str]] = self.get_raw_data(query=query)
+        if data:
+            LOGGER.info(f'{self.backend} # of hosts found: {len(data)}')
+            return [host[0] for host in data]
+
+        LOGGER.warning(f'{self.backend} no hosts found')
+        return []
+
+    def query_hosts_by_label(self, label: str) -> Sequence[str]:
         query = (
             'GET hosts\n'
             'Columns: name\n'
             'OutputFormat: python3\n'
-            # f'Filter: label_names ~ {label}\n'
             f'Filter: labels = {label}\n'
         )
         data: Sequence[Sequence[str]] = self.get_raw_data(query=query)
-        LOGGER.debug(f'{self.backend} routing capable hosts: {data}')
+        LOGGER.debug(f'{self.backend} hosts matching label: {data}')
         if data:
-            hosts = []
-            for host in data:
-                hosts.append(host[0])
-            return hosts
+            LOGGER.info(f'{self.backend} # of hosts found: {len(data)}')
+            return [host[0] for host in data]
 
-        LOGGER.debug(f'{self.backend} no routing capable hosts found')
-        return None
+        LOGGER.warning(f'{self.backend} no hosts found matching label {label}')
+        return []
 
-    def pre_fetch_hosts(self):
-        LOGGER.debug(f'{self.backend} pre_fetch_hosts')
+    def query_inventory_data(self, hosts: str) -> Dict[str, Dict]:
         query = (
             'GET hosts\n'
-            'Columns: host_name\n'
+            'Columns: host_name mk_inventory\n'
             'OutputFormat: python3\n'
+            f'Filter: host_name ~~ {hosts}\n'
         )
-        data: Sequence[Sequence[str]] = self.get_raw_data(query=query)
+        inventory_data = {}
+        data: Sequence[Tuple[str, bytes]] = self.get_raw_data(query=query)
+        LOGGER.debug(f'{self.backend} data for hosts {hosts}: {data}')
         if data:
-            for host in data:
-                self._cache[host[0]] = {}
-            LOGGER.debug(f'{self.backend} # of host found: {len(self.cache.keys())}')
+            for host, inventory in data:
+                if not inventory:
+                    LOGGER.warning(f'{self.backend} Device: {host}: no inventory data found!')
+                    continue
+                inventory = literal_eval(inventory.decode('utf-8'))
+                inventory_data[host] = inventory
         else:
-            LOGGER.warning(f'{self.backend} no hosts found')
+            LOGGER.warning(f'{self.backend} Device: {hosts}: no inventory data found!')
 
+        return inventory_data
+
+    def query_interface_data(self, hosts: str) -> Dict[str, Dict]:
+        query = (
+            'GET services\n'
+            'Columns: host_name description long_plugin_output\n'
+            'Filter: description ~ ^Interface\n'
+            f'Filter: host_name ~~ {hosts}\n'
+            'OutputFormat: python3\n'
+        )
+        interface_data = {}
+        data: List[Tuple[str, str, str]] = self.get_raw_data(query=query)
+        LOGGER.debug(f'{self.backend} interface data for hosts {hosts}: {data}')
+        if data:
+            for host, description, long_plugin_output in data:
+                if interface_data.get(host) is None:
+                    interface_data[host] = {}
+                interface_data[host][description[10:]] = {  # remove 'Interface ' from description
+                    'long_plugin_output': long_plugin_output.split('\\n')
+                }
+        else:
+            LOGGER.warning(f'{self.backend} No Interfaces items found for hosts {hosts}')
+
+        return interface_data
 
 class HostCacheMultiSite(HostCacheLiveStatus):
     def __init__(
@@ -347,7 +390,7 @@ class HostCacheMultiSite(HostCacheLiveStatus):
         filter_customers: str | None = None,
         customers: List[str] = None,
     ):
-        self._backend = '[MULTISITE]'
+        self.backend = '[MULTISITE]'
         self.sites: SiteConfigurations = SiteConfigurations({})
         self.get_sites()
         self.filter_sites(filter_sites, sites)
@@ -361,9 +404,10 @@ class HostCacheMultiSite(HostCacheLiveStatus):
             dead_sites = ', '.join(self.dead_sites)
             LOGGER.warning(f'{self.backend} WARNING: use of dead site(s) {dead_sites} is disabled')
             self.c.set_only_sites(self.c.alive_sites())
-        super().__init__(pre_fetch, self._backend)
-        if self.pre_fetch:
-            self.pre_fetch_hosts()
+        super().__init__(pre_fetch, self.backend)
+
+    def get_raw_data(self, query: str) -> object:
+        return self.c.query(query=query)
 
     # https://github.com/Checkmk/checkmk/blob/master/packages/cmk-livestatus-client/example_multisite.py
     def get_sites(self):
@@ -435,10 +479,6 @@ class HostCacheMultiSite(HostCacheLiveStatus):
             case _:
                 return
 
-    def get_raw_data(self, query: str) -> object:
-        return self.c.query(query=query)
-
-
 class HostCacheRestApi(HostCache):
     def __init__(
             self,
@@ -447,10 +487,9 @@ class HostCacheRestApi(HostCache):
             filter_sites: str | None = None,
             sites: List[str] = [],
     ):
-        super().__init__(pre_fetch, '[RESTAPI]')
+        self.backend = '[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,33 +497,47 @@ class HostCacheRestApi(HostCache):
         except FileNotFoundError as e:
             LOGGER.exception(f'{self.backend} automation.secret not found, {e}')
             print(f'{self.backend} automation.secret not found, {e}')
-            sys_exit(ExitCodes.AUTOMATION_SECRET_NOT_FOUND.value)
+            sys_exit(ExitCodes.AUTOMATION_SECRET_NOT_FOUND)
+
+        self.__api_port = api_port
         self.__hostname = 'localhost'
         self.__site = OMD_ROOT.split('/')[-1]
-        self.__api_url = f"http://{self.__hostname}:{self._api_port}/{self.__site}/check_mk/api/1.0"
+        self.__api_url = f"http://{self.__hostname}:{self.__api_port}/{self.__site}/check_mk/api/1.0"
         self.__user = 'automation'
+
         LOGGER.info(f'{self.backend} Create REST API session')
         self.__session = session()
         self.__session.headers['Authorization'] = f"Bearer {self.__user} {self.__secret}"
         self.__session.headers['Accept'] = 'application/json'
 
-        self.get_sites()
+        self.sites: MutableSequence[str]  = self.query_sites()
         self.filter_sites(filter_=filter_sites, sites=sites)
         LOGGER.info(f'{self.backend} filtered sites : {self.sites}')
+        super().__init__(pre_fetch, self.backend)
 
-        if self.pre_fetch:
-            self.pre_fetch_hosts()
+    def get_raw_data(self, url: str, params: Mapping[str, object] | None):
+        resp = self.__session.get(
+            url=url,
+            params=params,
+        )
+        LOGGER.debug(f'{self.backend} raw data: {resp.text}')
+        if resp.status_code == 200:
+            return resp.json()
+        else:
+            LOGGER.warning(f'{self.backend} response: {resp.status_code}')
 
-    def get_sites(self):
+    def query_sites(self) -> MutableSequence[str]:
         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}')
+        url = f"{self.__api_url}/domain-types/site_connection/collections/all"
+        sites = []
+        if raw_data:= self.get_raw_data(url, None):
+            raw_sites = raw_data.get("value")
+            sites = [site.get('id') for site in raw_sites]
+            LOGGER.debug(f'{self.backend} sites : {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}')
+            LOGGER.warning(f'{self.backend} got no site information!')
+
+        return sites
 
     def filter_sites(self, filter_: str | None, sites: List[str]):
         match filter_:
@@ -495,170 +548,108 @@ class HostCacheRestApi(HostCache):
             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] = {}
-        # init host_data with None
-        for host in hosts:
-            host_data[host] = None
-        open_hosts = hosts.copy()
-        while open_hosts:
-            hosts_str, open_hosts = hosts_to_query(open_hosts)
-            LOGGER.debug(f'{self.backend} open hosts: {open_hosts}, len: {len(open_hosts)}')
-            query = '{"op": "~~", "left": "name", "right": "' + hosts_str + '"}'
-            resp = self.__session.get(
-                url=f"{self.__api_url}/domain-types/host/collections/all",
-                params={
-                    'query': query,
-                    'columns': ['name', 'mk_inventory'],
-                    'sites': self.sites,
-                },
-            )
-            if resp.status_code == 200:
-                LOGGER.debug(f'{self.backend} {resp.elapsed}|{self._count:0>4}|inventory|{hosts}')
-                data = resp.json().get('value', [])
-                for raw_host in data:
-                    host = raw_host.get('extensions', {}).get('name')
-                    if host:
-                        host_data[host] = raw_host['extensions'].get('mk_inventory')
+    def query_host(self, host: str) -> bool:
+        query = '{"op": "=", "left": "name", "right": "' + host + '"}'
+        url = f'{self.__api_url}/domain-types/host/collections/all'
+        params = {
+            'query': query,
+            'columns': ['name'],
+            'sites': self.sites,
+        }
+
+        if raw_data := self.get_raw_data(url, params):
+            try:
+                data = raw_data['value'][0]['extensions']['name']
+                LOGGER.debug(f'{self.backend} data for host {host}: {data}')
+            except IndexError:
+                LOGGER.warning(f'Host {host} not found in CMK')
             else:
-                LOGGER.warning(
-                    f'{self.backend} got no inventory data found!, status code {resp.status_code}'
-                )
-                LOGGER.debug(f'{self.backend} response query: {query}')
-                LOGGER.debug(f'{self.backend} response text: {resp.text}')
-                LOGGER.debug(f'{self.backend} response url: {resp.url}, len: {len(resp.url)}')
+                if data == host:
+                    return True
 
-        return host_data
+        return False
 
-    def get_interface_data(self, hosts: List[str]) -> Dict[str, Dict | None]:
-        LOGGER.debug(f'{self.backend} get_interface_data {hosts}')
-        host_data: Dict[str, Dict | None] = {}
-        # init host_data with None
-        for host in hosts:
-            host_data[host] = None
-        open_hosts = hosts.copy()
-        while open_hosts:
-            hosts_str, open_hosts = hosts_to_query(open_hosts)
+    def query_all_hosts(self) -> Sequence[str]:
+        url = f'{self.__api_url}/domain-types/host/collections/all'
+        params = {
+            'columns': ['name'],
+            'sites': self.sites,
+        }
 
-            query_host = f'{{"op": "~~", "left": "host_name", "right": "{hosts_str}"}}'
-            query_item = '{"op": "~", "left": "description", "right": "Interface "}'
-            query = f'{{"op": "and", "expr": [{query_item},{query_host}]}}'
-
-            resp = self.__session.get(
-                url=f'{self.__api_url}/domain-types/service/collections/all',
-                params={
-                    'query': query,
-                    'columns': ['host_name', 'description', 'long_plugin_output'],
-                    'sites': self.sites,
-                },
-            )
+        if raw_data := self.get_raw_data(url, params):
+            if data := raw_data.get('value', []):
+                LOGGER.info(f'{self.backend} # of hosts found: {len(data)}')
+                return [host.get('extensions', {}).get('name') for host in data]
+
+        return []
+
+    def query_hosts_by_label(self, label: str) -> Sequence[str]:
+        query = '{"op": "=", "left": "labels", "right": "' + label + '"}'
+
+        url = f'{self.__api_url}/domain-types/host/collections/all'
+        params = {
+            'columns': ['name', 'labels'],
+            'query': query,
+            'sites': self.sites,
+        }
+
+        if raw_data := self.get_raw_data(url, params):
+            if data := raw_data.get('value'):
+                LOGGER.info(f'{self.backend} # of hosts found: {len(data)}')
+                return [host['extensions']['name'] for host in data]
+
+        LOGGER.warning(f'{self.backend} no hosts found matching label {label}')
+        return []
+
+    def query_inventory_data(self, hosts: str) -> Dict[str, Dict]:
+        query = '{"op": "~~", "left": "name", "right": "' + hosts + '"}'
+        url = f"{self.__api_url}/domain-types/host/collections/all"
+        params = {
+            'query': query,
+            'columns': ['name', 'mk_inventory'],
+            'sites': self.sites,
+        }
+
+        inventory_data = {}
+
+        if raw_data := self.get_raw_data(url, params):
+            LOGGER.debug(f'{self.backend} raw inventory data: {raw_data}')
+            if data := raw_data.get('value', []):
+                for raw_host in data:
+                    if host := raw_host.get('extensions', {}).get('name'):
+                        inventory = raw_host['extensions'].get('mk_inventory')
+                        if not inventory:
+                            LOGGER.warning(f'{self.backend} Device: {host}: no inventory data found!')
+                        inventory_data[host] = inventory
+
+        return inventory_data
+
+    def query_interface_data(self, hosts: str) -> Dict[str, Dict]:
+        query_host = f'{{"op": "~~", "left": "host_name", "right": "{hosts}"}}'
+        query_item = '{"op": "~", "left": "description", "right": "Interface "}'
+        query = f'{{"op": "and", "expr": [{query_item},{query_host}]}}'
+
+        url = f'{self.__api_url}/domain-types/service/collections/all'
+        params = {
+            'query': query,
+            'columns': ['host_name', 'description', 'long_plugin_output'],
+            'sites': self.sites,
+        }
 
-            if resp.status_code == 200:
-                LOGGER.debug(f'{resp.elapsed}|{self._count:0>4}|items|{hosts}')
+        interface_data = {}
 
-                data = resp.json().get('value', [])
+        if raw_data := self.get_raw_data(url, params):
+            LOGGER.debug(f'{self.backend} raw interface data: {raw_data}')
+
+            if data := raw_data.get('value', []):
                 for raw_service in data:
                     LOGGER.debug(f'{self.backend} data for service : {raw_service}')
                     service = raw_service.get('extensions')
                     host, description, long_plugin_output = service.values()
-                    host = service['host_name']
-                    description = service['description']
-                    if host_data.get(host) is None:
-                        host_data[host] = {}
-                    host_data[host][description[10:]] = {
+                    if interface_data.get(host) is None:
+                        interface_data[host] = {}
+                    interface_data[host][description[10:]] = {
                         'long_plugin_output': long_plugin_output.split('\\n')
                     }
-            else:
-                LOGGER.warning(
-                    f'{self.backend} got no interface data, response code {resp.status_code}'
-                )
-                LOGGER.debug(f'{self.backend} response query: {query}')
-                LOGGER.debug(f'{self.backend} response text: {resp.text}')
-                LOGGER.debug(f'{self.backend} response url: {resp.url}, len: {len(resp.url)}')
-
-        return host_data
-
-    def host_exists(self, host: str) -> bool:
-        LOGGER.debug(f'{self.backend} host_exists {host}')
-        if self.cache.get(host) is not None:
-            LOGGER.debug(f'{self.backend} host found in cache {host}')
-            return True
-        elif host in self.cache:
-            return False
-
-        # if self.pre_fetch:
-        #     LOGGER.warning(f'{self.backend} pre_fetch host not found in cache {host}')
-        #     return False
-        query = '{"op": "=", "left": "name", "right": "' + host + '"}'
-        resp = self.__session.get(
-                url=f'{self.__api_url}/domain-types/host/collections/all',
-                params={
-                    'query': query,
-                    'columns': ['name'],
-                    'sites': self.sites,
-                },
-            )
-        if resp.status_code == 200:
-            LOGGER.debug(f'{resp.elapsed}|{self._count:0>4}|name|{host}')
-            try:
-                data = resp.json()['value'][0]['extensions']['name']
-                LOGGER.debug(f'{self.backend} data for host {host}: {resp.json()}')
-            except IndexError:
-                LOGGER.warning(f'Host {host} not found in CMK')
-                self.stop_host(host)
-                return False
-            if data == host:
-                return True
-        else:
-            LOGGER.warning(f'{self.backend} response: {resp.status_code}')
-        return False
-
-    def get_hosts_by_label(self, label: str) -> List[str] | None:
-        LOGGER.debug(f'{self.backend} get_hosts_by_label {label}')
-        query = '{"op": "=", "left": "labels", "right": "' + label + '"}'
-
-        resp = self.__session.get(
-            url=f'{self.__api_url}/domain-types/host/collections/all',
-            params={
-                'columns': ['name', 'labels'],
-                'query': query,
-                'sites': self.sites,
-            },
-        )
-        if resp.status_code == 200:
-            LOGGER.debug(f'{self.backend} data for routing_capable: {resp.json()}')
-            LOGGER.debug(f'{resp.elapsed}|{self._count:0>4}|name|routing_capable')
-            try:
-                data = resp.json().get('value')
-                hosts = []
-                for host in data:
-                    hosts.append(host['extensions']['name'])
-                LOGGER.debug(f'{self.backend} host list {hosts}')
-                return hosts
-            except IndexError:
-                LOGGER.debug(f'{self.backend} no routing capable hosts found')
-                return None
-        else:
-            LOGGER.warning(f'{self.backend} response: {resp.status_code}')
-        return None
 
-    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',
-            params={
-                'columns': ['name'],
-                'sites': self.sites,
-            },
-        )
-        if resp.status_code == 200:
-            data = resp.json().get('value', [])
-            for raw_host in data:
-                host = raw_host.get('extensions', {}).get('name')
-                if host:
-                    self._cache[host] = {}
-            LOGGER.debug(f'{self.backend} # of host found: {len(self.cache.keys())}')
-        else:
-            LOGGER.warning(f'{self.backend} respons: {resp.text}')
+        return interface_data
\ No newline at end of file
diff --git a/source/bin/nvdct/lib/constants.py b/source/bin/nvdct/lib/constants.py
index 4bf5240..590cba1 100755
--- a/source/bin/nvdct/lib/constants.py
+++ b/source/bin/nvdct/lib/constants.py
@@ -8,11 +8,42 @@
 # File  : nvdct/lib/constants.py
 
 
+from dataclasses import dataclass
+from enum import Enum, unique, auto
 from logging import getLogger
 from os import environ
 from typing import Final
 
-NVDCT_VERSION: Final[str] = '0.9.5-20241217'
+#
+NVDCT_VERSION: Final[str] = '0.9.6-20241222'
+#
+@unique
+class ExitCodes(Enum):
+    OK = 0
+    BAD_OPTION_LIST = auto()
+    BAD_TOML_FORMAT = auto()
+    BACKEND_NOT_IMPLEMENTED = auto()
+    AUTOMATION_SECRET_NOT_FOUND = auto()
+    NO_LAYER_CONFIGURED = auto()
+
+    def __get__(self, instance, owner):
+        return self.value
+
+@unique
+class IPVersion(Enum):
+    IPv4 = 4
+    IPv6 = 6
+
+    def __get__(self, instance, owner):
+        return self.value
+
+@dataclass(frozen=True)
+class Layer:
+    path: str
+    columns: str
+    label: str
+    host_label: str
+
 #
 OMD_ROOT: Final[str] = environ["OMD_ROOT"]
 #
@@ -20,17 +51,21 @@ 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_L3: 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_L3V6_HOSTS: Final[str] = "'nvdct/l3v6_topology' 'host'"
+HOST_LABEL_L3V6_ROUTER: Final[str] = "'nvdct/l3v6_topology' 'router'"
 HOST_LABEL_LLDP: Final[str] = "'nvdct/has_lldp_neighbours' 'yes'"
 LABEL_CDP: Final[str] = 'CDP'
-LABEL_L3v4: Final[str] = 'LAYER3v4'
+LABEL_L3v4: Final[str] = 'L3v4'
+LABEL_L3v6: Final[str] = 'L3v6'
 LABEL_LLDP: Final[str] = 'LLDP'
+LABEL_STATIC: Final[str] = 'STATIC'
 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'
@@ -40,9 +75,55 @@ 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_L3: 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'
+#
+TOML_CUSTOMERS : Final[str] = 'CUSTOMERS'
+TOML_CUSTOM_LAYERS : Final[str] = 'CUSTOM_LAYERS'
+TOML_EMBLEMS : Final[str] = 'EMBLEMS'
+TOML_L2_DROP_HOSTS: Final[str] = 'L2_DROP_HOSTS'
+TOML_L2_HOST_MAP : Final[str] = 'L2_HOST_MAP'
+TOML_L2_NEIGHBOUR_REPLACE_REGEX : Final[str] = 'L2_NEIGHBOUR_REPLACE_REGEX'
+TOML_L2_SEED_DEVICES: Final[str] = 'L2_SEED_DEVICES'
+TOML_L3V4_IGNORE_WILDCARD : Final[str] = 'L3V4_IGNORE_WILDCARD'
+TOML_L3_IGNORE_HOSTS : Final[str] = 'L3_IGNORE_HOSTS'
+TOML_L3_IGNORE_IP : Final[str] = 'L3_IGNORE_IP'
+TOML_L3_REPLACE : Final[str] = 'L3_REPLACE'
+TOML_L3_SUMMARIZE : Final[str] = 'L3_SUMMARIZE'
+TOML_MAP_SPEED_TO_THICKNESS : Final[str] = 'MAP_SPEED_TO_THICKNESS'
+TOML_PROTECTED_TOPOLOGIES : Final[str] = 'PROTECTED_TOPOLOGIES'
+TOML_SETTINGS : Final[str] = 'SETTINGS'
+TOML_SITES : Final[str] = 'SITES'
+TOML_STATIC_CONNECTIONS : Final[str] = 'STATIC_CONNECTIONS'
+#
+LAYERS = {
+    'CDP': Layer(
+        path=PATH_CDP,
+        columns=COLUMNS_CDP,
+        label=LABEL_CDP,
+        host_label=HOST_LABEL_CDP,
+    ),
+    'LLDP': Layer(
+        path=PATH_LLDP,
+        columns=COLUMNS_LLDP,
+        label=LABEL_LLDP,
+        host_label=HOST_LABEL_LLDP,
+    ),
+    'L3v4': Layer(
+        path=PATH_L3,
+        columns='',
+        label=LABEL_L3v4,
+        host_label=HOST_LABEL_L3V4_ROUTER,
+    ),
+    'L3v6': Layer(
+        path=PATH_L3,
+        columns='',
+        label=LABEL_L3v6,
+        host_label=HOST_LABEL_L3V6_ROUTER,
+    ),
+}
+
diff --git a/source/bin/nvdct/lib/settings.py b/source/bin/nvdct/lib/settings.py
index 0252f74..e625634 100755
--- a/source/bin/nvdct/lib/settings.py
+++ b/source/bin/nvdct/lib/settings.py
@@ -11,7 +11,7 @@
 # 2024-12-17: fixed wrong import for OMD_ROOT (copy&paste) (ThX to BH2005@forum.checkmk.com)
 
 from collections.abc import Mapping
-from ipaddress import AddressValueError, IPv4Address, IPv4Network, NetmaskValueError
+from ipaddress import AddressValueError, NetmaskValueError, ip_address, ip_network
 from logging import CRITICAL, FATAL, ERROR, WARNING, INFO, DEBUG
 from sys import exit as sys_exit
 from time import strftime
@@ -20,15 +20,33 @@ from pathlib import Path
 
 from lib.constants import (
     API_PORT,
+    ExitCodes,
     LOGGER,
     LOG_FILE,
+    Layer,
     OMD_ROOT,
     TIME_FORMAT,
     USER_DATA_FILE,
+
+    TOML_CUSTOMERS,
+    TOML_CUSTOM_LAYERS,
+    TOML_EMBLEMS,
+    TOML_L2_DROP_HOSTS,
+    TOML_L2_HOST_MAP,
+    TOML_L2_NEIGHBOUR_REPLACE_REGEX,
+    TOML_L2_SEED_DEVICES,
+    TOML_L3V4_IGNORE_WILDCARD,
+    TOML_L3_IGNORE_HOSTS,
+    TOML_L3_IGNORE_IP,
+    TOML_L3_REPLACE,
+    TOML_L3_SUMMARIZE,
+    TOML_MAP_SPEED_TO_THICKNESS,
+    TOML_PROTECTED_TOPOLOGIES,
+    TOML_SETTINGS,
+    TOML_SITES,
+    TOML_STATIC_CONNECTIONS,
 )
 from lib.utils import (
-    ExitCodes,
-    Layer,
     get_data_from_toml,
     get_local_cmk_api_port,
     is_valid_customer_name,
@@ -43,8 +61,8 @@ class Emblems(NamedTuple):
     host_node: str
     ip_address: str
     ip_network: str
-    l3v4_replace: str
-    l3v4_summarize: str
+    l3_replace: str
+    l3_summarize: str
     service_node: str
 
 
@@ -113,10 +131,10 @@ class Settings:
         if self.__args.get('check_user_data_only'):
             LOGGER.info(msg=f'Could read/parse the user data from {self.user_data_file}')
             print(f'Could read/parse the user data from {self.user_data_file}')
-            sys_exit(ExitCodes.OK.value)
+            sys_exit(ExitCodes.OK)
 
         # defaults -> overridden by toml -> overridden by cli
-        self.__settings.update(self.__user_data.get('SETTINGS', {}))
+        self.__settings.update(self.__user_data.get(TOML_SETTINGS, {}))
         self.__settings.update(self.__args)
 
         if self.layers:
@@ -126,7 +144,7 @@ class Settings:
                     msg='-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.value)
+                sys_exit(ExitCodes.BAD_OPTION_LIST)
 
         self.__api_port: int | None = None
 
@@ -138,11 +156,11 @@ class Settings:
         self.__l2_host_map: Dict[str, str] | None = None
         self.__l2_neighbour_replace_regex: List[Tuple[str, str]] | None = None
         self.__l2_seed_devices: List[str] | None = None
-        self.__l3v4_ignore_hosts: List[str] | None = None
-        self.__l3v4_ignore_ip: List[IPv4Network] | None = None
+        self.__l3_ignore_hosts: List[str] | None = None
+        self.__l3_ignore_ip: List[ip_network] | None = None
         self.__l3v4_ignore_wildcard: List[Wildcard] | None = None
-        self.__l3v4_replace: Dict[str, str] | None = None
-        self.__l3v4_summarize: List[IPv4Network] | None = None
+        self.__l3_replace: Dict[str, str] | None = None
+        self.__l3_summarize: List[ip_network] | None = None
         self.__map_speed_to_thickness: List[Thickness] | None = None
         self.__protected_topologies: List[str] | None = None
         self.__sites: List[str] | None = None
@@ -318,7 +336,7 @@ class Settings:
     def customers(self) -> List[str]:
         if self.__customers is None:
             self.__customers = [
-                str(customer) for customer in set(self.__user_data.get('CUSTOMERS', []))
+                str(customer) for customer in set(self.__user_data.get(TOML_CUSTOMERS, []))
                 if is_valid_customer_name(customer)]
             LOGGER.info(f'Found {len(self.__customers)} to filter on')
         return self.__customers
@@ -327,7 +345,7 @@ class Settings:
     def custom_layers(self) -> List[Layer]:
         if self.__custom_layers is None:
             self.__custom_layers = []
-            for _layer in self.__user_data.get('CUSTOM_LAYERS', []):
+            for _layer in self.__user_data.get(TOML_CUSTOM_LAYERS, []):
                 try:
                     self.__custom_layers.append(Layer(
                         path=_layer['path'],
@@ -337,25 +355,25 @@ class Settings:
                     ))
                 except KeyError:
                     LOGGER.error(
-                        f'Invalid entry in CUSTOM_LAYERS -> {_layer} -> ignored'
+                        f'Invalid entry in {TOML_CUSTOM_LAYERS} -> {_layer} -> ignored'
                     )
                     continue
             LOGGER.critical(
-                f'Valid entries in CUSTOM_LAYERS found: {len(self.__custom_layers)}/'
-                f'{len(self.__user_data.get("CUSTOM_LAYERS", []))}'
+                f'Valid entries in {TOML_CUSTOM_LAYERS} found: {len(self.__custom_layers)}/'
+                f'{len(self.__user_data.get(TOML_CUSTOM_LAYERS, []))}'
             )
         return self.__custom_layers
 
     @property
     def emblems(self) -> Emblems:
         if self.__emblems is None:
-            raw_emblems = self.__user_data.get('EMBLEMS', {})
+            raw_emblems = self.__user_data.get(TOML_EMBLEMS, {})
             self.__emblems = Emblems(
                 host_node=str(raw_emblems.get('host_node', 'icon_missing')),
                 ip_address=str(raw_emblems.get('ip_address', 'ip-address_80')),
                 ip_network=str(raw_emblems.get('ip_network', 'ip-network_80')),
-                l3v4_replace=str(raw_emblems.get('l3v4_replace', 'icon_plugins_cloud')),
-                l3v4_summarize=str(raw_emblems.get('l3v4_summarize', 'icon_aggr')),
+                l3_replace=str(raw_emblems.get('l3_replace', 'icon_plugins_cloud')),
+                l3_summarize=str(raw_emblems.get('l3_summarize', 'icon_aggr')),
                 service_node=str(raw_emblems.get('service_node', 'icon_missing')),
             )
         return self.__emblems
@@ -363,14 +381,14 @@ class Settings:
     @property
     def l2_drop_hosts(self) -> List[str]:
         if self.__l2_drop_host is None:
-            self.__l2_drop_host = [str(host) for host in set(self.__user_data.get('L2_DROP_HOSTS', []))]
+            self.__l2_drop_host = [str(host) for host in set(self.__user_data.get(TOML_L2_DROP_HOSTS, []))]
         return self.__l2_drop_host
 
     @property
     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', [])) if is_valid_hostname(host)))
+                self.__user_data.get(TOML_L2_SEED_DEVICES, [])) if is_valid_hostname(host)))
         return self.__l2_seed_devices
 
     @property
@@ -378,7 +396,7 @@ class Settings:
         if self.__l2_host_map is None:
             self.__l2_host_map = {
                 str(host): str(replace_host) for host, replace_host in self.__user_data.get(
-                    'L2_HOST_MAP', {}
+                    TOML_L2_HOST_MAP, {}
                 ).items() if is_valid_hostname(host)
             }
         return self.__l2_host_map
@@ -389,47 +407,47 @@ class Settings:
             self.__l2_neighbour_replace_regex = [
                 (
                     str(regex), str(replace)
-                ) for regex, replace in self.__user_data.get('L2_NEIGHBOUR_REPLACE_REGEX', {}).items()
+                ) for regex, replace in self.__user_data.get(TOML_L2_NEIGHBOUR_REPLACE_REGEX, {}).items()
             ]
         return self.__l2_neighbour_replace_regex
 
     @property
-    def l3v4_ignore_hosts(self) -> List[str]:
-        if self.__l3v4_ignore_hosts is None:
-            self.__l3v4_ignore_hosts = [str(host) for host in set(self.__user_data.get(
-                'L3V4_IGNORE_HOSTS', []
+    def l3_ignore_hosts(self) -> List[str]:
+        if self.__l3_ignore_hosts is None:
+            self.__l3_ignore_hosts = [str(host) for host in set(self.__user_data.get(
+                TOML_L3_IGNORE_HOSTS, []
             )) if is_valid_hostname(host)]
-        return self.__l3v4_ignore_hosts
+        return self.__l3_ignore_hosts
 
     @property
-    def l3v4_ignore_ips(self) -> List[IPv4Network]:
-        if self.__l3v4_ignore_ip is None:
-            self.__l3v4_ignore_ip = []
-            for ip_network in self.__user_data.get('L3V4_IGNORE_IP', []):
+    def l3_ignore_ips(self) -> List[ip_network]:
+        if self.__l3_ignore_ip is None:
+            self.__l3_ignore_ip = []
+            for raw_ip_network in self.__user_data.get(TOML_L3_IGNORE_IP, []):
                 try:
-                    self.__l3v4_ignore_ip.append(IPv4Network(ip_network, strict=False))
+                    self.__l3_ignore_ip.append(ip_network(raw_ip_network, strict=False))
                 except (AddressValueError, NetmaskValueError):
                     LOGGER.error(
-                        f'Invalid entry in L3V4_IGNORE_IP found: {ip_network} -> ignored'
+                        f'Invalid entry in {TOML_L3_IGNORE_IP} found: {raw_ip_network} -> ignored'
                     )
                     continue
             LOGGER.info(
-                f'Valid entries in L3V4_IGNORE_IP found: {len(self.__l3v4_ignore_ip)}/'
-                f'{len(self.__user_data.get("L3V4_IGNORE_IP", []))}'
+                f'Valid entries in {TOML_L3_IGNORE_IP} found: {len(self.__l3_ignore_ip)}/'
+                f'{len(self.__user_data.get(TOML_L3_IGNORE_IP, []))}'
             )
 
-        return self.__l3v4_ignore_ip
+        return self.__l3_ignore_ip
 
     @property
     def l3v4_ignore_wildcard(self) -> List[Wildcard]:
         if self.__l3v4_ignore_wildcard is None:
             self.__l3v4_ignore_wildcard = []
-            for entry in self.__user_data.get('L3V4_IRNORE_WILDCARD', []):
+            for entry in self.__user_data.get(TOML_L3V4_IGNORE_WILDCARD, []):
                 try:
-                    ip_address, wildcard = entry
+                    raw_ip_address, wildcard = entry
                 except ValueError:
                     LOGGER.error(
-                        f'Invalid entry in L3V4_IRNORE_WILDCARD -> {entry} -> ignored'
+                        f'Invalid entry in {TOML_L3V4_IGNORE_WILDCARD} -> {entry} -> ignored'
                     )
                     continue
                 try:
@@ -438,71 +456,71 @@ class Settings:
                         [str(255 - int(octet)) for octet in wildcard.split('.')]
                     )
                     self.__l3v4_ignore_wildcard.append(Wildcard(
-                        int_ip_address=int(IPv4Address(ip_address)),
-                        int_wildcard=int(IPv4Address(inverted_wildcard)),
-                        ip_address=ip_address,
+                        int_ip_address=int(ip_address(raw_ip_address)),
+                        int_wildcard=int(ip_address(inverted_wildcard)),
+                        ip_address=raw_ip_address,
                         wildcard=wildcard,
-                        bit_pattern=int(IPv4Address(ip_address)) & int(
-                            IPv4Address(inverted_wildcard)
+                        bit_pattern=int(ip_address(raw_ip_address)) & int(
+                            ip_address(inverted_wildcard)
                         )
                     ))
                 except (AddressValueError, NetmaskValueError):
                     LOGGER.error(
-                        f'Invalid entry in L3V4_IRNORE_WILDCARD -> {entry} -> ignored'
+                        f'Invalid entry in {TOML_L3V4_IGNORE_WILDCARD} -> {entry} -> ignored'
                     )
                     continue
             LOGGER.info(
-                f'Valid entries in L3V4_IRNORE_WILDCARD found: {len(self.__l3v4_ignore_wildcard)}/'
-                f'{len(self.__user_data.get("L3V4_IRNORE_WILDCARD", []))}'
+                f'Valid entries in {TOML_L3V4_IGNORE_WILDCARD} found: {len(self.__l3v4_ignore_wildcard)}/'
+                f'{len(self.__user_data.get(TOML_L3V4_IGNORE_WILDCARD, []))}'
             )
         return self.__l3v4_ignore_wildcard
 
     @property
-    def l3v4_replace(self) -> Dict[str, str]:
-        if self.__l3v4_replace is None:
-            self.__l3v4_replace = {}
-            for ip_network, node in self.__user_data.get('L3V4_REPLACE', {}).items():
+    def l3_replace(self) -> Dict[str, str]:
+        if self.__l3_replace is None:
+            self.__l3_replace = {}
+            for raw_ip_network, node in self.__user_data.get(TOML_L3_REPLACE, {}).items():
                 try:
-                    _ip_network = IPv4Network(ip_network)  # noqa: F841
+                    _ip_network = ip_network(raw_ip_network)  # noqa: F841
                 except (AddressValueError, NetmaskValueError):
                     LOGGER.error(
-                        f'Invalid entry in L3V4_REPLACE found: {ip_network} -> line ignored'
+                        f'Invalid entry in {TOML_L3_REPLACE} found: {raw_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)
+                self.__l3_replace[raw_ip_network] = str(node)
             LOGGER.info(
-                f'Valid entries in L3V4_REPLACE found: {len(self.__l3v4_replace)}/'
-                f'{len(self.__user_data.get("L3V4_REPLACE", {}))}'
+                f'Valid entries in {TOML_L3_REPLACE} found: {len(self.__l3_replace)}/'
+                f'{len(self.__user_data.get(TOML_L3_REPLACE, {}))}'
             )
-        return self.__l3v4_replace
+        return self.__l3_replace
 
     @property
-    def l3v4_summarize(self) -> List[IPv4Network]:
-        if self.__l3v4_summarize is None:
-            self.__l3v4_summarize = []
-            for ip_network in self.__user_data.get('L3V4_SUMMARIZE', []):
+    def l3_summarize(self) -> List[ip_network]:
+        if self.__l3_summarize is None:
+            self.__l3_summarize = []
+            for raw_ip_network in self.__user_data.get(TOML_L3_SUMMARIZE, []):
                 try:
-                    self.__l3v4_summarize.append(IPv4Network(ip_network, strict=False))
+                    self.__l3_summarize.append(ip_network(raw_ip_network, strict=False))
                 except (AddressValueError, NetmaskValueError):
                     LOGGER.error(
-                        f'Invalid entry in L3V4_SUMMARIZE -> {ip_network} -> ignored'
+                        f'Invalid entry in {TOML_L3_SUMMARIZE} -> {raw_ip_network} -> ignored'
                     )
                     continue
             LOGGER.info(
-                f'Valid entries in L3V4_SUMMARIZE found: {len(self.__l3v4_summarize)}/'
-                f'{len(self.__user_data.get("L3V4_SUMMARIZE", []))}'
+                f'Valid entries in {TOML_L3_SUMMARIZE} found: {len(self.__l3_summarize)}/'
+                f'{len(self.__user_data.get(TOML_L3_SUMMARIZE, []))}'
             )
-        return self.__l3v4_summarize
+        return self.__l3_summarize
 
     @property
     def map_speed_to_thickness(self) -> List[Thickness]:
         if self.__map_speed_to_thickness is None:
             self.__map_speed_to_thickness = []
             map_speed_to_thickness = self.__user_data.get(
-                'MAP_SPEED_TO_THICKNESS', {}
+                TOML_MAP_SPEED_TO_THICKNESS, {}
             )
             for speed, thickness in map_speed_to_thickness.items():
                 try:
@@ -512,12 +530,12 @@ class Settings:
                     ))
                 except ValueError:
                     LOGGER.error(
-                        f'Invalid entry in MAP_SPEED_TO_THICKNESS -> {speed}={thickness} -> ignored'
+                        f'Invalid entry in {TOML_MAP_SPEED_TO_THICKNESS} -> {speed}={thickness} -> ignored'
                     )
                     continue
             LOGGER.info(
-                f'Valid entries in MAP_SPEED_TO_THICKNESS found: {len(self.__map_speed_to_thickness)}'  # noqa: E501
-                f'/{len(self.__user_data.get("MAP_SPEED_TO_THICKNESS", []))}'
+                f'Valid entries in {TOML_MAP_SPEED_TO_THICKNESS} found: {len(self.__map_speed_to_thickness)}'  # noqa: E501
+                f'/{len(self.__user_data.get(TOML_MAP_SPEED_TO_THICKNESS, []))}'
             )
         return self.__map_speed_to_thickness
 
@@ -525,7 +543,7 @@ class Settings:
     def protected_topologies(self) -> List[str]:
         if self.__protected_topologies is None:
             self.__protected_topologies = [str(topology) for topology in self.__user_data.get(
-                'PROTECTED_TOPOLOGIES', []
+                TOML_PROTECTED_TOPOLOGIES, []
             )]
         return self.__protected_topologies
 
@@ -533,12 +551,12 @@ class Settings:
     def static_connections(self) -> List[StaticConnection]:
         if self.__static_connections is None:
             self.__static_connections = []
-            for connection in self.__user_data.get('STATIC_CONNECTIONS', []):
+            for connection in self.__user_data.get(TOML_STATIC_CONNECTIONS, []):
                 try:
                     left_host, left_service, right_service, right_host = connection
                 except ValueError:
                     LOGGER.error(
-                        f'Wrong entry in STATIC_CONNECTIONS -> {connection} -> ignored'
+                        f'Wrong entry in {TOML_STATIC_CONNECTIONS} -> {connection} -> ignored'
                     )
                     continue
                 if not right_host or not left_host:
@@ -553,14 +571,14 @@ class Settings:
                     left_host=str(left_host),
                 ))
             LOGGER.info(
-                f'Valid entries in STATIC_CONNECTIONS found: {len(self.__static_connections)}/'
-                f'{len(self.__user_data.get("STATIC_CONNECTIONS", []))}'
+                f'Valid entries in {TOML_STATIC_CONNECTIONS} found: {len(self.__static_connections)}/'
+                f'{len(self.__user_data.get(TOML_STATIC_CONNECTIONS, []))}'
             )
         return self.__static_connections
 
     @property
     def sites(self) -> List[str]:
         if self.__sites is None:
-            self.__sites = [str(site) for site in set(self.__user_data.get('SITES', [])) if is_valid_site_name(site)]
+            self.__sites = [str(site) for site in set(self.__user_data.get(TOML_SITES, [])) if is_valid_site_name(site)]
             LOGGER.info(f'Found {len(self.__sites)} to filter on')
         return self.__sites
diff --git a/source/bin/nvdct/lib/topologies.py b/source/bin/nvdct/lib/topologies.py
index 32710db..3fa4cbb 100755
--- a/source/bin/nvdct/lib/topologies.py
+++ b/source/bin/nvdct/lib/topologies.py
@@ -2,16 +2,21 @@
 # -*- coding: utf-8 -*-
 #
 # License: GNU General Public License v2
+import sys
 
 # Author: thl-cmk[at]outlook[dot]com
 # URL   : https://thl-cmk.hopto.org
 # Date  : 2024-06-09
 # File  : lib/topologies.py
 
-from collections.abc import Mapping, Sequence
-from ipaddress import IPv4Address, IPv4Network
-from typing import Dict, List, Tuple
+# 2024-12-22: refactoring topology creation into classes
+#             made L3 topology IP version independent
+
+from abc import abstractmethod
+from collections.abc import Mapping, MutableMapping, Sequence
+from ipaddress import ip_address, ip_network, ip_interface
 from re import sub as re_sub
+from typing import Dict, List, Tuple
 
 from lib.backends import (
     CacheItems,
@@ -21,9 +26,13 @@ from lib.constants import (
     CACHE_INTERFACES_DATA,
     HOST_LABEL_L3V4_HOSTS,
     HOST_LABEL_L3V4_ROUTER,
+    HOST_LABEL_L3V6_HOSTS,
+    HOST_LABEL_L3V6_ROUTER,
+    IPVersion,
     LOGGER,
     PATH_INTERFACES,
-    PATH_L3v4,
+    PATH_L3,
+    DATAPATH,
 )
 from lib.settings import (
     Emblems,
@@ -33,8 +42,9 @@ from lib.settings import (
 )
 from lib.utils import (
     InventoryColumns,
-    Ipv4Info,
-    is_valid_hostname,
+    IpInfo,
+    # is_valid_hostname,
+    save_data_to_file,
 )
 
 
@@ -165,21 +175,21 @@ class NvObjects:
         elif metadata is not {}:
             self.nv_objects[service_object]['metadata'].update(metadata)
 
-    def add_ipv4_address(
+    def add_ip_address(
         self,
         host: str,
-        ipv4_address: str,
+        raw_ip_address: str,
         emblem: str,
         interface: str | None,
     ) -> None:
         if interface is not None:
-            service_object = f'{ipv4_address}@{interface}@{host}'
+            service_object = f'{raw_ip_address}@{interface}@{host}'
         else:
-            service_object = f'{ipv4_address}@{host}'
+            service_object = f'{raw_ip_address}@{host}'
 
         if service_object not in self.nv_objects:
             self.nv_objects[service_object] = {
-                'name': ipv4_address,
+                'name': raw_ip_address,
                 'link': {},
                 'metadata': {
                     'images': {
@@ -188,13 +198,13 @@ class NvObjects:
                     },
                     'tooltip': {
                         'quickinfo': [
-                            {'name': 'IP-Address', 'value': ipv4_address},
+                            {'name': 'IP-Address', 'value': raw_ip_address},
                         ]
                     }
                 }
             }
 
-    def add_ipv4_network(self, network: str, emblem: str, ) -> None:
+    def add_ip_network(self, network: str, emblem: str, ) -> None:
         if network not in self.nv_objects:
             self.nv_objects[network] = {
                 'name': network,
@@ -306,17 +316,18 @@ class NvConnections:
                         f'<->{right} (duplex: {right_duplex})'
                     )
             if left_native_vlan and right_native_vlan:
-                if left_native_vlan != right_native_vlan:
-                    warning = True
-
-                    metadata = add_tooltip_html(
-                        metadata, 'Native<br>VLAN', left, left_native_vlan, right, right_native_vlan
-                    )
-
-                    LOGGER.warning(
-                        f'Connection with native vlan mismatch: '
-                        f'{left} (vlan: {left_native_vlan})<->{right} (vlan: {right_native_vlan})'
-                    )
+                if left_native_vlan != '0' and right_native_vlan != '0':  # ignore VLAN 0 (Native VLAN on routed ports)
+                    if left_native_vlan != right_native_vlan:
+                        warning = True
+
+                        metadata = add_tooltip_html(
+                            metadata, 'Native<br>VLAN', left, left_native_vlan, right, right_native_vlan
+                        )
+
+                        LOGGER.warning(
+                            f'Connection with native vlan mismatch: '
+                            f'{left} (vlan: {left_native_vlan})<->{right} (vlan: {right_native_vlan})'
+                        )
             if warning:
                 metadata['line_config'].update({
                         'color': 'red',
@@ -329,6 +340,481 @@ class NvConnections:
             connection.append(metadata)
 
 
+class Topology:
+    def __init__(
+            self,
+            emblems: Emblems,
+            host_cache: HostCache,
+    ):
+        self.nv_objects: NvObjects = NvObjects()
+        self.nv_connections: NvConnections = NvConnections()
+        self.emblems: Emblems = emblems
+        self.host_cache: HostCache = host_cache
+
+    @abstractmethod
+    def create(self):
+        raise NotImplementedError
+
+    def save(self, label:str, output_directory: str, make_default: bool):
+        data = {
+            'version': 1,
+            'name': label,
+            'objects': dict(sorted(self.nv_objects.nv_objects.items())),
+            'connections': sorted(self.nv_connections.nv_connections)
+        }
+        save_data_to_file(
+            data=data,
+            path=(
+                f'{DATAPATH}/{output_directory}'
+            ),
+            file=f'data_{label}.json',
+            make_default=make_default,
+        )
+
+class TopologyStatic(Topology):
+    def __init__(
+            self,
+            connections: Sequence[StaticConnection],
+            emblems: Emblems,
+            host_cache: HostCache,
+    ):
+        super().__init__(
+            emblems=emblems,
+            host_cache=host_cache,
+        )
+        self.connections: Sequence[StaticConnection] = connections
+
+    def create(self):
+        for connection in self.connections:
+            LOGGER.info(msg=f'connection: {connection}')
+            self.nv_objects.add_host(
+                host=connection.right_host,
+                host_cache=self.host_cache,
+                emblem=self.emblems.host_node
+            )
+            self.nv_objects.add_host(
+                host=connection.left_host,
+                host_cache=self.host_cache,
+                emblem=self.emblems.host_node
+            )
+            if connection.right_service:
+                self.nv_objects.add_service(
+                    host=connection.right_host,
+                    host_cache=self.host_cache,
+                    emblem=self.emblems.service_node,
+                    service=connection.right_service
+                )
+                self.nv_connections.add_connection(
+                    left=connection.right_host,
+                    right=f'{connection.right_service}@{connection.right_host}',
+                )
+
+            if connection.left_service:
+                self.nv_objects.add_service(
+                    host=connection.left_host,
+                    host_cache=self.host_cache,
+                    emblem=self.emblems.service_node,
+                    service=connection.left_service
+                )
+                self.nv_connections.add_connection(
+                    left=connection.left_host,
+                    right=f'{connection.left_service}@{connection.left_host}',
+                )
+
+            if connection.right_service and connection.left_service:
+                self.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
+                self.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
+                self.nv_connections.add_connection(
+                    left=f'{connection.right_host}',
+                    right=f'{connection.left_service}@{connection.left_host}',
+                )
+            else:  # connect right_host with left_host
+                self.nv_connections.add_connection(
+                    left=f'{connection.right_host}',
+                    right=f'{connection.left_host}',
+                )
+
+class TopologyL2(Topology):
+    def __init__(
+            self,
+            emblems: Emblems,
+            host_cache: HostCache,
+            case: str,
+            inv_columns: InventoryColumns,
+            l2_drop_hosts: List[str],
+            l2_host_map: Dict[str, str],
+            l2_neighbour_replace_regex: List[Tuple[str, str]],
+            label: str,
+            path_in_inventory: str,
+            prefix: str,
+            remove_domain: bool,
+            seed_devices: Sequence[str],
+    ):
+        super().__init__(
+            emblems=emblems,
+            host_cache=host_cache,
+        )
+        self.case: str = case
+        self.inv_columns: InventoryColumns = inv_columns
+        self.l2_drop_hosts: List[str] = l2_drop_hosts
+        self.l2_host_map: Dict[str, str] = l2_host_map
+        self.l2_neighbour_replace_regex: List[Tuple[str, str]] = l2_neighbour_replace_regex
+        self.label: str = label
+        self.neighbour_to_host: MutableMapping[str, str] = {}
+        self.path_in_inventory: str = path_in_inventory
+        self.prefix: str = prefix
+        self.remove_domain: bool = remove_domain
+        self.seed_devices: Sequence[str] = seed_devices
+
+    def create(self):
+        if not (devices_to_go := list(set(self.seed_devices))):  # remove duplicates
+            LOGGER.error('No seed devices configured!')
+            return
+
+        devices_done = []
+
+        while devices_to_go:
+            device = devices_to_go[0]
+
+            if device in self.l2_host_map.keys():
+                try:
+                    devices_to_go.remove(device)
+                except ValueError:
+                    pass
+                device = self.l2_host_map[device]
+                if device in devices_done:
+                    continue
+
+            topo_data = self.host_cache.get_data(
+                host=device, item=CacheItems.inventory, path=self.path_in_inventory
+            )
+            if topo_data:
+                self.device_from_inventory(
+                    host=device,
+                    inv_data=topo_data,
+                )
+
+                for _entry in self.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: {self.label}')
+
+    def device_from_inventory(
+            self,
+            host: str,
+            inv_data,
+    ):
+        for topo_neighbour in inv_data:
+            # check if required data are not empty
+            if not (neighbour := topo_neighbour.get(self.inv_columns.neighbour)):
+                LOGGER.warning(f'incomplete data, neighbour missing {topo_neighbour}')
+                continue
+            if not (raw_local_port := topo_neighbour.get(self.inv_columns.local_port)):
+                LOGGER.warning(f'incomplete data, local port missing {topo_neighbour}')
+                continue
+            if not (raw_neighbour_port := topo_neighbour.get(self.inv_columns.neighbour_port)):
+                LOGGER.warning(f'incomplete data, neighbour port missing {topo_neighbour}')
+                continue
+
+            if not (neighbour:= self.get_host_from_neighbour(neighbour)):
+                continue
+
+            # getting/checking interfaces
+            local_port = get_service_by_interface(host, raw_local_port, self.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, self.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'),
+            }
+
+            self.nv_objects.add_host(host=host, host_cache=self.host_cache)
+            self.nv_objects.add_host(host=neighbour, host_cache=self.host_cache)
+            self.nv_objects.add_interface(
+                host=str(host),
+                service=str(local_port),
+                host_cache=self.host_cache,
+                metadata=metadata,
+                name=str(raw_local_port),
+                item=str(local_port)
+            )
+            self.nv_objects.add_interface(
+                host=str(neighbour),
+                service=str(neighbour_port),
+                host_cache=self.host_cache,
+                name=str(raw_neighbour_port),
+                item=str(neighbour_port)
+            )
+            self.nv_connections.add_connection(
+                left=str(host),
+                right=f'{local_port}@{host}',
+            )
+            self.nv_connections.add_connection(
+                left=str(neighbour),
+                right=f'{neighbour_port}@{neighbour}',
+            )
+            self.nv_connections.add_connection(
+                left=f'{local_port}@{host}',
+                right=f'{neighbour_port}@{neighbour}',
+            )
+
+    def get_host_from_neighbour(self, neighbour: str) -> str | None:
+        try:
+            return self.neighbour_to_host[neighbour]
+        except KeyError:
+            pass
+
+        if neighbour in self.l2_drop_hosts:
+            LOGGER.info(msg=f'drop neighbour: {neighbour}')
+            self.neighbour_to_host[neighbour] = None
+            return None
+
+        if self.l2_neighbour_replace_regex:
+            for re_str, replace_str in self.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:
+                self.neighbour_to_host[neighbour] = None
+                return None
+
+        if self.remove_domain:
+            neighbour = neighbour.split('.')[0]
+
+        # drop neighbour after domain split
+        if neighbour in self.l2_drop_hosts:
+            LOGGER.info(msg=f'drop neighbour: {neighbour}')
+            self.neighbour_to_host[neighbour] = None
+            return None
+
+        if self.case == 'UPPER':
+            neighbour = neighbour.upper()
+            LOGGER.debug(f'Changed neighbour to upper case: {neighbour}')
+        elif self.case == 'LOWER':
+            neighbour = neighbour.lower()
+            LOGGER.debug(f'Changed neighbour to lower case: {neighbour}')
+
+        if self.prefix:
+            neighbour = f'{self.prefix}{neighbour}'
+        # rewrite neighbour if inventory neighbour and checkmk host don't match
+        if neighbour in self.l2_host_map.keys():
+            neighbour = self.l2_host_map[neighbour]
+
+        return neighbour
+
+class TopologyL3(Topology):
+    def __init__(
+            self,
+            emblems: Emblems,
+            host_cache: HostCache,
+            ignore_hosts: Sequence[str],
+            ignore_ips: Sequence[ip_network],
+            ignore_wildcard: Sequence[Wildcard],
+            include_hosts: bool,
+            replace: Mapping[str, str],
+            skip_if: bool,
+            skip_ip: bool,
+            summarize: Sequence[ip_network],
+            version: int
+    ):
+        super().__init__(
+            emblems=emblems,
+            host_cache=host_cache,
+        )
+        self.ignore_hosts: Sequence[str] = ignore_hosts
+        self.ignore_ips: Sequence[ip_network] = ignore_ips
+        self.ignore_wildcard: Sequence[Wildcard] = ignore_wildcard
+        self.include_hosts: bool = include_hosts
+        self.replace: Mapping[str, str] = replace
+        self.skip_if: bool = skip_if
+        self.skip_ip: bool = skip_ip
+        self.summarize: Sequence[ip_network] = summarize
+        self.version = version
+
+    def create(self):
+        match self.version:
+            case IPVersion.IPv4:
+                host_list: Sequence[str] = self.host_cache.get_hosts_by_label(HOST_LABEL_L3V4_ROUTER)
+
+                if self.include_hosts:
+                    host_list += self.host_cache.get_hosts_by_label(HOST_LABEL_L3V4_HOSTS)
+
+            case IPVersion.IPv6:
+                host_list: Sequence[str] = self.host_cache.get_hosts_by_label(HOST_LABEL_L3V6_ROUTER)
+
+                if self.include_hosts:
+                    host_list += self.host_cache.get_hosts_by_label(HOST_LABEL_L3V6_HOSTS)
+
+            case _:
+                host_list = []
+
+        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'L3 ignore hosts: {self.ignore_hosts}')
+        for raw_host in host_list:
+            host = raw_host
+            if host in self.ignore_hosts:
+                LOGGER.info(f'L3 host {host} ignored')
+                continue
+            if not (inv_ip_addresses := self.host_cache.get_data(
+                    host=host, item=CacheItems.inventory, path=PATH_L3)
+            ):
+                LOGGER.warning(f'No IP address inventory found for host: {host}')
+                continue
+
+            self.nv_objects.add_host(host=host, host_cache=self.host_cache)
+            for inv_ip_address in inv_ip_addresses:
+                emblem = self.emblems.ip_network
+                try:
+                    ip_info = IpInfo(
+                        address=inv_ip_address['address'],
+                        device=inv_ip_address['device'],
+                        broadcast=inv_ip_address['broadcast'],
+                        cidr=inv_ip_address['cidr'],
+                        netmask=inv_ip_address['netmask'],
+                        network=inv_ip_address['network'],
+                        type=inv_ip_address['type'],
+                        scope_id=inv_ip_address.get('scope_id'),  # this is an optional field
+                    )
+                except KeyError:
+                    LOGGER.warning(f'Drop IP address data for host: {host}, data: {inv_ip_address}')
+                    continue
+
+                interface_address: ip_interface = ip_interface(f'{ip_info.address}/{ip_info.cidr}')
+                if interface_address.version != self.version:
+                    LOGGER.info(
+                        f'host: {host} dropped non IPv{self.version} address: {ip_info.address},'
+                        f' type: {ip_info.type}'
+                    )
+                    continue
+
+                if interface_address.is_loopback:
+                    LOGGER.info(f'host: {host} dropped loopback address: {ip_info.address}')
+                    continue
+
+                if interface_address.is_link_local:
+                    LOGGER.info(f'host: {host} dropped link-local address: {ip_info.address}')
+                    continue
+
+                # if interface_address.network.prefixlen == 32 or interface_address.network.prefixlen == 128: # drop host addresses
+                #     LOGGER.info(
+                #         f'host: {host} dropped host address: {ip_info.address}/{ip_info.cidr}'
+                #     )
+                #     continue
+
+                if is_ignore_ip(ip_info.address, self.ignore_ips):
+                    LOGGER.info(f'host: {host} dropped ignore address: {ip_info.address}')
+                    continue
+
+                if is_ignore_wildcard(ip_info.address, self.ignore_wildcard):
+                    LOGGER.info(f'host: {host} dropped wildcard address: {ip_info.address}')
+                    continue
+
+                if network := get_network_summary(
+                        raw_ip_address=ip_info.address,
+                        summarize=self.summarize,
+                ):
+                    emblem = self.emblems.l3_summarize
+                    LOGGER.info(
+                        f'Network summarized: {ip_info.network}/{ip_info.cidr} -> {network}'
+                    )
+                else:
+                    network = f'{ip_info.network}/{ip_info.cidr}'
+
+                if network in self.replace.keys():
+                    LOGGER.info(f'Replaced network {network} with {self.replace[network]}')
+                    network = self.replace[network]
+                    emblem = self.emblems.l3_replace
+
+                self.nv_objects.add_ip_network(network=network, emblem=emblem)
+
+                if self.skip_if is True and self.skip_ip is True:
+                    self.nv_connections.add_connection(left=host, right=network)
+                elif self.skip_if is True and self.skip_ip is False:
+                    self.nv_objects.add_ip_address(
+                        host=host,
+                        interface=None,
+                        raw_ip_address=ip_info.address,
+                        emblem=self.emblems.ip_address,
+                    )
+                    self.nv_objects.add_tooltip_quickinfo(
+                        '{ip_info.address}@{host}', 'Interface', ip_info.device
+                    )
+                    self.nv_connections.add_connection(left=f'{host}', right=f'{ip_info.address}@{host}')
+                    self.nv_connections.add_connection(left=network, right=f'{ip_info.address}@{host}')
+                elif self.skip_if is False and self.skip_ip is True:
+                    self.nv_objects.add_interface(
+                        host=host, service=ip_info.device, host_cache=self.host_cache
+                    )
+                    self.nv_objects.add_tooltip_quickinfo(
+                        f'{ip_info.device}@{host}', 'IP-address', ip_info.address
+                    )
+                    self.nv_connections.add_connection(left=f'{host}', right=f'{ip_info.device}@{host}')
+                    self.nv_connections.add_connection(left=network, right=f'{ip_info.device}@{host}')
+                else:
+                    self.nv_objects.add_ip_address(
+                        host=host,
+                        interface=ip_info.device,
+                        raw_ip_address=ip_info.address,
+                        emblem=self.emblems.ip_address,
+                    )
+                    self.nv_objects.add_interface(
+                        host=host, service=ip_info.device, host_cache=self.host_cache,
+                    )
+                    self.nv_connections.add_connection(
+                        left=host, right=f'{ip_info.device}@{host}')
+                    self.nv_connections.add_connection(
+                        left=f'{ip_info.device}@{host}',
+                        right=f'{ip_info.address}@{ip_info.device}@{host}',
+                    )
+                    self.nv_connections.add_connection(
+                        left=network, right=f'{ip_info.address}@{ip_info.device}@{host}',
+                    )
+
+
 def map_speed_to_thickness(speed_to_map: int, speed_map: Sequence[Thickness]) -> int:
     thickness: int = 1  # use in case of empty MAP_SPEED_TO_THICKNESS
     for speed, thickness in speed_map:
@@ -584,27 +1070,33 @@ def close_tooltip_html(metadata: Dict) -> Dict:
     return metadata
 
 
-def get_network_summary(ipv4_address: str, summarize: Sequence[IPv4Network]) -> str | None:
+def get_network_summary(raw_ip_address: str, summarize: Sequence[ip_network]) -> str | None:
     for network in summarize:
-        if IPv4Network(ipv4_address).subnet_of(network):
-            return network.exploded
+        try:
+            if ip_network(raw_ip_address).subnet_of(network):
+                return network.compressed
+        except TypeError:
+            pass
     return None
 
 
-def is_ignore_ipv4(ip_address: str, ignore_ips: Sequence[IPv4Network]) -> bool:
+def is_ignore_ip(raw_ip_address: str, ignore_ips: Sequence[ip_network]) -> bool:
     for ip in ignore_ips:
-        if IPv4Network(ip_address).subnet_of(ip):
-            LOGGER.info(f'IP address {ip_address} is in ignore list -> ({ip})')
-            return True
+        try:
+            if ip_network(raw_ip_address).subnet_of(ip):
+                LOGGER.info(f'IP address {raw_ip_address} is in ignore list -> ({ip})')
+                return True
+        except TypeError:
+            continue
     return False
 
 
-def is_ignore_wildcard(ip_address: str, ignore_wildcard: Sequence[Wildcard]) -> bool:
-    int_ip_address = int(IPv4Address(ip_address))
+def is_ignore_wildcard(raw_ip_address: str, ignore_wildcard: Sequence[Wildcard]) -> bool:
+    int_ip_address = int(ip_address(raw_ip_address))
     for wildcard in ignore_wildcard:
         if int_ip_address & wildcard.int_wildcard == wildcard.bit_pattern:
             LOGGER.info(
-                f'IP address {ip_address} matches ignore wildcard '
+                f'IP address {raw_ip_address} matches ignore wildcard '
                 f'list ({wildcard.ip_address}/{wildcard.wildcard})'
             )
             return True
@@ -616,387 +1108,3 @@ 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=str(host),
-            service=str(local_port),
-            host_cache=host_cache,
-            metadata=metadata,
-            name=str(raw_local_port),
-            item=str(local_port)
-        )
-        nv_objects.add_interface(
-            host=str(neighbour),
-            service=str(neighbour_port),
-            host_cache=host_cache,
-            name=str(raw_neighbour_port),
-            item=str(neighbour_port)
-        )
-        nv_connections.add_connection(
-            left=str(host),
-            right=f'{local_port}@{host}',
-        )
-        nv_connections.add_connection(
-            left=str(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},'
-                    f' 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 4bd7f57..c504922 100755
--- a/source/bin/nvdct/lib/utils.py
+++ b/source/bin/nvdct/lib/utils.py
@@ -7,10 +7,9 @@
 # Date  : 2023-10-12
 # File  : nvdct/lib/utils.py
 
-from collections.abc import Mapping, Sequence
 from ast import literal_eval
+from collections.abc import Mapping, Sequence
 from dataclasses import dataclass
-from enum import Enum, unique
 from json import dumps
 from logging import disable as log_off, Formatter, getLogger, StreamHandler
 from logging.handlers import RotatingFileHandler
@@ -24,33 +23,15 @@ from typing import List, Dict, TextIO
 
 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,
+    DATAPATH,
+    ExitCodes,
     LOGGER,
     OMD_ROOT,
-    PATH_CDP,
-    PATH_L3v4,
-    PATH_LLDP,
-    DATAPATH,
 )
 
-@unique
-class ExitCodes(Enum):
-    OK = 0
-    BAD_OPTION_LIST = 1
-    BAD_TOML_FORMAT = 2
-    BACKEND_NOT_IMPLEMENTED = 3
-    AUTOMATION_SECRET_NOT_FOUND = 4
-    NO_LAYER_CONFIGURED = 5
 
 @dataclass(frozen=True)
-class Ipv4Info:
+class IpInfo:
     address: str
     device: str
     broadcast: str
@@ -58,6 +39,7 @@ class Ipv4Info:
     netmask: str
     network: str
     type: str
+    scope_id: str | None
 
 @dataclass(frozen=True)
 class InventoryColumns:
@@ -65,35 +47,6 @@ class InventoryColumns:
     local_port: str
     neighbour_port: str
 
-@dataclass(frozen=True)
-class Layer:
-    path: str
-    columns: str
-    label: str
-    host_label: str
-
-LAYERS = {
-    'CDP': Layer(
-        path=PATH_CDP,
-        columns=COLUMNS_CDP,
-        label=LABEL_CDP,
-        host_label=HOST_LABEL_CDP,
-    ),
-    'LLDP': Layer(
-        path=PATH_LLDP,
-        columns=COLUMNS_LLDP,
-        label=LABEL_LLDP,
-        host_label=HOST_LABEL_LLDP,
-    ),
-    'L3v4': Layer(
-        path=PATH_L3v4,
-        columns='',
-        label=LABEL_L3v4,
-        host_label=HOST_LABEL_L3V4_ROUTER,
-    ),
-}
-
-
 def get_local_cmk_version() -> str:
     return Path(f'{OMD_ROOT}/version').readlink().name
 
@@ -133,7 +86,7 @@ def get_data_from_toml(file: str) -> Dict:
                 msg=f'ERROR: data file {toml_file} is not in valid TOML format! ({e}),'
                     f' (see https://toml.io/en/)'
             )
-            sys_exit(ExitCodes.BAD_TOML_FORMAT.value)
+            sys_exit(ExitCodes.BAD_TOML_FORMAT)
 
     else:
         LOGGER.error(msg=f'WARNING: User data {file} not found.')
@@ -414,7 +367,6 @@ def get_table_from_inventory(inventory: Dict[str, object], raw_path: str) -> Lis
         return None
     for m in path:
         try:
-            # print(raw_table[m])
             table = table[m]
         except KeyError:
             LOGGER.info(msg=f'Inventory table for {path} not found')
diff --git a/source/bin/nvdct/nvdct.py b/source/bin/nvdct/nvdct.py
index 8966b10..ffd8468 100755
--- a/source/bin/nvdct/nvdct.py
+++ b/source/bin/nvdct/nvdct.py
@@ -82,7 +82,7 @@
 #             added duplex mismatch ('yellow')
 #             added native vlan mismatch ('orange')
 # 2024-03-25: added --pre-fetch, this will speed up the RESTAPI backend performance
-#             min inv_cdp_cahe version: 0.7.1-20240320 -> host label nvdct/has_cdp_neighbours
+#             min inv_cdp_cache version: 0.7.1-20240320 -> host label nvdct/has_cdp_neighbours
 #             min inv_lldp_cache version: 0.9.3-20240320 -> host label nvdct/has_lldp_neighbours
 # 2024-03-27: added option --api-port, defaults to 80
 # 2024-03-28: changed restapi get_interface_data to use one call to fetch all data
@@ -107,38 +107,37 @@
 #             added IP-Address/IP-Network as quickinfo
 # 2024-05-18: fixed crash non empty neighbour port (ThX to andreas doehler)
 # 2024-06-05: fixed interface index padding
-# 2024-06-06: added interfcae detection alias+index
+# 2024-06-06: added interface detection alias+index
 #             added interface detection description + index
-# 2024-06-09: moved topologiy helpers to lib/topologies.py
+# 2024-06-09: moved topology helpers to lib/topologies.py
 #             moved (default) config file(s) to ./conf/
 # 2024-06-14: added debug code for bad IPv4 address data
 # 2024-06-17: fixed bad IPv4 address data (just drop it)
-# 2024-09-23: incompatible replaced options --lowercase/--uppercase with --case LOWER|UPPER
+# 2024-09-23: INCOMPATIBLE: replaced options --lowercase/--uppercase with --case LOWER|UPPER
 #             changed version output from settings to argparse action
-#             incompatible removed backend FILESYSTEM -> will fallback to MULTISITE
-#             incompatible
-#             removed support for CMK2.2.x file format (removed option --new-format)
+#             INCOMPATIBLE: removed backend FILESYSTEM -> will fallback to MULTISITE
+#             INCOMPATIBLE: removed support for CMK2.2.x file format (removed option --new-format)
 # 2024-09-24: added site filter for multisite deployments (MULTISITE only), option --filter-sites
 #               and SITES section in toml file
 #             added customer filter for MSP deployments (MULTISITE only), option --filter-customers
 #               and section CUSTOMERS in toml file
 # 2024-11-16: added better logging for missing L2 data
 # 2024-11-17: added L2_NEIGHBOUR_REPLACE_REGEX (ThX to Frankb@checkmk forum for the base idea)
-#             incompatible removed DROP_HOST_REGEX -> use L2_NEIGHBOUR_REPLACE_REGEX instead
-#             incompatible changed section names in TOML file to better distinguish between L2 and L3v4
+#             INCOMPATIBLE: removed DROP_HOST_REGEX -> use L2_NEIGHBOUR_REPLACE_REGEX instead
+#             INCOMPATIBLE: changed section names in TOML file to better distinguish between L2 and L3v4
 #               HOST_MAP -> L2_HOST_MAP
 #               DROP_HOSTS -> L2_DROP_HOSTS
 #               SEED_DEVICES -> L2_SEED_DEVICES
-#             incompatible removed option -s, --seed-devices from CLI -> use TOML section L2_SEED_DEVICES instead
-#             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
+#             INCOMPATIBLE_ removed option -s, --seed-devices from CLI -> use TOML section L2_SEED_DEVICES instead
+#             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
+# 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
 #
@@ -146,6 +145,18 @@
 #               - inv_lnx_if_ip-0.0.4-20241210.mkp
 #               - inv_ip_address-0.0.6-20241210.mkp
 #               - inv_win_if_ip-0.0.3-20241210.mkp
+# 2024-12-20: fixed typo in TOML L3v4 -> "L3v4" (ThX to BH2005@checkmk_forum)
+#             fixed crash in topologies (devices_to_go.remove(device) ValueError if device not in list) (ThX to BH2005)
+# 2024-12-23: streamlined L3v4 in preparation for L3v6 topology
+#             INCOMPATIBLE: changes in TOML:
+#                 L3V4_IGNORE_HOSTS    -> L3_IGNORE_HOSTS
+#                 L3V4_IGNORE_IP       -> L3_IGNORE_IP
+#                 L3V4_SUMMARIZE       -> L3_SUMMARIZE
+#                 L3V4_REPLACE         -> L3_REPLACE
+#                 L3V4_IRNORE_WILDCARD -> L3V4_IGNORE_WILDCARD  # Typo
+#                 [EMBLEMS]
+#                 l3v4_replace         -> l3_replace
+#                 l3v4_summarize       -> l3_summarize
 
 # creating topology data json from inventory data
 #
@@ -243,7 +254,6 @@ __data = {
 """
 
 import sys
-from logging import DEBUG
 from time import strftime, time_ns
 from typing import List
 
@@ -255,37 +265,33 @@ from lib.backends import (
     HostCacheRestApi,
 )
 from lib.constants import (
+    DATAPATH,
     HOME_URL,
+    IPVersion,
+    LABEL_L3v4,
+    LAYERS,
     LOGGER,
+    Layer,
     NVDCT_VERSION,
-    DATAPATH,
 )
 from lib.settings import Settings
 from lib.topologies import (
-    NvConnections,
-    NvObjects,
-    create_l2_topology,
-    create_l3v4_topology,
-    create_static_connections,
+    TopologyL2,
+    TopologyL3,
+    TopologyStatic,
 )
 from lib.utils import (
     ExitCodes,
     InventoryColumns,
-    LAYERS,
-    Layer,
     StdoutQuiet,
     configure_logger,
     remove_old_data,
-    save_data_to_file,
 )
 
 
 def main():
     start_time = time_ns()
 
-    nv_connections = NvConnections()
-    nv_objects = NvObjects()
-
     settings: Settings = Settings(vars(parse_arguments()))
 
     sys.stdout = StdoutQuiet(quiet=settings.quiet)
@@ -329,36 +335,38 @@ def main():
         case _:
             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)
+            sys.exit(ExitCodes.BACKEND_NOT_IMPLEMENTED)
 
     jobs: List[Layer] = []
     pre_fetch_layers: List[str] = []
     pre_fetch_host_list: List[str] = []
 
     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)
-            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)
-            pre_fetch_layers.append(LAYERS[layer].host_label)
-
-        elif layer == 'CUSTOM':
-            for entry in settings.custom_layers:
-                jobs.append(entry)
-                host_cache.add_inventory_prefetch_path(entry.path)
+        match layer:
+            case 'STATIC':
+                jobs.append(layer)
+            case 'L3v4':
+                jobs.append(layer)
+                host_cache.add_inventory_path(path=LAYERS[layer].path)
+                pre_fetch_layers.append(LAYERS[layer].host_label)
+            case 'CUSTOM':
+                for entry in settings.custom_layers:
+                    jobs.append(entry)
+                    host_cache.add_inventory_path(entry.path)
+            case 'CDP' | 'LLDP':
+                jobs.append(LAYERS[layer])
+                host_cache.add_inventory_path(path=LAYERS[layer].path)
+                pre_fetch_layers.append(LAYERS[layer].host_label)
+            case _:
+                LOGGER.warning(f'Unknown layer {layer} dropped.')
+                continue
 
     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)
+        sys.exit(ExitCodes.NO_LAYER_CONFIGURED)
 
     if settings.pre_fetch:
         LOGGER.info('Pre fill cache...')
@@ -367,95 +375,81 @@ def main():
                 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 hosts: {len(pre_fetch_host_list)} of {len(host_cache.cache)}')
+        host_cache.fill_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)}')
 
     for job in jobs:
         match job:
             case 'STATIC':
-                label = 'STATIC'
-                create_static_connections(
-                    connections_=settings.static_connections,
+                label = job
+                topology = TopologyStatic(
+                    connections=settings.static_connections,
                     emblems=settings.emblems,
                     host_cache=host_cache,
-                    nv_objects=nv_objects,
-                    nv_connections=nv_connections,
                 )
+                topology.create()
+
             case 'L3v4':
-                label = 'L3v4'
-                create_l3v4_topology(
-                    ignore_hosts=settings.l3v4_ignore_hosts,
-                    ignore_ips=settings.l3v4_ignore_ips,
+                label = job
+                topology = TopologyL3(
+                    emblems=settings.emblems,
+                    host_cache=host_cache,
+                    ignore_hosts=settings.l3_ignore_hosts,
+                    ignore_ips=settings.l3_ignore_ips,
                     ignore_wildcard=settings.l3v4_ignore_wildcard,
                     include_hosts=settings.include_l3_hosts,
-                    replace=settings.l3v4_replace,
+                    replace=settings.l3_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,
+                    summarize=settings.l3_summarize,
+                    version=IPVersion.IPv4 if job == LABEL_L3v4 else IPVersion.IPv6
                 )
+                topology.create()
+
             case _:
                 label = job.label.upper()
                 columns = job.columns.split(',')
-                create_l2_topology(
-                    seed_devices=settings.l2_seed_devices,
-                    path_in_inventory=job.path,
+                topology = TopologyL2(
+                    case=settings.case,
+                    emblems=settings.emblems,
+                    host_cache=host_cache,
                     inv_columns=InventoryColumns(
                         neighbour=columns[0],
                         local_port=columns[1],
                         neighbour_port=columns[2]
                     ),
-                    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,
+                    label=label,
+                    path_in_inventory=job.path,
                     prefix=settings.prefix,
                     remove_domain=settings.remove_domain,
+                    seed_devices=settings.l2_seed_devices,
                 )
+                topology.create()
+
 
-        nv_connections.add_meta_data_to_connections(
-            nv_objects=nv_objects,
+        topology.nv_connections.add_meta_data_to_connections(
+            nv_objects=topology.nv_objects,
             speed_map=settings.map_speed_to_thickness,
         )
 
-        _data = {
-            'version': 1,
-            'name': label,
-            'objects': nv_objects.nv_objects if not settings.loglevel == DEBUG else dict(
-                sorted(nv_objects.nv_objects.items())
-            ),
-            'connections': nv_connections.nv_connections if not settings.loglevel == DEBUG else sorted(
-                nv_connections.nv_connections
-            )
-        }
-        save_data_to_file(
-            data=_data,
-            path=(
-                f'{DATAPATH}/{settings.output_directory}'
-            ),
-            file=f'data_{label}.json',
-            make_default=settings.default,
+        topology.save(
+            label=label,
+            output_directory=settings.output_directory,
+            make_default=settings.default
         )
 
         message = (
-            f'Layer {label:.<8s}: 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 {topology.nv_objects.host_count}/'
+            f'{len(topology.nv_objects.nv_objects)}/{len(topology.nv_connections.nv_connections)}'
         )
         LOGGER.info(msg=message)
         print(message)
 
-        nv_objects = NvObjects()
-        nv_connections = NvConnections()
-
     if settings.keep:
         remove_old_data(
             keep=settings.keep,
diff --git a/source/packages/nvdct b/source/packages/nvdct
index a41f73e..329c316 100644
--- a/source/packages/nvdct
+++ b/source/packages/nvdct
@@ -47,7 +47,7 @@
                    'htdocs/images/icons/location_80.png']},
  'name': 'nvdct',
  'title': 'Network Visualization Data Creation Tool (NVDCT)',
- 'version': '0.9.5-20241217',
+ 'version': '0.9.6-20241222',
  'version.min_required': '2.3.0b1',
  'version.packaged': 'cmk-mkp-tool 0.2.0',
  'version.usable_until': '2.4.0p1'}
-- 
GitLab