From ab24d246a5aafa00c4e7597a40bb0aa40cdca793 Mon Sep 17 00:00:00 2001
From: "th.l" <thl-cmk@outlook.com>
Date: Wed, 11 Dec 2024 20:16:40 +0100
Subject: [PATCH] added host label nvdct/l3v6_topology:host and
 nvdct/l3v6_topology:router

---
 README.md                                     |   2 +-
 mkp/inv_ip_address-0.0.6-20241210.mkp         | Bin 0 -> 4126 bytes
 .../agent_based/inv_ip_addresses.py           |  86 +++++++++++++-----
 source/packages/inv_ip_address                |   2 +-
 source/web/plugins/views/inv_ip_addresses.py  |   5 +-
 5 files changed, 70 insertions(+), 25 deletions(-)
 create mode 100644 mkp/inv_ip_address-0.0.6-20241210.mkp

diff --git a/README.md b/README.md
index 0b3d2b2..97eea09 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[PACKAGE]: ../../raw/master/mkp/inv_ip_address-0.0.5-20241209.mkp "inv_ip_address-0.0.5-20241209.mkp"
+[PACKAGE]: ../../raw/master/mkp/inv_ip_address-0.0.6-20241210.mkp "inv_ip_address-0.0.6-20241210.mkp"
 # Inventory of IP addresses
 
 The plugin adds the IP-addresses information to the inventory for devices monitored via SNMP. The plugin supports IPv4 and IPv6.
diff --git a/mkp/inv_ip_address-0.0.6-20241210.mkp b/mkp/inv_ip_address-0.0.6-20241210.mkp
new file mode 100644
index 0000000000000000000000000000000000000000..ea80d563999a5f6224b77e4a24475640e770b585
GIT binary patch
literal 4126
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
CLlLh4

literal 0
HcmV?d00001

diff --git a/source/cmk_addons_plugins/inv_ip_address/agent_based/inv_ip_addresses.py b/source/cmk_addons_plugins/inv_ip_address/agent_based/inv_ip_addresses.py
index 92b6d60..7ccce20 100644
--- a/source/cmk_addons_plugins/inv_ip_address/agent_based/inv_ip_addresses.py
+++ b/source/cmk_addons_plugins/inv_ip_address/agent_based/inv_ip_addresses.py
@@ -17,8 +17,12 @@
 # 2024-12-03: added IP-MIB::ipAddressTable for IPv6 support
 #             incompatible: renamed to inv_ip_address -> remove inv_ipv4_address.mkp before updating
 # 2024-12-05  changed to use ip_interface
-# 2024-12-06: incompatible: changed hostlabel to nvdct/l3v4_topology:host and nvdct/l3v4_topology:router
+# 2024-12-06: incompatible: changed host label to nvdct/l3v4_topology:host and nvdct/l3v4_topology:router
 # 2024-12-09: rewritten for CMK checkAPI 2.0
+# 2024-12-10: fixed crash in host label function (AttributeError ('dict_values' object has no attribute 'version'))
+#             added support for ipv6z address type
+#             fixed duplicate ip information in section
+#             added host label nvdct/l3v6_topology:host and nvdct/l3v6_topology:router
 
 from collections.abc import Mapping, MutableSequence, Sequence
 from ipaddress import AddressValueError, NetmaskValueError, ip_interface
@@ -130,6 +134,9 @@ def parse_inv_ip_addresses(string_table: List[StringByteTable]) -> Section:
         except ValueError:
             continue
 
+        if oid_end.startswith('4.20.254.128.'):  # ipv6z, link local
+            ip_prefix = '64'
+
         if (prefix := ip_prefix.split('.')[-1]) == '0':  # drop entries without prefix (0) -> fortinet
             continue
 
@@ -156,21 +163,36 @@ def parse_inv_ip_addresses(string_table: List[StringByteTable]) -> Section:
                         )
                     case '39':
                         raw_address = ''.join([chr(int(x)) for x in raw_address.split('.')])
