From 0e4dce1cc607cfd8d6e055663a9e0ea45f1ddf20 Mon Sep 17 00:00:00 2001
From: "th.l" <thl-cmk@outlook.com>
Date: Thu, 16 Nov 2023 21:15:07 +0100
Subject: [PATCH] update project

---
 README.md                                    |   2 +-
 bin/topology_data/create_topology_args.py    |  11 ++
 bin/topology_data/create_topology_classes.py | 160 ++++++++++++++++---
 bin/topology_data/create_topology_data.py    |  47 +++---
 bin/topology_data/create_topology_utils.py   |  41 +----
 mkp/create_topology_data-0.2.0-20231116.mkp  | Bin 0 -> 11625 bytes
 packages/create_topology_data                |   8 +-
 7 files changed, 188 insertions(+), 81 deletions(-)
 create mode 100644 mkp/create_topology_data-0.2.0-20231116.mkp

diff --git a/README.md b/README.md
index bdcad01..f903ebd 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[PACKAGE]: ../../raw/master/mkp/create_topology_data-0.1.0-20231028.mkp "create_topology_data-0.1.0-20231028.mkp"
+[PACKAGE]: ../../raw/master/mkp/create_topology_data-0.2.0-20231116.mkp "create_topology_data-0.2.0-20231116.mkp"
 Network Visualization data creation tool 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 fd8a801..af60d86 100755
--- a/bin/topology_data/create_topology_args.py
+++ b/bin/topology_data/create_topology_args.py
@@ -9,6 +9,7 @@
 
 #
 # options used
+# -b --backend
 # -d --default
 # -l --layer
 # -o --output-directory
@@ -59,6 +60,16 @@ def parse_arguments() -> arg_Namespace:
     )
     command_group = parser.add_mutually_exclusive_group()
 
