From 774904be961b694a40b94aad59fd49d56f2c3f62 Mon Sep 17 00:00:00 2001
From: "th.l" <thl-cmk@outlook.com>
Date: Sat, 2 Dec 2023 18:02:49 +0100
Subject: [PATCH] update project

---
 README.md                                    |   2 +-
 bin/topology_data/create_topology_args.py    |   5 +-
 bin/topology_data/create_topology_classes.py | 119 +++++++++++++++++--
 bin/topology_data/create_topology_data.py    |  24 ++--
 mkp/create_topology_data-0.4.0-20231130.mkp  | Bin 0 -> 13382 bytes
 packages/create_topology_data                |   2 +-
 6 files changed, 126 insertions(+), 26 deletions(-)
 create mode 100644 mkp/create_topology_data-0.4.0-20231130.mkp

diff --git a/README.md b/README.md
index 9bfcfca..e42d629 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[PACKAGE]: ../../raw/master/mkp/create_topology_data-0.3.0-20231128.mkp "create_topology_data-0.3.0-20231128.mkp"
+[PACKAGE]: ../../raw/master/mkp/create_topology_data-0.4.0-20231130.mkp "create_topology_data-0.4.0-20231130.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 8fde0d9..f0b696b 100755
--- a/bin/topology_data/create_topology_args.py
+++ b/bin/topology_data/create_topology_args.py
@@ -63,12 +63,13 @@ def parse_arguments() -> arg_Namespace:
     parser.add_argument(
         '-b', '--backend',
         # nargs='+',
-        choices=['LIVESTATUS', 'FILESYSTEM', 'MULTISITE'],
+        choices=['LIVESTATUS', 'FILESYSTEM', 'MULTISITE', 'RESTAPI'],
         default='LIVESTATUS',
         help=f'Backend used to retrieve the topology data\n'
              f'     LIVESTATUS is the default for performance reasons -> only local site\n'
              f'     FILESYSTEM fetches the data directly form the inventory files -> use in distributed environments\n'
-             f'     MULTISITE like LIVESTATUS but for distribute environments -> may become the default',
+             f'     MULTISITE like LIVESTATUS but for distribute environments -> may become the default\n'
+             f'     RESTAPI uses the CMK REST API.',
     )
 
     parser.add_argument(
diff --git a/bin/topology_data/create_topology_classes.py b/bin/topology_data/create_topology_classes.py
index 6b3a176..edbb2bc 100755
--- a/bin/topology_data/create_topology_classes.py
+++ b/bin/topology_data/create_topology_classes.py
@@ -11,11 +11,12 @@
 
 from os import environ
 from pathlib import Path
-from time import strftime
+from time import strftime, time_ns
 from typing import Dict, List, Any, NamedTuple
 from enum import Enum, unique
 from abc import abstractmethod
 from ast import literal_eval
+from requests import session
 import livestatus
 
 
@@ -193,6 +194,10 @@ class HostCache:
         self.__inventory_pre_fetch_list: List[str] = [
             PATH_INTERFACES,
         ]
+        self._count = 0
+        self._debug = debug
+        if debug:
+            print('init HOST_CACHE')
 
     @abstractmethod
     def get_inventory_data(self, host: str, debug: bool = False) -> Dict[str, str] | None:
@@ -221,7 +226,15 @@ class HostCache:
 
     def __fill_cache(self, host: str):
         # pre fill inventory data
+        if self._debug:
+            self._count += 1
+            _pre_query = time_ns()
+
         inventory = self.get_inventory_data(host=host)
+
+        if self._debug:
+            print(f'{(time_ns() - _pre_query) / 1e9}|{self._count:0>4}|inventory|{host}')
+
         if inventory:
             self.__cache[host][CacheItems.inventory.value] = {}
             self.__cache[host][CacheItems.inventory.value].update({
@@ -233,8 +246,16 @@ class HostCache:
         else:
             self.__cache[host][CacheItems.inventory.value] = None
         self.__cache[host][CacheItems.interfaces.value] = {}
+
+        if self._debug:
+            self._count += 1
+            _pre_query = time_ns()
+
         self.__cache[host][CacheItems.interfaces.value][CACHE_INTERFACES_ITEM] = self.get_interface_items(host)
 
+        if self._debug:
+            print(f'{(time_ns() - _pre_query) / 1e9}|{self._count:0>4}|items|{host}')
+
     def get_data(self, host: str, item: CacheItems, path: str):
         if host not in self.__cache.keys():
             self.__cache[host]: Dict[str, Any] = {}
@@ -262,6 +283,8 @@ class HostCacheLiveStatus(HostCache):
                     print(f'exception: {e}')
                 return
             return data
+        if debug:
+            print(f'Device: {host}: no inventory data found!')
 
     def get_interface_items(self, host: str, debug: bool = False) -> List:
         """
@@ -307,24 +330,27 @@ class HostCacheFileSystem(HostCache):
             data = literal_eval(inventory_file.read_text())
             return data
         else:
+            if debug:
+                print(f'Device: {host}: not found in inventory data path!')
             return None
 
     def get_interface_items(self, host: str, debug: bool = False) -> List:
         """
-        Sample autochecks data we keep only the item
+        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'], ...}}
+         {'check_plugin_name': 'if64', 'item': 'Fa0', 'parameters': {'discovered_oper_status': ['2'], ...}},\n
+         {'check_plugin_name': 'if64', 'item': 'Fa1/0/1', 'parameters': {'discovered_oper_status': ['1'], ...}},\n
+         {'check_plugin_name': 'if64', 'item': 'Fa1/0/10', 'parameters': {'discovered_oper_status': ['2'], ...}},\n
+         {'check_plugin_name': 'if64', 'item': 'Fa1/0/11', 'parameters': {'discovered_oper_status': ['2'], ...}},\n
+         {'check_plugin_name': 'if64', 'item': 'Fa1/0/12', 'parameters': {'discovered_oper_status': ['1'], ...}}\n
         ]
 
         Args:
-            host:
-            debug:
+            host: name of the host object in cmk to fetch the data for
+            debug: output debug information
 
         Returns:
+            List of interface service items
 
         """
         __autochecks_path = 'var/check_mk/autochecks'
@@ -348,12 +374,14 @@ class HostCacheMultiSite(HostCache):
         super().__init__(debug=debug)
         self.__sites = {}
         self.get_sites(debug=debug)
+        if debug:
+            print('Create livestatus connection(s)')
         self.__c = livestatus.MultiSiteConnection(self.__sites)
         # self.__c.set_prepend_site(False)  # is default
         # self.__c.parallelize = True   # is default
         self.__dead_sites = [site['site']['alias'] for site in self.__c.dead_sites().values()]
         if self.__dead_sites:
-            self.__dead_sites = ', '.join( self.__dead_sites)
+            self.__dead_sites = ', '.join(self.__dead_sites)
             print(f'WARNING: use of dead site(s) {self.__dead_sites} is disabled')
             self.__c.set_only_sites(self.__c.alive_sites())
 
@@ -416,7 +444,74 @@ class HostCacheMultiSite(HostCache):
         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}')
+            if items:
+                print(f'Interfaces items found: {len(items)} an host {host}')
+            else:
+                print(f'No Interfaces items found for host {host}')
         return items
+
+
+class HostCacheRestApi(HostCache):
+    def __init__(self, debug: bool = False):
+        super().__init__(debug=debug)
+        try:
+            self.__secret = Path(f'{OMD_ROOT}/var/check_mk/web/automation/automation.secret').read_text().strip('\n)')
+        except FileNotFoundError as e:
+            print(f'automation.secret not found, {e}')
+            exit()
+        self.__hostname = 'localhost'
+        self.__site = OMD_ROOT.split('/')[-1]
+        self.__api_url = f"http://{self.__hostname}/{self.__site}/check_mk/api/1.0"
+        self.__user = 'automation'
+        if debug:
+            print('Create REST API session')
+        self.__session = session()
+        self.__session.headers['Authorization'] = f"Bearer {self.__user} {self.__secret}"
+        self.__session.headers['Accept'] = 'application/json'
+
+    def get_inventory_data(self, host: str, debug: bool = False) -> Dict[str, str] | None:
+        # self._count += 1
+
+        __query = '{"op": "=", "left": "name", "right": "' + host + '"}'
+        resp = self.__session.get(
+            f"{self.__api_url}/domain-types/host/collections/all",
+            params={
+                "query": __query,
+                "columns": ['mk_inventory'],
+            },
+        )
+        if resp.status_code == 200:
+            # print(f'{resp.elapsed}|{self._count:0>4}|inventory|{host}')
+            try:
+                return resp.json()['value'][0]['extensions']['mk_inventory']
+            except IndexError:
+                return None
+        else:
+            if debug:
+                print(f'Device: {host}: no inventory data found!')
+            return None
+
+    def get_interface_items(self, host: str, debug: bool = False) -> List:
+        # self._count += 1
+
+        __query = '{"op": "~", "left": "description", "right": "Interface "}'
+        resp = self.__session.get(
+            f"{self.__api_url}/objects/host/NX01/collections/services",
+            params={
+                "query": __query,
+                "columns": ['host_name', 'description', 'check_command'],
+            },
+        )
+
+        if resp.status_code == 200:
+            # print(f'{resp.elapsed}|{self._count:0>4}|items|{host}')
+
+            items = [service['extensions']['description'][10:] for service in resp.json()['value']]
+            if debug:
+                print(f'Interfaces items found: {len(items)} an host {host}')
+            return items
+        else:
+            if debug:
+                print(f'No Interfaces items found for host {host}')
+            return []
diff --git a/bin/topology_data/create_topology_data.py b/bin/topology_data/create_topology_data.py
index 96198b1..445249d 100755
--- a/bin/topology_data/create_topology_data.py
+++ b/bin/topology_data/create_topology_data.py
@@ -39,6 +39,7 @@
 #             LIVESTATUS is the default for performance reasons -> for now only local site
 #             FILESYSTEM fetches the data directly form the inventory files -> use in distributed environments
 # 2023-11-28: implemented MULTISITE as backend, via livestatus -> use in distributed environments, my become the default
+# 2023-11-30: added RESTAPI backend --> this is kind of slow (20 seconds compared to 0.5 for MULTISITE)
 #
 # creating topology_data.json from inventory data
 #
@@ -77,6 +78,7 @@ from create_topology_classes import (
     HostCacheLiveStatus,
     HostCacheFileSystem,
     HostCacheMultiSite,
+    HostCacheRestApi,
     CacheItems,
 )
 
@@ -279,16 +281,18 @@ if __name__ == '__main__':
     print()
     print(f'Start time: {strftime(SETTINGS.time_format)}')
 
-    match SETTINGS.backend:
-        case 'LIVESTATUS':
-            HOST_CACHE = HostCacheLiveStatus()
-        case 'FILESYSTEM':
-            HOST_CACHE = HostCacheFileSystem()
-        case 'MULTISITE':
-            HOST_CACHE = HostCacheMultiSite(debug=SETTINGS.debug)
-        case _:
-            print(f'Backend {SETTINGS.backend} not (yet) implemented')
-            exit()
+    _backends = {
+        'LIVESTATUS': HostCacheLiveStatus,
+        'FILESYSTEM': HostCacheFileSystem,
+        'MULTISITE': HostCacheMultiSite,
+        'RESTAPI': HostCacheRestApi,
+    }
+    HOST_CACHE = _backends.get(SETTINGS.backend, None)
+    if not HOST_CACHE:
+        print(f'Backend {SETTINGS.backend} not (yet) implemented')
+        exit()
+    else:
+        HOST_CACHE = HOST_CACHE(debug=SETTINGS.debug)
 
     user_data = get_data_from_toml(file=SETTINGS.user_data_file)
     HOST_MAP = user_data.get('HOST_MAP', {})
diff --git a/mkp/create_topology_data-0.4.0-20231130.mkp b/mkp/create_topology_data-0.4.0-20231130.mkp
new file mode 100644
index 0000000000000000000000000000000000000000..47c80f9322aeac5082214ab6b980a24bd42fafff
GIT binary patch
literal 13382
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

literal 0
HcmV?d00001

diff --git a/packages/create_topology_data b/packages/create_topology_data
index 9c199bf..d37aa91 100644
--- a/packages/create_topology_data
+++ b/packages/create_topology_data
@@ -59,7 +59,7 @@
                    'topology_data/create_topology_args.py']},
  'name': 'create_topology_data',
  'title': 'Network Visualization data creation',
- 'version': '0.3.0-20231128',
+ 'version': '0.4.0-20231130',
  'version.min_required': '2.2.0p1',
  'version.packaged': '2.2.0p14',
  'version.usable_until': '2.3.0p1'}
-- 
GitLab