+            case '4':  # ipv6z
+                # [
+                #     ['4.20.254.128.0.0.0.0.0.0.1.146.1.104.0.16.1.65.18.0.0.2', [], '1', '.0.0'],
+                #     ['4.20.254.128.0.0.0.0.0.0.1.146.1.104.0.16.1.65.18.0.0.3', [], '2', '.0.0']
+                # ]
+                # IP-MIB::ipAddressIfIndex.ipv6z."fe:80:00:00:00:00:00:00:01:92:01:68:00:10:01:41%301989890" = INTEGER: 1
+                # IP-MIB::ipAddressIfIndex.ipv6z."fe:80:00:00:00:00:00:00:01:92:01:68:00:10:01:41%301989891" = INTEGER: 2
+                match raw_length:
+                    case '20':
+                        raw_address = [f'{int(x):02x}' for x in raw_address.split('.')]
+                        scope_id = '.'.join(raw_address[16:])
+                        raw_address = ':'.join(
+                            [''.join([raw_address[i], raw_address[i + 1]]) for i in range(0, len(raw_address) - 4, 2)]
+                        )
+                        raw_address += f'%{scope_id}'
             case _:
                 continue
 
         try:
-            interface = ip_interface(f'{raw_address}/{prefix}')
+            interface_ip = ip_interface(f'{raw_address}/{prefix}')
         except (AddressValueError, NetmaskValueError):
             continue
 
-        if interface.ip.is_loopback:  # Drop localhost
+        if interface_ip.ip.is_loopback:  # Drop localhost
             continue
 
-        if interface.ip.exploded == '0.0.0.0':  # drop this host address
+        if interface_ip.ip.exploded == '0.0.0.0':  # drop this host address
             continue
 
-        ip_infos.append({(str(interface_by_index.get(if_index, if_index))): interface})
+        ip_infos.append({(str(interface_by_index.get(if_index, if_index))): interface_ip})
 
     for entry in ip_info_20:
         try:
@@ -179,17 +201,18 @@ def parse_inv_ip_addresses(string_table: List[StringByteTable]) -> Section:
             continue
 
         try:
-            interface = ip_interface(f'{raw_address}/{raw_netmask}')
+            interface_ip = ip_interface(f'{raw_address}/{raw_netmask}')
         except (AddressValueError, NetmaskValueError):
             continue
 
-        if interface.ip.is_loopback:  # Drop localhost
+        if interface_ip.ip.is_loopback:  # Drop localhost
             continue
 
-        if interface.ip.exploded == '0.0.0.0':  # drop this host address
+        if interface_ip.ip.exploded == '0.0.0.0':  # drop this host address
             continue
 
-        ip_infos.append({str(interface_by_index.get(if_index, if_index)): interface})
+        if not (ip_info := {str(interface_by_index.get(if_index, if_index)): interface_ip}) in ip_infos:
+            ip_infos.append(ip_info)
 
     return ip_infos
 
@@ -199,19 +222,33 @@ def host_label_inv_ip_addresses(section: Section) -> HostLabelGenerator:
     Host label function
     Labels:
         nvdct/l3v4_topology:
-            This label is set to "host" for all devices with one IPv4 address except form 127.0.0.0/8 and to
-            "router" for all devices with more than one IPv4 address except form 127.0.0.0/8
+            "host" is set for all devices with one IPv4 address
+            "router" is set for all devices with more than one IPv4 address.
+        nvdct/l3v6_topology:
+            "host" is set for all devices with one IPv6 address
+            "router" is set for all devices with more than one IPv6 address.
+
+        Link-local ("FE80::/64), unspecified ("::") and local-host ("127.0.0.0/8", "::1") IPs don't count.
     """
-    non_host_ips = 0
-    for entry in section:
-        ip_data = entry.values()
-        if ip_data.version == 4 and not ip_data.ip.is_loopback:
-            non_host_ips += 1
-            if non_host_ips == 1:
-                yield HostLabel(name="nvdct/l3v4_topology", value="host")
-            if non_host_ips > 1:
-                yield HostLabel(name="nvdct/l3v4_topology", value="router")
-                return
+
+    valid_v4_ips = 0
+    valid_v6_ips = 0
+    for interface_ips in section:
+        for interface_ip in interface_ips.values():
+            if interface_ip.version == 4 and not interface_ip.is_loopback:
+                valid_v4_ips += 1
+                if valid_v4_ips == 1:
+                    yield HostLabel(name="nvdct/l3v4_topology", value="host")
+                if valid_v4_ips == 2:
+                    yield HostLabel(name="nvdct/l3v4_topology", value="router")
+
+            elif interface_ip.version == 6 and not interface_ip.is_loopback \
+                    and not interface_ip.is_link_local and not interface_ip.is_unspecified:
+                valid_v6_ips += 1
+                if valid_v6_ips == 1:
+                    yield HostLabel(name="nvdct/l3v6_topology", value="host")
+                if valid_v6_ips == 2:
+                    yield HostLabel(name="nvdct/l3v6_topology", value="router")
 
 
 def inventory_ip_addresses(section: Section) -> InventoryResult:
@@ -221,6 +258,11 @@ def inventory_ip_addresses(section: Section) -> InventoryResult:
     }
     for entry in section:
         for if_name, ip_data in entry.items():
+            try:  # ipv4 has no scope_id
+                scope_id = ip_data.scope_id
+            except AttributeError:
+                scope_id = None
+
             yield TableRow(
                 path=['networking', 'addresses'],
                 key_columns={
@@ -233,6 +275,7 @@ def inventory_ip_addresses(section: Section) -> InventoryResult:
                     'netmask': str(ip_data.network.netmask),
                     'network': str(ip_data.network.network_address),
                     'type': address_type.get(ip_data.version).lower(),
+                    **({"scope_id": str(scope_id)} if scope_id else {}),
                 }
             )
 
@@ -273,3 +316,4 @@ inventory_plugin_inv_ip_address = InventoryPlugin(
     name='inv_ip_addresses',
     inventory_function=inventory_ip_addresses,
 )
+
diff --git a/source/packages/inv_ip_address b/source/packages/inv_ip_address
index e96434d..db97744 100644
--- a/source/packages/inv_ip_address
+++ b/source/packages/inv_ip_address
@@ -13,7 +13,7 @@
            'web': ['plugins/views/inv_ip_addresses.py']},
  'name': 'inv_ip_address',
  'title': 'Inventory of IP addresses',
- 'version': '0.0.5-20241209',
+ 'version': '0.0.6-20241210',
  'version.min_required': '2.3.0b1',
  'version.packaged': 'cmk-mkp-tool 0.2.0',
  'version.usable_until': '2.4.0b1'}
diff --git a/source/web/plugins/views/inv_ip_addresses.py b/source/web/plugins/views/inv_ip_addresses.py
index 81afc04..84c70ab 100644
--- a/source/web/plugins/views/inv_ip_addresses.py
+++ b/source/web/plugins/views/inv_ip_addresses.py
@@ -28,9 +28,10 @@ inventory_displayhints.update({
     },
     '.networking.addresses:*.address': {'title': _l('Address')},
     '.networking.addresses:*.broadcast': {'title': _l('Broadcast')},
-    '.networking.addresses:*.cidr': {'title': _l('Prefix'), },  # 'filter': FilterInvtableIDRange},
+    '.networking.addresses:*.cidr': {'title': _l('Prefix Length'), },  # 'filter': FilterInvtableIDRange},
     '.networking.addresses:*.device': {'title': _l('Device')},
     '.networking.addresses:*.netmask': {'title': _l('Netmask')},
     '.networking.addresses:*.network': {'title': _l('Network')},
-    '.networking.addresses:*.type': {'title': _l('Address Type'), 'paint': 'ip_address_type'},
+    '.networking.addresses:*.type': {'title': _l('Type'), 'paint': 'ip_address_type'},
+    '.networking.addresses:*.scope_id': {'title': _l('Scope ID')},
 })
-- 
GitLab