+    parser.add_argument(
+        '-b', '--backend',
+        # nargs='+',
+        choices=['LIVESTATUS', 'FILESYSTEM', 'MULTISITE'],
+        default='LIVESTATUS',
+        help=f'Backend used to retrieve the topology data\n'
+             f'LIVESTATUS fetches the data only from the local site. In distributed environments use FILESYSTEM.\n'
+             f'For performance reasons LIVESTATUS is the default.',
+    )
+
     parser.add_argument(
         '-d', '--default', default=False, action='store_const', const=True,
         help='Set the created topology data as default',
diff --git a/bin/topology_data/create_topology_classes.py b/bin/topology_data/create_topology_classes.py
index 14631a5..2610ead 100755
--- a/bin/topology_data/create_topology_classes.py
+++ b/bin/topology_data/create_topology_classes.py
@@ -14,26 +14,27 @@ from pathlib import Path
 from time import strftime
 from typing import Dict, List, Any, NamedTuple
 from enum import Enum, unique
+from abc import abstractmethod
+from ast import literal_eval
+
 from create_topology_utils import (
     CREATE_TOPOLOGY_VERSION,
-    PATH_CDP,
-    PATH_LLDP,
     PATH_INTERFACES,
     USER_DATA_FILE,
-    LQ_INTERFACES,
+    CACHE_INTERFACES_ITEM,
     SCRIPT,
-    get_inventory_data,
+    get_data_form_live_status,
     get_table_from_inventory,
-    get_interface_items_from_lq,
     ExitCodes,
     get_data_from_toml,
+    OMD_ROOT,
 )
 
 
 @unique
-class CacheSources(Enum):
+class CacheItems(Enum):
     inventory = 'inventory'
-    lq = 'lq'
+    interfaces = 'interfaces'
 
 
 class InventoryColumns(NamedTuple):
@@ -59,9 +60,6 @@ class Settings:
         self.__topology_save_path = 'var/topology_data'
         self.__topology_file_name = 'network_data.json'
         self.__path_to_if_table = 'networking,interfaces'
-        # self.__inventory_path = 'var/check_mk/inventory'
-        # self.__autochecks_path = 'var/check_mk/autochecks'
-
         self.__settings = {
             'layers': [],
             'seed_devices': None,
@@ -78,6 +76,7 @@ class Settings:
             'check_user_data_only': False,
             'keep': None,
             'min_age': None,
+            'backend': 'LIVESTATUS',
         }
         # args in the form {'s, __seed_devices': 'CORE01', 'p, __path_in_inventory': None, ... }}
         # we will remove 's, __'
@@ -99,6 +98,10 @@ class Settings:
                 print(f'-l/--layers options must be unique. Don~\'t use any layer more than once.')
                 exit(code=ExitCodes.BAD_OPTION_LIST.value)
 
+    @property
+    def backend(self) -> str:
+        return self.__settings['backend']
+
     @property
     def version(self) -> bool:
         return self.__settings['version']
@@ -186,36 +189,151 @@ class HostCache:
     def __init__(self):
         self.__cache = {}
         self.__inventory_pre_fetch_list: List[str] = [
-            # PATH_CDP,
-            # PATH_LLDP,
             PATH_INTERFACES,
         ]
 
+    @abstractmethod
+    def get_inventory_data(self, host: str, debug: bool = False) -> Dict[str, str] | None:
+        """
+        Args:
+            host: the host name to return the inventory data for
+            debug: enable debug output
+
+        Returns:
+            the inventory data as dictionary
+        """
+
+    @abstractmethod
+    def get_interface_items(self, host: str, debug: bool = False) -> List:
+        """
+
+        Args:
+            host:  the host name to return the interface items
+            debug: enable debug output
+
+        Returns:
+            list of the interface items
+        """
+
     def __fill_cache(self, host: str):
         # pre fill inventory data
-        inventory = get_inventory_data(host=host)
+        inventory = self.get_inventory_data(host=host)
         if inventory:
-            self.__cache[host][CacheSources.inventory.value] = {}
-            self.__cache[host][CacheSources.inventory.value].update({
+            self.__cache[host][CacheItems.inventory.value] = {}
+            self.__cache[host][CacheItems.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)
+            self.__cache[host][CacheItems.inventory.value] = None
+        self.__cache[host][CacheItems.interfaces.value] = {}
+        self.__cache[host][CacheItems.interfaces.value][CACHE_INTERFACES_ITEM] = self.get_interface_items(host)
 
-    def get_data(self, host: str, source: CacheSources, path: str):
+    def get_data(self, host: str, item: CacheItems, 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]
+            return self.__cache[host][item.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]))
+
+
+class HostCacheLiveStatus(HostCache):
+    def get_inventory_data(self, 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)
+        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_interface_items(self, host: str, debug: bool = False) -> List:
+        """
+        Sample data from lq query, we keep only the item (description without "Interface" ).
+        [
+         ['C9540-7-1', 'Interface Vlan-999', 'check_mk-if64'],
+         ['C9540-7-1', 'Interface Vlan-998', 'check_mk-if64'],
+         ['C9540-7-1', 'Interface Vlan-997', 'check_mk-if64'],
+         ['C9540-7-1', 'Interface Vlan-996', 'check_mk-if64'],
+         ['C9540-7-1', 'Interface Vlan-8', 'check_mk-if64'],
+         ['C9540-7-1', 'Interface Te2/0/2', 'check_mk-if64'],
+         ['C9540-7-1', 'Interface Te2/0/30', 'check_mk-if64']
+         ]
+        Args:
+            host:
+            debug:
+
+        Returns:
+
+        """
+        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:
+            items.append(description[10:])  # remove 'Interface ' from description
+
+        if debug:
+            print(f'Interfaces items found: {len(items)} an host {host}')
+        return items
+
+
+class HostCacheFileSystem(HostCache):
+    def get_inventory_data(self, host: str, debug: bool = False) -> Dict[str, str] | None:
+        __inventory_path = 'var/check_mk/inventory'
+        inventory_file = Path(f'{OMD_ROOT}/{__inventory_path}/{host}')
+        if inventory_file.exists():
+            data = literal_eval(inventory_file.read_text())
+            return data
+        else:
+            return None
+
+    def get_interface_items(self, host: str, debug: bool = False) -> List:
+        """
+        Sample autochecks data we keep only the item
+        [
+         {'check_plugin_name': 'if64', 'item': 'Fa0', 'parameters': {'discovered_oper_status': ['2'], ...}},
+         {'check_plugin_name': 'if64', 'item': 'Fa1/0/1', 'parameters': {'discovered_oper_status': ['1'], ...}},
+         {'check_plugin_name': 'if64', 'item': 'Fa1/0/10', 'parameters': {'discovered_oper_status': ['2'], ...}},
+         {'check_plugin_name': 'if64', 'item': 'Fa1/0/11', 'parameters': {'discovered_oper_status': ['2'], ...}},
+         {'check_plugin_name': 'if64', 'item': 'Fa1/0/12', 'parameters': {'discovered_oper_status': ['1'], ...}}
+        ]
+
+        Args:
+            host:
+            debug:
+
+        Returns:
+
+        """
+        __autochecks_path = 'var/check_mk/autochecks'
+        autochecks_file = Path(f'{OMD_ROOT}/{__autochecks_path}/{host}.mk')
+        __data = []
+        if autochecks_file.exists():
+            data: List[Dict[str, str]] = literal_eval(autochecks_file.read_text())
+            for service in data:
+                if service['check_plugin_name'] in ['if64']:
+                    __data.append(service['item'])
+        else:
+            if debug:
+                print(f'Device: {host}: not found in auto checks path!')
+            return []
+
+        return __data
diff --git a/bin/topology_data/create_topology_data.py b/bin/topology_data/create_topology_data.py
index 86d43c5..f0b3578 100755
--- a/bin/topology_data/create_topology_data.py
+++ b/bin/topology_data/create_topology_data.py
@@ -34,6 +34,7 @@
 #             removed option --inventory-columns, now handled in CUSTOM_LAYERS
 #             removed option --path-in-inventory, now handled in CUSTOM_LAYERS
 #             removed lower limits for --keep and --min-age
+# 2023-1ß-29: fixed missing path from custom layer in host prefetch
 
 #
 # PoC for creating topology_data.json from inventory data
@@ -61,7 +62,7 @@ from create_topology_utils import (
     get_data_from_toml,
     merge_topologies,
     save_topology,
-    LQ_INTERFACES,
+    CACHE_INTERFACES_ITEM,
     PATH_INTERFACES,
     LAYERS,
 )
@@ -70,8 +71,9 @@ from create_topology_classes import (
     InventoryColumns,
     Settings,
     StaticConnection,
-    HostCache,
-    CacheSources,
+    HostCacheLiveStatus,
+    HostCacheFileSystem,
+    CacheItems,
 )
 
 HOST_MAP: Dict[str, str] = {}
@@ -89,7 +91,7 @@ def get_list_of_devices(data) -> List[str]:
 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')]
+        values = [_entry.get('name'), _entry.get('description'), _entry.get('alias')]
         for value in values:
             if value in items:
                 return value
@@ -112,24 +114,24 @@ def get_service_by_interface(host: str, interface: str, debug: bool = False) ->
         return interface
 
     # get list of interface items
-    items = HOST_CACHE.get_data(host=host, source=CacheSources.lq, path=LQ_INTERFACES)
+    items = HOST_CACHE.get_data(host=host, item=CacheItems.interfaces, path=CACHE_INTERFACES_ITEM)
 
     # the easy case
     if interface in items:
         return interface
     else:
         # try to find the interface in the host interface inventory list
-        inventory = HOST_CACHE.get_data(host=host, source=CacheSources.inventory, path=PATH_INTERFACES)
+        inventory = HOST_CACHE.get_data(host=host, item=CacheItems.inventory, path=PATH_INTERFACES)
         if inventory:
-            for entry in 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'),
+                    _entry.get('name'),
+                    _entry.get('description'),
+                    _entry.get('alias'),
+                    str(_entry.get('index')),
+                    _entry.get('phys_address'),
                 ]:
