From 16cac35e878a629a58115c24d2855b31c5edd382 Mon Sep 17 00:00:00 2001
From: "th.l" <thl-cmk@outlook.com>
Date: Sat, 28 Oct 2023 17:21:55 +0200
Subject: [PATCH] update project

---
 README.md                                    |   2 +-
 bin/topology_data/create_topology_args.py    |  59 +++------------
 bin/topology_data/create_topology_classes.py |  73 +++++--------------
 bin/topology_data/create_topology_data.py    |  54 +++++++++-----
 bin/topology_data/create_topology_data.toml  |   6 ++
 bin/topology_data/create_topology_utils.py   |  27 ++-----
 mkp/create_topology_data-0.1.0-20231028.mkp  | Bin 0 -> 11078 bytes
 packages/create_topology_data                |   2 +-
 8 files changed, 76 insertions(+), 147 deletions(-)
 mode change 100644 => 100755 bin/topology_data/create_topology_data.toml
 create mode 100644 mkp/create_topology_data-0.1.0-20231028.mkp

diff --git a/README.md b/README.md
index 1730be2..1a04927 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[PACKAGE]: ../../raw/master/mkp/create_topology_data-0.0.9-20231027.mkp "create_topology_data-0.0.9-20231027.mkp"
+[PACKAGE]: ../../raw/master/mkp/create_topology_data-0.1.0-20231028.mkp "create_topology_data-0.1.0-20231028.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
index 0460d88..8ffb328 100755
--- a/bin/topology_data/create_topology_args.py
+++ b/bin/topology_data/create_topology_args.py
@@ -10,21 +10,18 @@
 #
 # options used
 # -d --default
+# -l --layer
 # -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
 
@@ -33,17 +30,11 @@ from argparse import (
     ArgumentParser,
     RawTextHelpFormatter,
 )
-from create_topology_utils import(
+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
 )
 
 
@@ -65,8 +56,6 @@ def parse_arguments() -> arg_Namespace:
         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()
 
@@ -74,17 +63,7 @@ def parse_arguments() -> arg_Namespace:
         '-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'
@@ -97,6 +76,14 @@ def parse_arguments() -> arg_Namespace:
         help=f'List of devices to start the topology discovery from.\n'
              f'I.e. {SAMPLE_SEEDS}',
     )
+    parser.add_argument(
+        '-l', '--layers',
+        nargs='+',
+        choices=['CDP', 'CUSTOM', 'LLDP', 'STATIC'],
+        default=['CDP', 'STATIC'],
+        help=f'Layers with least significant layer first. Listed layers\n'
+             f'will be merged automatically. Default is "-l CDP STATIC"',
+    )
     parser.add_argument(
         '-u', '--user-data-file', type=str,
         help='Set the name uf the user provided data file\n'
@@ -110,12 +97,6 @@ def parse_arguments() -> arg_Namespace:
         '--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',
@@ -128,13 +109,6 @@ def parse_arguments() -> arg_Namespace:
              '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',
@@ -145,12 +119,6 @@ def parse_arguments() -> arg_Namespace:
              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',
@@ -161,11 +129,6 @@ def parse_arguments() -> arg_Namespace:
              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)',
