From 9976a268764c740f2c2919ff31d5728fb7c11a29 Mon Sep 17 00:00:00 2001
From: "th.l" <thl-cmk@outlook.com>
Date: Fri, 27 Oct 2023 20:08:16 +0200
Subject: [PATCH] update project

---
 README.md                                    |   2 +-
 bin/topology_data/create_topology_args.py    | 177 ++++++++++++++++
 bin/topology_data/create_topology_classes.py | 115 +++++++++--
 bin/topology_data/create_topology_data.py    | 199 +++++++-----------
 bin/topology_data/create_topology_data.toml  |  24 +--
 bin/topology_data/create_topology_utils.py   | 201 ++++++-------------
 mkp/create_topology_data-0.0.9-20231027.mkp  | Bin 0 -> 11296 bytes
 packages/create_topology_data                |   5 +-
 8 files changed, 418 insertions(+), 305 deletions(-)
 create mode 100755 bin/topology_data/create_topology_args.py
 mode change 100755 => 100644 bin/topology_data/create_topology_data.toml
 create mode 100644 mkp/create_topology_data-0.0.9-20231027.mkp

diff --git a/README.md b/README.md
index de840b4..fbb9884 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[PACKAGE]: ../../raw/master/mkp/create_topology_data-0.0.8-20231020.mkp "create_topology_data-0.0.8-20231020.mkp"
+[PACKAGE]: ../../raw/master/mkp/create_topology_data-0.0.9-20231027.mkp "create_topology_data-0.0.9-20231027.mkp"
 # PoC for Network Visualization data creation from inventory data
 
 This script creates the topology data file needed for the [Checkmk Exchange Network visualization](https://exchange.checkmk.com/p/network-visualization) plugin by Andreas Boesl and [schnetz](https://exchange.checkmk.com/u/schnetz).\
diff --git a/bin/topology_data/create_topology_args.py b/bin/topology_data/create_topology_args.py
new file mode 100755
index 0000000..0460d88
--- /dev/null
+++ b/bin/topology_data/create_topology_args.py
@@ -0,0 +1,177 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# License: GNU General Public License v2
+# Author: thl-cmk[at]outlook[dot]com
+# URL   : https://thl-cmk.hopto.org
+# Date  : 2023-10-12
+# File  : create_topology_utils.py
+
+#
+# options used
+# -d --default
+# -o --output-directory
+# -s --seed-devices
+# -u --user-data-file
+# -v --version
+# --check-user-data-only
+# --data-source
+# --debug
+# --dont-compare
+# --inventory-columns
+# --keep-domain
+# --keep
+# --lldp
+# --lowercase
+# --min-age
+# --path-in-inventory
+# --time-format
+# --uppercase
+
+from argparse import (
+    Namespace as arg_Namespace,
+    ArgumentParser,
+    RawTextHelpFormatter,
+)
+from create_topology_utils import(
+    CREATE_TOPOLOGY_VERSION,
+    SCRIPT,
+    SAMPLE_SEEDS,
+    USER_DATA_FILE,
+    LABEL_CDP,
+    COLUMNS_CDP,
+    LABEL_LLDP,
+    PATH_LLDP,
+    COLUMNS_LLDP,
+    PATH_CDP
+)
+
+
+def parse_arguments() -> arg_Namespace:
+    parser = ArgumentParser(
+        prog='create_topology_data.py',
+        description='This script creates the topology data file needed for the Checkmk "network_visualization"\n'
+                    'plugin by Andreas Boesl and schnetz. For more information see\n'
+                    'the announcement from schnetz: https://forum.checkmk.com/t/network-visualization/41680\n'
+                    'and the plugin on the Exchange: https://exchange.checkmk.com/p/network-visualization .\n'
+                    '\n'
+                    'The required inventory data can be created with my inventory plugins:\n'
+                    'CDP: https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_cdp_cache\n'
+                    'LLDP: https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_lldp_cache\n'
+                    '\n'
+                    f'\nVersion: {CREATE_TOPOLOGY_VERSION} | Written by: thl-cmk\n'
+                    f'for more information see: https://thl-cmk.hopto.org',
+        formatter_class=RawTextHelpFormatter,
+        epilog='Usage:\n'
+               'for CDP (the default):\n'
+               f'{SCRIPT} -s {SAMPLE_SEEDS} -d\n'
+               'for LLDP:\n'
+               f'{SCRIPT} -s {SAMPLE_SEEDS} -d --lldp\n',
+    )
+    command_group = parser.add_mutually_exclusive_group()
+
+    parser.add_argument(
+        '-d', '--default', default=False, action='store_const', const=True,
+        help='Set the created topology data as default',
+    )
+    parser.add_argument(
+        '-m', '--merge',
+        nargs=2,
+        choices=['CDP', 'LLDP'],
+        help=f'Merge topologies. This runs the topology creation for CDP and LLDP.\n'
+             f'The topologies are then merged in the specified order.\n'
+             f'I.e. -m CDP LLDP merges the CDP topology into the LLDP topology, overwriting\n'
+             f'the LLDP data in case there are conflicts.\n'
+             f'NOTE: static connection data from the user file will always merged with the\n'
+             f' highest priority',
+    )
+    parser.add_argument(
+        '-o', '--output-directory', type=str,
+        help='Directory name where to save the topology data.\n'
+             'I.e.: my_topology. Default is the actual date/time\n'
+             'in "--time-format" format.\n'
+             'NOTE: the directory is a sub directory under "~/var/topology_data/"',
+    )
+    parser.add_argument(
+        '-s', '--seed-devices', type=str, nargs='+',
+        help=f'List of devices to start the topology discovery from.\n'
+             f'I.e. {SAMPLE_SEEDS}',
+    )
+    parser.add_argument(
+        '-u', '--user-data-file', type=str,
+        help='Set the name uf the user provided data file\n'
+             'Default is ~local/bin/topology_data/create_topology_data.toml\n',
+    )
+    parser.add_argument(
+        '-v', '--version', default=False, action='store_const', const=True,
+        help='Print version of this script and exit',
+    )
+    parser.add_argument(
+        '--check-user-data-only', default=False, action='store_const', const=True,
+        help=f'Only tries to read/parse the user data from {USER_DATA_FILE} and exits.',
+    )
+    parser.add_argument(
+        '--data-source', type=str,
+        help=f'The source from which the topology data originates.\n'
+             f'I.e. {LABEL_CDP} for CDP data from the inventory.\n'
+             'NOTE: right now this only an unused label.',
+    )
+    parser.add_argument(
+        '--debug', default=False, action='store_const', const=True,
+        help='Print debug information',
+    )
+    parser.add_argument(
+        '--dont-compare', default=False, action='store_const', const=True,
+        help='Do not compare the actual topology data with the default topology\n'
+             'data. By default, the actual topology is compared with the default\n'
+             'topology. If the data matches, the actual topology is not saved.\n'
+             'So, if you run this tool in a cron job, a new topology will be\n'
+             'created only if there was a change, unless you use "--dont-compare".'
+    )
+    parser.add_argument(
+        '--inventory-columns', type=str,
+        help=f'Columns used from the inventory data.\n'
+             f'I.e. "{COLUMNS_CDP}"\n'
+             'NOTE: the columns must be in the order: neighbour, local_port,\n'
+             'neighbour_port',
+    )
+    parser.add_argument(
+        '--keep-domain', default=False, action='store_const', const=True,
+        help='Do not remove the domain name from the neighbor name',
+    )
+    parser.add_argument(
+        '--keep', type=int,
+        help=f'Number of topologies to keep. The oldest topologies above keep\n'
+             f'max will be deleted. The minimum value for --keep is 1.\n'
+             f'NOTE: The default topologies will be always kept.\n'
+    )
+    parser.add_argument(
+        '--lldp', default=False, action='store_const', const=True,
+        help=f'Sets data source to {LABEL_LLDP}, inventory path \n'
+             f'to "{PATH_LLDP}" and columns\n'
+             f'to "{COLUMNS_LLDP}"',
+    )
+    command_group.add_argument(
+        '--lowercase', default=False, action='store_const', const=True,
+        help='Change neighbour names to all lower case',
+    )
+    parser.add_argument(
+        '--min-age', type=int,
+        help=f'The minimum number of days before a topology is deleted\n'
+             f'by "--keep".\n'
+             f'NOTE: Topologies that are not older than 1 days are always kept.',
+    )
+    parser.add_argument(
+        '--path-in-inventory', type=str,
+        help=f'Checkmk inventory path to the topology data.\n'
+             f'I.e. "{PATH_CDP}"',
+    )
+    parser.add_argument(
+        '--time-format', type=str,
+        help='Format string to render the time. (default: %%Y-%%m-%%dT%%H:%%M:%%S.%%m)',
+    )
+    command_group.add_argument(
+        '--uppercase', default=False, action='store_const', const=True,
+        help='Change neighbour names to all upper case',
+    )
+    return parser.parse_args()
diff --git a/bin/topology_data/create_topology_classes.py b/bin/topology_data/create_topology_classes.py
index 145a84d..8bf30df 100755
--- a/bin/topology_data/create_topology_classes.py
+++ b/bin/topology_data/create_topology_classes.py
@@ -10,17 +10,27 @@
 
 
 from os import environ
+from pathlib import Path
 from time import strftime
 from typing import Dict, List, Any, NamedTuple
 from enum import Enum, unique
 from create_topology_utils import (
-    cdp_path,
-    cdp_columns,
-    cdp_label,
-    lldp_path,
-    lldp_columns,
-    lldp_label,
-    user_data_file,
+    CREATE_TOPOLOGY_VERSION,
+    PATH_CDP,
+    COLUMNS_CDP,
+    LABEL_CDP,
+    PATH_LLDP,
+    COLUMNS_LLDP,
+    LABEL_LLDP,
+    PATH_INTERFACES,
+    USER_DATA_FILE,
+    LQ_INTERFACES,
+    SCRIPT,
+    get_inventory_data,
+    get_table_from_inventory,
+    get_interface_items_from_lq,
+    ExitCodes,
+    get_data_from_toml,
 )
 
 
@@ -32,8 +42,14 @@ class TopologyParams(NamedTuple):
 
 @unique
 class Topologies(Enum):
-    CDP = TopologyParams(path=cdp_path, columns=cdp_columns, label=cdp_label)
-    LLDP = TopologyParams(path=lldp_path, columns=lldp_columns, label=lldp_label)
+    CDP = TopologyParams(path=PATH_CDP, columns=COLUMNS_CDP, label=LABEL_CDP)
+    LLDP = TopologyParams(path=PATH_LLDP, columns=COLUMNS_LLDP, label=LABEL_LLDP)
+
+
+@unique
+class CacheSources(Enum):
+    inventory = 'inventory'
+    lq = 'lq'
 
 
 class InventoryColumns(NamedTuple):
@@ -72,11 +88,11 @@ class Settings:
 
         self.__settings = {
             'seed_devices': None,
-            'path_in_inventory': cdp_path,
-            'inventory_columns': cdp_columns,
-            'data_source': cdp_label,
+            'path_in_inventory': PATH_CDP,
+            'inventory_columns': COLUMNS_CDP,
+            'data_source': LABEL_CDP,
             'time_format': '%Y-%m-%dT%H:%M:%S.%m',
-            'user_data_file': f'{self.__omd_root}/local/bin/topology_data/{user_data_file}',
+            'user_data_file': f'{self.__omd_root}/local/bin/topology_data/{USER_DATA_FILE}',
             'merge': None,
             'output_directory': None,
             'default': False,
@@ -96,16 +112,31 @@ class Settings:
         self.__args = ({k.split(',')[-1].strip(' ').strip('_'): v for k, v in cli_args.items() if v})
         # first set values
         if self.__args.get('lldp'):
-            self.__settings['data_source'] = lldp_label
-            self.__settings['path_in_inventory'] = lldp_path
-            self.__settings['inventory_columns'] = lldp_columns
+            self.__settings['data_source'] = LABEL_LLDP
+            self.__settings['path_in_inventory'] = PATH_LLDP
+            self.__settings['inventory_columns'] = COLUMNS_LLDP
         # Then update values with cli values
         self.__settings.update(self.__args)
-        if not self.merge:
-            if not self.lldp:
-                self.merge = ['CDP']
-            else:
+
+        if self.version:
+            print(f'{Path(SCRIPT).name} version: {CREATE_TOPOLOGY_VERSION}')
+            exit(code=ExitCodes.OK.value)
+
+        if self.check_user_data_only:
+            user_data = get_data_from_toml(file=self.user_data_file)
+            print(f'Could read/parse the user data from {self.user_data_file}')
+            exit(code=ExitCodes.OK.value)
+
+        if self.merge:
+            _merge = list(set(self.merge.copy()))
+            if len(_merge) != len(self.merge):
+                print(f'-m/--merge options must be unique. Use "-m CDP LLDP" oder "-m LLDP CDP"')
+                exit(code=ExitCodes.BAD_OPTION_LIST.value)
+        else:
+            if self.lldp:
                 self.merge = ['LLDP']
+            else:
+                self.merge = ['CDP']
 
     def set_topology_param(self, topology: TopologyParams):
         self.__settings['path_in_inventory'] = topology.path
@@ -199,8 +230,9 @@ class Settings:
 
     @property
     def path_in_inventory(self) -> List[str]:
-        path = ('Nodes,' + ',Nodes,'.join(self.__settings['path_in_inventory'].split(',')) + ',Table,Rows').split(',')
-        return path
+        # path = ('Nodes,' + ',Nodes,'.join(self.__settings['path_in_inventory'].split(',')) + ',Table,Rows').split(',')
+        # return path
+        return self.__settings['path_in_inventory']
 
     @property
     def path_to_if_table(self) -> List[str]:
@@ -233,3 +265,42 @@ class Settings:
             return f'{strftime(self.__settings["time_format"])}'
         else:
             return self.__settings['output_directory']
+
+
+class HostCache:
+    def __init__(self):
+        self.__cache = {}
+        self.__inventory_pre_fetch_list: List[str] = [
+            PATH_CDP,
+            PATH_LLDP,
+            PATH_INTERFACES,
+        ]
+
+    def __fill_cache(self, host: str):
+        # pre fill inventory data
+        inventory = get_inventory_data(host=host)
+        if inventory:
+            self.__cache[host][CacheSources.inventory.value] = {}
+            self.__cache[host][CacheSources.inventory.value].update({
+                entry: get_table_from_inventory(
+                    inventory=inventory,
+                    path=entry
+                ) for entry in self.__inventory_pre_fetch_list
+            })
+        else:
+            self.__cache[host][CacheSources.inventory.value] = None
+        # prefill live status data
+        self.__cache[host][CacheSources.lq.value] = {}
+        self.__cache[host][CacheSources.lq.value][LQ_INTERFACES] = get_interface_items_from_lq(host)
+
+    def get_data(self, host: str, source: CacheSources, path: str):
+        if host not in self.__cache.keys():
+            self.__cache[host]: Dict[str, Any] = {}
+            self.__fill_cache(host=host)
+        try:
+            return self.__cache[host][source.value][path]
+        except (KeyError, TypeError) as e:
+            return None
+
+    def add_inventory_prefetch_path(self, path: str):
+        self.__inventory_pre_fetch_list = list(set(self.__inventory_pre_fetch_list + [path]))
diff --git a/bin/topology_data/create_topology_data.py b/bin/topology_data/create_topology_data.py
index c47adbc..cded083 100755
--- a/bin/topology_data/create_topology_data.py
+++ b/bin/topology_data/create_topology_data.py
@@ -23,6 +23,8 @@
 #             added option -u, --user-data-file to provide a user specific data file
 # 2023-10-20: changed option -m/--make-default to -d/--default (-m is needed for -m/--merge
 #             added -m/--merge option
+# 2023-10-27: improved matching of port name from CDP/LLDP data to interface (item) name of service
+#             improved internal data handling (HostCache)
 
 #
 # PoC for creating topology_data.json from inventory data
@@ -37,78 +39,39 @@
 # NOTE: the topology_data configuration (layout etc.) is saved under ~/var/check_mk/topology
 #
 # The inventory data could be created with my CDP/LLDP inventory plugins:
-#
 # CDP: https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_cdp_cache\
-# CDP path-in-inventory: 'networking,cdp_cache'
-# CDP inventory-columns: 'device_id,local_port,device_port'
-#
 # LLDP: https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_lldp_cache\
-# LLDP path-in-inventory: 'networking,lldp_cache'
-# LLDP inventory-columns: 'system_name,local_port_num,port_id'
 #
-# USAGE CDP (is the default):
-# ~/local/lib/topology_data/create_topology_data.py -s CORE01 -m
-#
-# USAGE LLDP:
-# ~/local/lib/topology_data/create_topology_data.py -s CORE01 -m --lldp
+# USAGE:
+# ~/local/lib/topology_data/create_topology_data.py --help
 #
 
-from ast import literal_eval
-from pathlib import Path
 from typing import Dict, List, Any
 from time import strftime, time_ns
 
 from create_topology_utils import (
-    parse_arguments,
     remove_old_data,
     get_data_from_toml,
     merge_topologies,
     save_topology,
-    get_data_form_live_status,
-    ExitCodes,
+    LQ_INTERFACES,
+    PATH_INTERFACES,
 )
+from create_topology_args import parse_arguments
 from create_topology_classes import (
     InventoryColumns,
     Settings,
     StaticConnection,
     Topologies,
-    TopologyParams,
+    HostCache,
+    CacheSources,
 )
 
-
-CREATE_TOPOLOGY_VERSION = '0.0.8-202310120'
-
-ITEMS = {}
 HOST_MAP: Dict[str, str] = {}
 DROP_HOSTS: List[str] = []
 STATIC_CONNECTIONS: Dict = {}
 
 
-def get_interface_items_from_lq(host: str, debug: bool = False) -> Dict:
-    query = (
-        'GET services\n'
-        'Columns: host_name description check_command\n'
-        'Filter: description ~ Interface\n'
-        f'Filter: host_name = {host}\n'
-        'OutputFormat: python3\n'
-    )
-    data = get_data_form_live_status(query=query)
-    items = {}
-    for host, description, check_command in data:
-        if host:
-            if host not in items.keys():
-                items[host] = []
-            items[host].append(description[10:])  # remove 'Interface ' from description
-
-    interfaces = 0
-    for host in items.keys():
-        items[host] = list(set(items[host]))
-        items[host].sort()
-        interfaces += len(items[host])
-    # print(f'Interfaces found: {interfaces}')
-    return items
-
-
 def get_list_of_devices(data) -> List[str]:
     devices = []
     for connection in data.values():
@@ -116,66 +79,64 @@ def get_list_of_devices(data) -> List[str]:
     return list(set(devices))
 
 
-def get_service_by_interface(host: str, interface: str, debug: bool = False) -> str | None:
+def get_service_by_interface(host: str, interface: str, debug: bool = False) -> str:
+    # try to find the item for an interface
+    def _match_entry_with_item(_entry: Dict[str, str]):
+        values = [_entry.get('name'), _entry.get('description'), entry.get('alias')]
+        for value in values:
+            if value in items:
+                return value
+
+        index = str(_entry.get('index'))
+        # for index try with padding
+        for i in range(1, 6):
+            index_padded = f'{index:0>{i}}'
+            if index_padded in items:
+                return index_padded
+            # still not found try values + index
+            for value in values:
+                if f'{value} {index_padded}' in items:
+                    return f'{value} {index_padded}'
+
+        return interface
+
     # empty host/neighbour should never happen here
     if not host:
         return interface
-    items = ITEMS.get(host, [])
 
+    # get list of interface items
+    items = HOST_CACHE.get_data(host=host, source=CacheSources.lq, path=LQ_INTERFACES)
+
+    # the easy case
     if interface in items:
         return interface
     else:
-        if_padding = interface
-        if '/' in interface:
-            if_padding = '/'.join(interface.split('/')[:-1]) + f'/{interface.split("/")[-1]:0>2}'
-
-        for item in items:
-            # interface = Gi0/1
-            # item = Gi0/1 - Access port
-            # this might not catch the correct interface (fa0 and fa0/0)
-            item = item.split(' ')[0]
-            if item == interface:
-                return item
-            elif item == if_padding:
-                return item
-
+        # try to find the interface in the host interface inventory list
+        inventory = HOST_CACHE.get_data(host=host, source=CacheSources.inventory, path=PATH_INTERFACES)
+        if inventory:
+            for entry in inventory:
+                if interface in [
+                    entry.get('name'),
+                    entry.get('description'),
+                    entry.get('alias'),
+                    str(entry.get('index')),
+                    entry.get('phys_address'),
+                ]:
+                    return _match_entry_with_item(entry)
         if debug:
-            print(f'Device: {host}: interface ({interface}) not found in services')
+            print(f'Device: {host}: service for interface {interface} not found')
 
         return interface
 
 
-def get_inventory_data_lq(host: str, path: List[str], debug: bool = False, ) -> List[Dict[str, str]]:
-    query = f'GET hosts\nColumns: mk_inventory\nOutputFormat: python3\nFilter: host_name = {host}\n'
-    data = get_data_form_live_status(query=query)
-
-    if data:
-        try:
-            data = literal_eval(data[0][0].decode('utf-8'))
-        except SyntaxError as e:
-            if debug:
-                print(f'data: |{data}|')
-                print(f'type: {type(data)}')
-                print(f'exception: {e}')
-            return []
-        for m in path:
-            try:
-                data = data[m]
-            except KeyError:
-                return []
-        return data
-    return []
-
-
 def create_device_from_inv(
         host: str,
         inv_data: List[Dict[str, str]],
         inv_columns: InventoryColumns,
         data_source: str,
+        debug: bool = False,
 ) -> Dict[str, Any] | None:
     data = {'connections': {}, "interfaces": []}
-    if not host in ITEMS.keys():
-        ITEMS.update(get_interface_items_from_lq(host=host))
     for topo_neighbour in inv_data:
         neighbour = topo_neighbour.get(inv_columns.neighbour)
         if not neighbour:
@@ -194,18 +155,18 @@ def create_device_from_inv(
         if neighbour in HOST_MAP.keys():
             neighbour = HOST_MAP[neighbour]
 
-        # has to be done before checking interfaces
-        if not neighbour in ITEMS.keys():
-            ITEMS.update(get_interface_items_from_lq(host=neighbour))
-
         # getting/checking interfaces
         local_port = topo_neighbour.get(inv_columns.local_port)
-
-        local_port = get_service_by_interface(host, local_port)
+        _local_port = local_port
+        local_port = get_service_by_interface(host, local_port, debug=debug)
+        if debug and local_port != _local_port:
+            print(f'host: {host}, local_port: {local_port}, _local_port: {_local_port}')
 
         neighbour_port = topo_neighbour.get(inv_columns.neighbour_port)
-
-        neighbour_port = get_service_by_interface(neighbour, neighbour_port)
+        _neighbour_port = neighbour_port
+        neighbour_port = get_service_by_interface(neighbour, neighbour_port, debug=debug)
+        if debug and neighbour_port != _neighbour_port:
+            print(f'neighbour: {neighbour}, neighbour_port {neighbour_port}, _neighbour_port {_neighbour_port}')
 
         if neighbour and local_port and neighbour_port:
             data['connections'].update({local_port: [neighbour, neighbour_port, data_source]})
@@ -272,18 +233,20 @@ def create_topology(
             if device in devices_done:
                 continue
 
-        topo_data = get_inventory_data_lq(host=device, path=path_in_inventory, debug=debug)
-        topology_data.update(
-            create_device_from_inv(
-                host=device,
-                inv_data=topo_data,
-                inv_columns=inv_columns,
-                data_source=data_source,
-            ))
-        devices_list = get_list_of_devices(topology_data[device]['connections'])
-        for entry in devices_list:
-            if entry not in devices_done:
-                devices_to_go.append(entry)
+        # topo_data = get_inventory_data_lq(host=device, path=path_in_inventory, debug=debug)
+        topo_data = HOST_CACHE.get_data(host=device, source=CacheSources.inventory, path=path_in_inventory)
+        if topo_data:
+            topology_data.update(
+                create_device_from_inv(
+                    host=device,
+                    inv_data=topo_data,
+                    inv_columns=inv_columns,
+                    data_source=data_source,
+                ))
+            devices_list = get_list_of_devices(topology_data[device]['connections'])
+            for entry in devices_list:
+                if entry not in devices_done:
+                    devices_to_go.append(entry)
 
         devices_to_go = list(set(devices_to_go))
         devices_done.append(device)
@@ -297,26 +260,12 @@ def create_topology(
 
 
 if __name__ == '__main__':
-    SETTINGS = Settings(vars(parse_arguments(CREATE_TOPOLOGY_VERSION)))
-
-    if SETTINGS.version:
-        print(f'{Path(__file__).name} version: {CREATE_TOPOLOGY_VERSION}')
-        exit(code=ExitCodes.ok)
-
-    if SETTINGS.check_user_data_only:
-        user_data = get_data_from_toml(file=SETTINGS.user_data_file)
-        print(f'Could read/parse the user data from {SETTINGS.user_data_file}')
-        exit(code=ExitCodes.OK)
-
-    if SETTINGS.merge:
-        _merge = list(set(SETTINGS.merge.copy()))
-        if len(_merge) != len(SETTINGS.merge):
-            print(f'-m/--merge options must be unique. Use "-m cdp lldp" oder "-m lldp cdp"')
-            exit(code=ExitCodes.BAD_OPTION_LIST)
+    start_time = time_ns()
+    SETTINGS = Settings(vars(parse_arguments()))
+    HOST_CACHE = HostCache()
 
     print()
     print(f'Start time: {strftime(SETTINGS.time_format)}')
-    start_time = time_ns()
 
     user_data = get_data_from_toml(file=SETTINGS.user_data_file)
     HOST_MAP = user_data.get('HOST_MAP', {})
@@ -353,7 +302,7 @@ if __name__ == '__main__':
         remove_old_data(
             keep=SETTINGS.keep,
             min_age=SETTINGS.min_age,
-            path=Path(f'{SETTINGS.omd_root}/{SETTINGS.topology_save_path}'),
+            path=f'{SETTINGS.omd_root}/{SETTINGS.topology_save_path}',
             protected=user_data.get('PROTECTED_TOPOLOGIES', []),
         )
 
diff --git a/bin/topology_data/create_topology_data.toml b/bin/topology_data/create_topology_data.toml
old mode 100755
new mode 100644
index 5e31d3d..9e1c7c8
--- a/bin/topology_data/create_topology_data.toml
+++ b/bin/topology_data/create_topology_data.toml
@@ -14,33 +14,33 @@
 
 # list of (additional to -s/--seed-devices) seed devices
 SEED_DEVICES = [
-    "CORE01",
-    "LOCATION01",
-    "LOCATION02",
+    # "CORE01",
+    # "LOCATION01",
+    # "LOCATION02",
 ]
 
 # drop neighbours with invalid names
 DROP_HOSTS = [
-    "not advertised",
-     "a nother invalid name",
+    # "not advertised",
+    #  "a nother invalid name",
 ]
 
 # topologies will not be deleted by "--keep"
 PROTECTED_TOPOLOGIES = [
-    "2023-10-17T14:08:05.10",
-    "your_important_topology"
+    # "2023-10-17T14:08:05.10",
+    # "your_important_topology"
 ]
 
 # user defined static connections
 # connections will be added from host to neighbour and in reverese
 # hosts/neighbours in this section will be added to SEED_DEVICES
 STATIC_CONNECTIONS = [
-    ["cmk_host1", "local-port1", "neighbour-port1", "neighbour1", "label"],
-    ["cmk_host1", "local-port2", "neighbour-port2", "neighbour2", "label"],
+    # ["cmk_host1", "local-port1", "neighbour-port1", "neighbour1", "label"],
+    # ["cmk_host1", "local-port2", "neighbour-port2", "neighbour2", "label"],
 ]
 
 # map inventory neighbour name to Checkmk host name
 [HOST_MAP]
-inventory_neighbour1 = "cmk_host1"
-inventory_neighbour2 = "cmk_host2"
-inventory_neighbour3 = "cmk_host3"
+# inventory_neighbour1 = "cmk_host1"
+# inventory_neighbour2 = "cmk_host2"
+# inventory_neighbour3 = "cmk_host3"
diff --git a/bin/topology_data/create_topology_utils.py b/bin/topology_data/create_topology_utils.py
index bf6776f..ab4bda8 100755
--- a/bin/topology_data/create_topology_utils.py
+++ b/bin/topology_data/create_topology_utils.py
@@ -37,12 +37,7 @@ from tomllib import loads as toml_loads
 from tomllib import TOMLDecodeError
 from re import match as re_match
 from pathlib import Path
-from typing import List, Dict
-from argparse import (
-    Namespace as arg_Namespace,
-    ArgumentParser,
-    RawTextHelpFormatter,
-)
+from typing import List, Dict, Any
 from enum import Enum, unique
 
 
@@ -53,15 +48,19 @@ class ExitCodes(Enum):
     BAD_TOML_FORMAT = 3
 
 
-script = '~/local/bin/network-topology/create_topology_data.py'
-sample_seeds = 'Core01 Core02'
-cdp_path = 'networking,cdp_cache'
-cdp_columns = 'device_id,local_port,device_port'
-cdp_label = 'inv_CDP'
-lldp_path = 'networking,lldp_cache'
-lldp_columns = 'system_name,local_port_num,port_id'
-lldp_label = 'inv_LLDP'
-user_data_file = 'create_topology_data.toml'
+# constants
+CREATE_TOPOLOGY_VERSION = '0.0.9-202310127'
+SCRIPT = '~/local/bin/network-topology/create_topology_data.py'
+SAMPLE_SEEDS = 'Core01 Core02'
+PATH_CDP = 'networking,cdp_cache'
+PATH_LLDP = 'networking,lldp_cache'
+PATH_INTERFACES = 'networking,interfaces'
+LABEL_CDP = 'inv_CDP'
+LABEL_LLDP = 'inv_LLDP'
+COLUMNS_LLDP = 'system_name,local_port_num,port_id'
+COLUMNS_CDP = 'device_id,local_port,device_port'
+USER_DATA_FILE = 'create_topology_data.toml'
+LQ_INTERFACES = 'interface_items'
 
 
 def get_data_form_live_status(query: str):
@@ -91,7 +90,7 @@ def get_data_from_toml(file: str, debug: bool = False) -> Dict:
         except TOMLDecodeError as e:
             print(
                 f'ERROR: data file {toml_file} is not in valid TOML format! ({e}), (see https://toml.io/en/)')
-            exit(code=ExitCodes.BAD_TOML_FORMAT)
+            exit(code=ExitCodes.BAD_TOML_FORMAT.value)
     else:
         print(f'WARNING: User data {file} not found.')
     if debug:
@@ -113,7 +112,8 @@ def rm_tree(root: Path):
     root.rmdir()
 
 
-def remove_old_data(keep: int, min_age: int, path: Path, protected: List[str], debug: bool = False):
+def remove_old_data(keep: int, min_age: int, path: str, protected: List[str], debug: bool = False):
+    path = Path(path)
     default_topo = path.joinpath('default')
     directories = [str(directory) for directory in list(path.iterdir())]
     # keep default top
@@ -317,131 +317,46 @@ def is_equal_with_default(data: Dict, file: str) -> bool:
         return compare_dicts(data, default_data)
 
 
-def parse_arguments(create_topology_version: str) -> arg_Namespace:
-    parser = ArgumentParser(
-        prog='create_topology_data.py',
-        description='This script creates the topology data file needed for the Checkmk "network_visualization"\n'
-                    'plugin by Andreas Boesl and schnetz. For more information see\n'
-                    'the announcement from schnetz: https://forum.checkmk.com/t/network-visualization/41680\n'
-                    'and the plugin on the Exchange: https://exchange.checkmk.com/p/network-visualization .\n'
-                    '\n'
-                    'The required inventory data can be created with my inventory plugins:\n'
-                    'CDP: https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_cdp_cache\n'
-                    'LLDP: https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_lldp_cache\n'
-                    '\n'
-                    f'\nVersion: {create_topology_version} | Written by: thl-cmk\n'
-                    f'for more information see: https://thl-cmk.hopto.org',
-        formatter_class=RawTextHelpFormatter,
-        epilog='Usage:\n'
-               'for CDP (the default):\n'
-               f'{script} -s {sample_seeds} -d\n'
-               'for LLDP:\n'
-               f'{script} -s {sample_seeds} -d --lldp\n',
-    )
-    command_group = parser.add_mutually_exclusive_group()
+def get_inventory_data(host: str, debug: bool = False, ) -> Dict[str, str] | None:
+    query = f'GET hosts\nColumns: mk_inventory\nOutputFormat: python3\nFilter: host_name = {host}\n'
+    data = get_data_form_live_status(query=query)
 
-    parser.add_argument(
-        '-d', '--default', default=False, action='store_const', const=True,
-        help='Set the created topology data as default',
-    )
-    parser.add_argument(
-        '-m', '--merge',
-        nargs=2,
-        choices=['CDP', 'LLDP'],
-        help=f'Merge topologies. This runs the topology creation for CDP and LLDP.\n'
-             f'The topologies are then merged in the specified order.\n'
-             f'I.e. -m CDP LLDP merges the CDP topology into the LLDP topology, overwriting\n'
-             f'the LLDP data in case there are conflicts.\n'
-             f'NOTE: static connection data from the user file will always merged with the\n'
-             f' highest priority',
-    )
-    parser.add_argument(
-        '-o', '--output-directory', type=str,
-        help='Directory name where to save the topology data.\n'
-             'I.e.: my_topology. Default is the actual date/time\n'
-             'in "--time-format" format.\n'
-             'NOTE: the directory is a sub directory under "~/var/topology_data/"',
-    )
-    parser.add_argument(
-        '-s', '--seed-devices', type=str, nargs='+',
-        help=f'List of devices to start the topology discovery from.\n'
-             f'I.e. {sample_seeds}',
-    )
-    parser.add_argument(
-        '-u', '--user-data-file', type=str,
-        help='Set the name uf the user provided data file\n'
-             'Default is ~local/bin/topology_data/create_topology_data.toml\n',
-    )
-    parser.add_argument(
-        '-v', '--version', default=False, action='store_const', const=True,
-        help='Print version of this script and exit',
-    )
-    parser.add_argument(
-        '--check-user-data-only', default=False, action='store_const', const=True,
-        help=f'Only tries to read/parse the user data from {user_data_file} and exits.',
-    )
-    parser.add_argument(
-        '--data-source', type=str,
-        help=f'The source from which the topology data originates.\n'
-             f'I.e. {cdp_label} for CDP data from the inventory.\n'
-             'NOTE: right now this only an unused label.',
-    )
-    parser.add_argument(
-        '--debug', default=False, action='store_const', const=True,
-        help='Print debug information',
-    )
-    parser.add_argument(
-        '--dont-compare', default=False, action='store_const', const=True,
-        help='Do not compare the actual topology data with the default topology\n'
-             'data. By default, the actual topology is compared with the default\n'
-             'topology. If the data matches, the actual topology is not saved.\n'
-             'So, if you run this tool in a cron job, a new topology will be\n'
-             'created only if there was a change, unless you use "--dont-compare".'
-    )
-    parser.add_argument(
-        '--inventory-columns', type=str,
-        help=f'Columns used from the inventory data.\n'
-             f'I.e. "{cdp_columns}"\n'
-             'NOTE: the columns must be in the order: neighbour, local_port,\n'
-             'neighbour_port',
-    )
-    parser.add_argument(
-        '--keep-domain', default=False, action='store_const', const=True,
-        help='Do not remove the domain name from the neighbor name',
-    )
-    parser.add_argument(
-        '--keep', type=int,
-        help=f'Number of topologies to keep. The oldest topologies above keep\n'
-             f'max will be deleted. The minimum value for --keep is 1.\n'
-             f'NOTE: The default topologies will be always kept.\n'
-    )
-    parser.add_argument(
-        '--lldp', default=False, action='store_const', const=True,
-        help=f'Sets data source to {lldp_label}, inventory path \n'
-             f'to "{lldp_path}" and columns\n'
-             f'to "{lldp_columns}"',
-    )
-    command_group.add_argument(
-        '--lowercase', default=False, action='store_const', const=True,
-        help='Change neighbour names to all lower case',
-    )
-    parser.add_argument(
-        '--min-age', type=int,
-        help=f'The minimum number of days before a topology is deleted\n'
-             f'by "--keep".\n'
-             f'NOTE: Topologies that are not older than 1 days are always kept.',
-    )
-    parser.add_argument(
-        '--path-in-inventory', type=str,
-        help=f'Checkmk inventory path to the topology data.\n'
-             f'I.e. "{cdp_path}"',
-    )
-    parser.add_argument(
-        '--time-format', type=str,
-        help='Format string to render the time. (default: %%Y-%%m-%%dT%%H:%%M:%%S.%%m)',
-    )
-    command_group.add_argument(
-        '--uppercase', default=False, action='store_const', const=True,
-        help='Change neighbour names to all upper case',
+    if data:
+        try:
+            data = literal_eval(data[0][0].decode('utf-8'))
+        except SyntaxError as e:
+            if debug:
+                print(f'data: |{data}|')
+                print(f'type: {type(data)}')
+                print(f'exception: {e}')
+            return
+        return data
+
+
+def get_table_from_inventory(inventory: Dict[str, Any], path: List[str]) -> List | None:
+    path = ('Nodes,' + ',Nodes,'.join(path.split(',')) + ',Table,Rows').split(',')
+    table = inventory.copy()
+    for m in path:
+        try:
+            table = table[m]
+        except KeyError:
+            return
+    return table
+
+
+def get_interface_items_from_lq(host: str, debug: bool = False) -> List:
+    query = (
+        'GET services\n'
+        'Columns: host_name description check_command\n'
+        'Filter: description ~ Interface\n'
+        f'Filter: host_name = {host}\n'
+        'OutputFormat: python3\n'
     )
-    return parser.parse_args()
+    data = get_data_form_live_status(query=query)
+    items = []
+    for host, description, check_command in data:
+        items.append(description[10:])  # remove 'Interface ' from description
+
+    if debug:
+        print(f'Interfaces items found: {len(items)} an host {host}')
+    return items
diff --git a/mkp/create_topology_data-0.0.9-20231027.mkp b/mkp/create_topology_data-0.0.9-20231027.mkp
new file mode 100644
index 0000000000000000000000000000000000000000..f47d7e12ba7db7499e18557f65dab4193738595a
GIT binary patch
literal 11296
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
Q{`z1Mqf9x3xPgH9A2Nn6YybcN

literal 0
HcmV?d00001

diff --git a/packages/create_topology_data b/packages/create_topology_data
index 85ce824..e181976 100644
--- a/packages/create_topology_data
+++ b/packages/create_topology_data
@@ -53,10 +53,11 @@
  'files': {'bin': ['topology_data/create_topology_classes.py',
                    'topology_data/create_topology_data.py',
                    'topology_data/create_topology_utils.py',
-                   'topology_data/create_topology_data.toml']},
+                   'topology_data/create_topology_data.toml',
+                   'topology_data/create_topology_args.py']},
  'name': 'create_topology_data',
  'title': 'Network Visualization data creation',
- 'version': '0.0.8-20231020',
+ 'version': '0.0.9-20231027',
  'version.min_required': '2.2.0p1',
  'version.packaged': '2.2.0p11',
  'version.usable_until': '2.3.0p1'}
-- 
GitLab