-                    return _match_entry_with_item(entry)
+                    return _match_entry_with_item(_entry)
         if debug:
             print(f'Device: {host}: service for interface {interface} not found')
 
@@ -242,7 +244,7 @@ def create_topology(
                 continue
 
         # 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)
+        topo_data = HOST_CACHE.get_data(host=device, item=CacheItems.inventory, path=path_in_inventory)
         if topo_data:
             topology_data.update(
                 create_device_from_inv(
@@ -252,9 +254,9 @@ def create_topology(
                     label=label,
                 ))
             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)
+            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)
@@ -270,7 +272,14 @@ def create_topology(
 if __name__ == '__main__':
     start_time = time_ns()
     SETTINGS = Settings(vars(parse_arguments()))
-    HOST_CACHE = HostCache()
+    match SETTINGS.backend:
+        case 'LIVESTATUS':
+            HOST_CACHE = HostCacheLiveStatus()
+        case 'FILESYSTEM':
+            HOST_CACHE = HostCacheFileSystem()
+        case _:
+            print(f'Backend {SETTINGS.backend} not (yet) implemented')
+            exit()
 
     print()
     print(f'Start time: {strftime(SETTINGS.time_format)}')
@@ -287,7 +296,7 @@ if __name__ == '__main__':
             jobs.append('STATIC')
         elif layer in LAYERS.keys():
             jobs.append(LAYERS[layer])
-            HOST_CACHE.add_inventory_prefetch_path(LAYERS[layer]['path'])
+            HOST_CACHE.add_inventory_prefetch_path(path=LAYERS[layer]['path'])
         elif layer == 'CUSTOM':
             for entry in CUSTOM_LAYERS:
                 jobs.append(entry)
diff --git a/bin/topology_data/create_topology_utils.py b/bin/topology_data/create_topology_utils.py
index cd0d023..a59bbde 100755
--- a/bin/topology_data/create_topology_utils.py
+++ b/bin/topology_data/create_topology_utils.py
@@ -39,15 +39,16 @@ 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'
+CACHE_INTERFACES_ITEM = 'interface_items'
 LAYERS = {
     'CDP': {'path': PATH_CDP, 'columns': COLUMNS_CDP, 'label': LABEL_CDP},
     'LLDP': {'path': PATH_LLDP, 'columns': COLUMNS_LLDP, 'label': LABEL_LLDP},
 }
+OMD_ROOT = environ["OMD_ROOT"]
 
 
 def get_data_form_live_status(query: str):
-    address = f'{environ.get("OMD_ROOT")}/tmp/run/live'
+    address = f'{OMD_ROOT}/tmp/run/live'
     family = socket.AF_INET if type(address) is tuple else socket.AF_UNIX
     sock = socket.socket(family, socket.SOCK_STREAM)
     sock.connect(address)
@@ -84,7 +85,7 @@ def get_data_from_toml(file: str, debug: bool = False) -> Dict:
 
 def rm_tree(root: Path):
     # safety
-    if not str(root).startswith(f'{environ["OMD_ROOT"]}/var/topology_data'):
+    if not str(root).startswith(f'{OMD_ROOT}/var/topology_data'):
         print(f"WARNING: bad path to remove, {str(root)}, don\'t delete it.")
         return
     for p in root.iterdir():
@@ -300,22 +301,6 @@ def is_equal_with_default(data: Dict, file: str) -> bool:
         return compare_dicts(data, default_data)
 
 
-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)
-
-    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()
@@ -325,21 +310,3 @@ def get_table_from_inventory(inventory: Dict[str, Any], path: List[str]) -> List
         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'