diff --git a/bin/topology_data/create_topology_classes.py b/bin/topology_data/create_topology_classes.py
index 8bf30df..f3d1794 100755
--- a/bin/topology_data/create_topology_classes.py
+++ b/bin/topology_data/create_topology_classes.py
@@ -20,8 +20,6 @@ from create_topology_utils import (
     COLUMNS_CDP,
     LABEL_CDP,
     PATH_LLDP,
-    COLUMNS_LLDP,
-    LABEL_LLDP,
     PATH_INTERFACES,
     USER_DATA_FILE,
     LQ_INTERFACES,
@@ -40,12 +38,6 @@ class TopologyParams(NamedTuple):
     label: str
 
 
-@unique
-class Topologies(Enum):
-    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'
@@ -87,20 +79,19 @@ class Settings:
         # self.__autochecks_path = 'var/check_mk/autochecks'
 
         self.__settings = {
+            'layers': [],
             'seed_devices': None,
             '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}',
-            'merge': None,
             'output_directory': None,
             'default': False,
             'keep_domain': False,
             'lowercase': False,
             'uppercase': False,
             'debug': False,
-            'lldp': False,
             'version': False,
             'dont_compare': False,
             'check_user_data_only': False,
@@ -110,12 +101,6 @@ class Settings:
         # args in the form {'s, __seed_devices': 'CORE01', 'p, __path_in_inventory': None, ... }}
         # we will remove 's, __'
         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'] = 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 self.version:
@@ -123,37 +108,28 @@ class Settings:
             exit(code=ExitCodes.OK.value)
 
         if self.check_user_data_only:
-            user_data = get_data_from_toml(file=self.user_data_file)
+            _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"')
+        if self.layers:
+            layers = list(set(self.layers))
+            if len(layers) != len(self.layers):
+                print(f'-l/--layers options must be unique. Don~\'t use any layer more than once.')
                 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
-        self.__settings['inventory_columns'] = topology.columns
-        self.__settings['data_source'] = topology.label
+    def set_topology_param(self, topology: Dict[str, str]):
+        self.__settings['path_in_inventory'] = topology['path']
+        self.__settings['inventory_columns'] = topology['columns']
+        self.__settings['data_source'] = topology['label']
 
     @property
     def version(self) -> bool:
         return self.__settings['version']
 
     @property
-    def merge(self) -> List[str] | None:
-        return self.__settings['merge']
-
-    @merge.setter
-    def merge(self, topologies: List[str]):
-        self.__settings['merge'] = topologies
+    def layers(self) -> List[str]:
+        return self.__settings['layers']
 
     @property
     def keep(self) -> int | None:
@@ -171,10 +147,6 @@ class Settings:
     def check_user_data_only(self) -> bool:
         return self.__settings['check_user_data_only']
 
-    @property
-    def lldp(self) -> bool:
-        return self.__settings['lldp']
-
     @property
     def debug(self) -> bool:
         return self.__settings['debug']
@@ -207,15 +179,6 @@ class Settings:
     def omd_root(self) -> str:
         return self.__omd_root
 
-    # replaced by live status query
-    # @property
-    # def inventory_path(self) -> str:
-    #     return self.__inventory_path
-
-    # @property
-    # def autochecks_path(self) -> str:
-    #     return self.__autochecks_path
-
     @property
     def user_data_file(self) -> str:
         return self.__settings['user_data_file']
@@ -230,14 +193,12 @@ 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
         return self.__settings['path_in_inventory']
 
-    @property
-    def path_to_if_table(self) -> List[str]:
-        path = ('Nodes,' + ',Nodes,'.join(self.__path_to_if_table.split(',')) + ',Table,Rows').split(',')
-        return path
+    # @property
+    # def path_to_if_table(self) -> List[str]:
+    #     path = ('Nodes,' + ',Nodes,'.join(self.__path_to_if_table.split(',')) + ',Table,Rows').split(',')
+    #     return path
 
     @property
     def inventory_columns(self) -> InventoryColumns:
@@ -299,7 +260,7 @@ class HostCache:
             self.__fill_cache(host=host)
         try:
             return self.__cache[host][source.value][path]
-        except (KeyError, TypeError) as e:
+        except (KeyError, TypeError) as _e:
             return None
 
     def add_inventory_prefetch_path(self, path: str):
diff --git a/bin/topology_data/create_topology_data.py b/bin/topology_data/create_topology_data.py
index cded083..70ce9bb 100755
--- a/bin/topology_data/create_topology_data.py
+++ b/bin/topology_data/create_topology_data.py
@@ -25,16 +25,22 @@
 #             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)
+# 2023-10-28: reworked handling of layers (see option -l/--layers)
+#             added option -l/-layers {CDP,CUSTOM,LLDP,STATIC} [{CDP,CUSTOM,LLDP,STATIC} ...]
+#             added section CUSTOM_LAYERS to user data file
+#             removed option -m/--merge, included in option -l/--layers
+#             removed option --lldp, included in option -l/--layers
+#             removed option --data-source, now handled in CUSTOM_LAYERS
+#             removed option --inventory-columns, now handled in CUSTOM_LAYERS
+#             removed option --path-in-inventory, now handled in CUSTOM_LAYERS
 
 #
 # PoC for creating topology_data.json from inventory data
 #
 # This script creates the topology data file needed for the Checkmk "network_visualization" plugin by
-# Andreas Boesl and schnetz. See
+# Andreas Boesl and schnetz. For more information see
 # https://forum.checkmk.com/t/network-visualization/41680
-# and
 # https://exchange.checkmk.com/p/network-visualization
-# for more information.
 #
 # NOTE: the topology_data configuration (layout etc.) is saved under ~/var/check_mk/topology
 #
@@ -56,13 +62,13 @@ from create_topology_utils import (
     save_topology,
     LQ_INTERFACES,
     PATH_INTERFACES,
+    LAYERS,
 )
 from create_topology_args import parse_arguments
 from create_topology_classes import (
     InventoryColumns,
     Settings,
     StaticConnection,
-    Topologies,
     HostCache,
     CacheSources,
 )
@@ -270,24 +276,34 @@ if __name__ == '__main__':
     user_data = get_data_from_toml(file=SETTINGS.user_data_file)
     HOST_MAP = user_data.get('HOST_MAP', {})
     DROP_HOSTS = user_data.get('DROP_HOSTS', [])
-
+    CUSTOM_LAYERS = user_data.get('CUSTOM_LAYERS', [])
+    jobs = []
     final_topology = {}
-    while SETTINGS.merge:
-        SETTINGS.set_topology_param(Topologies[SETTINGS.merge[-1]].value)
-        topology = create_topology(
-            seed_devices=list(set(SETTINGS.seed_devices + user_data.get('SEED_DEVICES', []))),
-            path_in_inventory=SETTINGS.path_in_inventory,
-            inv_columns=SETTINGS.inventory_columns,
-            data_source=SETTINGS.data_source,
-            debug=SETTINGS.debug,
-        )
+
+    for layer in SETTINGS.layers:
+        if layer == 'STATIC':
+            jobs.append('STATIC')
+        elif layer in LAYERS.keys():
+            jobs.append(LAYERS[layer])
+        elif layer == 'CUSTOM':
+            for entry in CUSTOM_LAYERS:
+                jobs.append(entry)
+
+    for job in jobs:
+        topology = {}
+        if job == 'STATIC':
+            topology = create_static_connections(connections=user_data.get('STATIC_CONNECTIONS', []))
+        else:
+            SETTINGS.set_topology_param(job)
+            topology = create_topology(
+                seed_devices=list(set(SETTINGS.seed_devices + user_data.get('SEED_DEVICES', []))),
+                path_in_inventory=SETTINGS.path_in_inventory,
+                inv_columns=SETTINGS.inventory_columns,
+                data_source=SETTINGS.data_source,
+                debug=SETTINGS.debug,
+            )
         if topology:
             final_topology = merge_topologies(topo_pri=topology, topo_sec=final_topology)
-        SETTINGS.merge.remove(SETTINGS.merge[-1])
-
-    STATIC_CONNECTIONS = create_static_connections(connections=user_data.get('STATIC_CONNECTIONS', []))
-
-    final_topology = merge_topologies(topo_pri=STATIC_CONNECTIONS, topo_sec=final_topology)
 
     save_topology(
         data=final_topology,
diff --git a/bin/topology_data/create_topology_data.toml b/bin/topology_data/create_topology_data.toml
old mode 100644
new mode 100755
index 9e1c7c8..bb5a2a0
--- a/bin/topology_data/create_topology_data.toml
+++ b/bin/topology_data/create_topology_data.toml
@@ -31,6 +31,12 @@ PROTECTED_TOPOLOGIES = [
     # "your_important_topology"
 ]
 
+# optional custom layers use option -l/--layers CUSTOM to include this layers
+CUSTOM_LAYERS = [
+    # { path = "networking,lldp_cache", columns = "system_name,local_port_num,port_id", label = "custom_LLDP" },
+    # { path = "networking,cdp_cache", columns = "device_id,local_port,device_port", label = "custom_CDP" },
+]
+
 # 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
diff --git a/bin/topology_data/create_topology_utils.py b/bin/topology_data/create_topology_utils.py
index ab4bda8..cd0d023 100755
--- a/bin/topology_data/create_topology_utils.py
+++ b/bin/topology_data/create_topology_utils.py
@@ -7,27 +7,6 @@
 # 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 os import environ
 from json import dumps
 import socket
@@ -49,7 +28,7 @@ class ExitCodes(Enum):
 
 
 # constants
-CREATE_TOPOLOGY_VERSION = '0.0.9-202310127'
+CREATE_TOPOLOGY_VERSION = '0.1.0-202310128'
 SCRIPT = '~/local/bin/network-topology/create_topology_data.py'
 SAMPLE_SEEDS = 'Core01 Core02'
 PATH_CDP = 'networking,cdp_cache'
@@ -61,6 +40,10 @@ 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'
+LAYERS = {
+    'CDP': {'path': PATH_CDP, 'columns': COLUMNS_CDP, 'label': LABEL_CDP},
+    'LLDP': {'path': PATH_LLDP, 'columns': COLUMNS_LLDP, 'label': LABEL_LLDP},
+}
 
 
 def get_data_form_live_status(query: str):
diff --git a/mkp/create_topology_data-0.1.0-20231028.mkp b/mkp/create_topology_data-0.1.0-20231028.mkp
new file mode 100644
index 0000000000000000000000000000000000000000..364cbc7b4d86abb72761f1b79cb1d2cbb89379c8
GIT binary patch
literal 11078
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

literal 0
HcmV?d00001

diff --git a/packages/create_topology_data b/packages/create_topology_data
index e181976..1fc13fc 100644
--- a/packages/create_topology_data
+++ b/packages/create_topology_data
@@ -57,7 +57,7 @@
                    'topology_data/create_topology_args.py']},
  'name': 'create_topology_data',
  'title': 'Network Visualization data creation',
- 'version': '0.0.9-20231027',
+ 'version': '0.1.0-20231028',
  'version.min_required': '2.2.0p1',
  'version.packaged': '2.2.0p11',
  'version.usable_until': '2.3.0p1'}
-- 
GitLab