-    )
-    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.2.0-20231116.mkp b/mkp/create_topology_data-0.2.0-20231116.mkp
new file mode 100644
index 0000000000000000000000000000000000000000..3dfb59c892d676c4d87023841ca96f214fbbc05a
GIT binary patch
literal 11625
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

literal 0
HcmV?d00001

diff --git a/packages/create_topology_data b/packages/create_topology_data
index 1fc13fc..49be6c7 100644
--- a/packages/create_topology_data
+++ b/packages/create_topology_data
@@ -1,6 +1,5 @@
 {'author': 'Th.L. (thl-cmk[at]outlook[dot]com)',
- 'description': 'PoC for Network Visualization data creation from inventory '
-                'data\n'
+ 'description': 'Network Visualization data creation tool from inventory data\n'
                 '\n'
                 'This tool creates the topology data file needed for the '
                 'Checkmk "Network Visualization" plugin \n'
@@ -34,6 +33,9 @@
                 'LLDP: '
                 'https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/inventory/inv_lldp_cache\n'
                 '\n'
+                'For the latest version and documentation see:\n'
+                'https://thl-cmk.hopto.org/gitlab/checkmk/vendor-independent/create_topology_data\n'
+                '\n'
                 '#########################################################################\n'
                 'Important:\n'
                 '\n'
@@ -57,7 +59,7 @@
                    'topology_data/create_topology_args.py']},
  'name': 'create_topology_data',
  'title': 'Network Visualization data creation',
- 'version': '0.1.0-20231028',
+ 'version': '0.2.0-20231116',
  'version.min_required': '2.2.0p1',
  'version.packaged': '2.2.0p11',
  'version.usable_until': '2.3.0p1'}
-- 
GitLab