Add SNMP profile mapping and fix asset cleanup
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
import random
|
||||
import socket
|
||||
from typing import Any
|
||||
@@ -26,6 +26,15 @@ class DiscoveredSnmpInterface:
|
||||
speed_bps: int | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DiscoveredSnmpHealthItem:
|
||||
item_id: str
|
||||
item_type: str
|
||||
group: str
|
||||
label: str
|
||||
unit: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DiscoveredSnmpDevice:
|
||||
host: str
|
||||
@@ -33,6 +42,10 @@ class DiscoveredSnmpDevice:
|
||||
description: str | None
|
||||
uptime_seconds: int | None
|
||||
interfaces: list[DiscoveredSnmpInterface]
|
||||
profile_key: str = "generic_snmp"
|
||||
profile_name: str = "Generic SNMP"
|
||||
capabilities: dict[str, bool] = field(default_factory=dict)
|
||||
health_items: list[DiscoveredSnmpHealthItem] = field(default_factory=list)
|
||||
|
||||
|
||||
SYS_DESCR = (1, 3, 6, 1, 2, 1, 1, 1, 0)
|
||||
@@ -43,6 +56,24 @@ IF_SPEED = (1, 3, 6, 1, 2, 1, 2, 2, 1, 5)
|
||||
IF_ADMIN_STATUS = (1, 3, 6, 1, 2, 1, 2, 2, 1, 7)
|
||||
IF_OPER_STATUS = (1, 3, 6, 1, 2, 1, 2, 2, 1, 8)
|
||||
IF_NAME = (1, 3, 6, 1, 2, 1, 31, 1, 1, 1, 1)
|
||||
HR_PROCESSOR_LOAD = (1, 3, 6, 1, 2, 1, 25, 3, 3, 1, 2)
|
||||
HR_STORAGE_TYPE = (1, 3, 6, 1, 2, 1, 25, 2, 3, 1, 2)
|
||||
HR_STORAGE_DESCR = (1, 3, 6, 1, 2, 1, 25, 2, 3, 1, 3)
|
||||
HR_STORAGE_ALLOCATION_UNITS = (1, 3, 6, 1, 2, 1, 25, 2, 3, 1, 4)
|
||||
HR_STORAGE_SIZE = (1, 3, 6, 1, 2, 1, 25, 2, 3, 1, 5)
|
||||
HR_STORAGE_USED = (1, 3, 6, 1, 2, 1, 25, 2, 3, 1, 6)
|
||||
ENT_PHYSICAL_DESCR = (1, 3, 6, 1, 2, 1, 47, 1, 1, 1, 1, 2)
|
||||
ENT_PHYSICAL_NAME = (1, 3, 6, 1, 2, 1, 47, 1, 1, 1, 1, 7)
|
||||
ENT_PHY_SENSOR_TYPE = (1, 3, 6, 1, 2, 1, 99, 1, 1, 1, 1)
|
||||
ENT_PHY_SENSOR_SCALE = (1, 3, 6, 1, 2, 1, 99, 1, 1, 1, 2)
|
||||
ENT_PHY_SENSOR_PRECISION = (1, 3, 6, 1, 2, 1, 99, 1, 1, 1, 3)
|
||||
ENT_PHY_SENSOR_VALUE = (1, 3, 6, 1, 2, 1, 99, 1, 1, 1, 4)
|
||||
ENT_PHY_SENSOR_OPER_STATUS = (1, 3, 6, 1, 2, 1, 99, 1, 1, 1, 5)
|
||||
|
||||
HR_STORAGE_RAM = "1.3.6.1.2.1.25.2.1.2"
|
||||
HR_STORAGE_VIRTUAL_MEMORY = "1.3.6.1.2.1.25.2.1.3"
|
||||
HR_STORAGE_FIXED_DISK = "1.3.6.1.2.1.25.2.1.4"
|
||||
HR_STORAGE_REMOVABLE_DISK = "1.3.6.1.2.1.25.2.1.5"
|
||||
|
||||
STATUS_LABELS = {
|
||||
1: "up",
|
||||
@@ -54,20 +85,92 @@ STATUS_LABELS = {
|
||||
7: "lower layer down",
|
||||
}
|
||||
|
||||
SENSOR_TYPE_LABELS = {
|
||||
3: ("AC voltage", "V"),
|
||||
4: ("DC voltage", "V"),
|
||||
5: ("Current", "A"),
|
||||
6: ("Power", "W"),
|
||||
7: ("Frequency", "Hz"),
|
||||
8: ("Temperature", "C"),
|
||||
9: ("Humidity", "%"),
|
||||
10: ("Fan speed", "rpm"),
|
||||
11: ("Airflow", "m3/min"),
|
||||
12: ("Sensor state", None),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SnmpProfile:
|
||||
key: str
|
||||
name: str
|
||||
match_terms: tuple[str, ...] = ()
|
||||
|
||||
def matches(self, system_text: str) -> bool:
|
||||
return any(term in system_text for term in self.match_terms)
|
||||
|
||||
def discover_health_items(self, client: "SnmpV2Client") -> list[DiscoveredSnmpHealthItem]:
|
||||
return [*_discover_host_resource_items(client), *_discover_sensor_items(client)]
|
||||
|
||||
|
||||
SNMP_PROFILES = (
|
||||
SnmpProfile("cisco_ios", "Cisco IOS SNMP", ("cisco", "ios")),
|
||||
SnmpProfile("mikrotik_routeros", "MikroTik RouterOS SNMP", ("mikrotik", "routeros")),
|
||||
SnmpProfile("net_snmp", "Net-SNMP Host Resources", ("net-snmp", "linux")),
|
||||
SnmpProfile("generic_snmp", "Generic SNMP"),
|
||||
)
|
||||
|
||||
|
||||
def discover_snmp_device(host: str, credential: SnmpCredential) -> DiscoveredSnmpDevice:
|
||||
client = SnmpV2Client(host, credential)
|
||||
system = client.get_many([SYS_NAME, SYS_DESCR, SYS_UPTIME])
|
||||
profile = _select_profile(system)
|
||||
interfaces = _discover_interfaces(client)
|
||||
health_items = profile.discover_health_items(client)
|
||||
return DiscoveredSnmpDevice(
|
||||
host=host,
|
||||
device_name=_string_value(system.get(SYS_NAME)),
|
||||
description=_string_value(system.get(SYS_DESCR)),
|
||||
uptime_seconds=_timeticks_to_seconds(system.get(SYS_UPTIME)),
|
||||
interfaces=interfaces,
|
||||
profile_key=profile.key,
|
||||
profile_name=profile.name,
|
||||
capabilities=_capabilities(system, interfaces, health_items),
|
||||
health_items=health_items,
|
||||
)
|
||||
|
||||
|
||||
def _select_profile(system: dict[tuple[int, ...], Any]) -> SnmpProfile:
|
||||
system_text = " ".join(
|
||||
value.lower()
|
||||
for value in [_string_value(system.get(SYS_NAME)), _string_value(system.get(SYS_DESCR))]
|
||||
if value
|
||||
)
|
||||
for profile in SNMP_PROFILES:
|
||||
if profile.match_terms and profile.matches(system_text):
|
||||
return profile
|
||||
return SNMP_PROFILES[-1]
|
||||
|
||||
|
||||
def _capabilities(
|
||||
system: dict[tuple[int, ...], Any],
|
||||
interfaces: list[DiscoveredSnmpInterface],
|
||||
health_items: list[DiscoveredSnmpHealthItem],
|
||||
) -> dict[str, bool]:
|
||||
item_types = {item.item_type for item in health_items}
|
||||
return {
|
||||
"system": any(system.get(oid) is not None for oid in [SYS_NAME, SYS_DESCR, SYS_UPTIME]),
|
||||
"interfaces": bool(interfaces),
|
||||
"interface_status": bool(interfaces),
|
||||
"interface_traffic": bool(interfaces),
|
||||
"interface_errors": bool(interfaces),
|
||||
"cpu": "cpu_load" in item_types,
|
||||
"memory": "memory_usage" in item_types,
|
||||
"storage": "storage_usage" in item_types,
|
||||
"sensors": "sensor_value" in item_types,
|
||||
"environmental": "sensor_value" in item_types,
|
||||
}
|
||||
|
||||
|
||||
def _discover_interfaces(client: "SnmpV2Client") -> list[DiscoveredSnmpInterface]:
|
||||
names = client.walk(IF_NAME)
|
||||
descriptions = client.walk(IF_DESCR)
|
||||
@@ -106,6 +209,108 @@ def _discover_interfaces(client: "SnmpV2Client") -> list[DiscoveredSnmpInterface
|
||||
return interfaces
|
||||
|
||||
|
||||
def _discover_host_resource_items(client: "SnmpV2Client") -> list[DiscoveredSnmpHealthItem]:
|
||||
items: list[DiscoveredSnmpHealthItem] = []
|
||||
|
||||
processor_loads = _indexed_values(client.walk(HR_PROCESSOR_LOAD, max_items=256))
|
||||
processor_indexes = sorted(index for index, value in processor_loads.items() if _int_value(value) is not None)
|
||||
for position, index in enumerate(processor_indexes, start=1):
|
||||
label = "CPU load" if len(processor_indexes) == 1 else f"CPU {position} load"
|
||||
items.append(
|
||||
DiscoveredSnmpHealthItem(
|
||||
item_id=f"cpu.{index}.load",
|
||||
item_type="cpu_load",
|
||||
group="Device Health",
|
||||
label=label,
|
||||
unit="%",
|
||||
)
|
||||
)
|
||||
|
||||
storage_types = _indexed_values(client.walk(HR_STORAGE_TYPE, max_items=256))
|
||||
descriptions = _indexed_values(client.walk(HR_STORAGE_DESCR, max_items=256))
|
||||
allocation_units = _indexed_values(client.walk(HR_STORAGE_ALLOCATION_UNITS, max_items=256))
|
||||
sizes = _indexed_values(client.walk(HR_STORAGE_SIZE, max_items=256))
|
||||
used = _indexed_values(client.walk(HR_STORAGE_USED, max_items=256))
|
||||
|
||||
for index in sorted(storage_types):
|
||||
storage_type = _string_value(storage_types.get(index))
|
||||
allocation_unit = _int_value(allocation_units.get(index))
|
||||
size = _int_value(sizes.get(index))
|
||||
used_blocks = _int_value(used.get(index))
|
||||
if not storage_type or not allocation_unit or not size or used_blocks is None:
|
||||
continue
|
||||
description = _string_value(descriptions.get(index)) or f"Storage {index}"
|
||||
if storage_type in {HR_STORAGE_RAM, HR_STORAGE_VIRTUAL_MEMORY}:
|
||||
items.append(
|
||||
DiscoveredSnmpHealthItem(
|
||||
item_id=f"storage.{index}.memory",
|
||||
item_type="memory_usage",
|
||||
group="Device Health",
|
||||
label="Memory used",
|
||||
unit="%",
|
||||
)
|
||||
)
|
||||
elif storage_type in {HR_STORAGE_FIXED_DISK, HR_STORAGE_REMOVABLE_DISK}:
|
||||
items.append(
|
||||
DiscoveredSnmpHealthItem(
|
||||
item_id=f"storage.{index}.usage",
|
||||
item_type="storage_usage",
|
||||
group="Storage",
|
||||
label=_storage_usage_label(description),
|
||||
unit="%",
|
||||
)
|
||||
)
|
||||
|
||||
return _deduplicate_items(items)
|
||||
|
||||
|
||||
def _discover_sensor_items(client: "SnmpV2Client") -> list[DiscoveredSnmpHealthItem]:
|
||||
sensor_types = _indexed_values(client.walk(ENT_PHY_SENSOR_TYPE, max_items=256))
|
||||
sensor_values = _indexed_values(client.walk(ENT_PHY_SENSOR_VALUE, max_items=256))
|
||||
sensor_names = _indexed_values(client.walk(ENT_PHYSICAL_NAME, max_items=256))
|
||||
sensor_descriptions = _indexed_values(client.walk(ENT_PHYSICAL_DESCR, max_items=256))
|
||||
|
||||
items: list[DiscoveredSnmpHealthItem] = []
|
||||
for index in sorted(sensor_types):
|
||||
sensor_type = _int_value(sensor_types.get(index))
|
||||
if sensor_type not in SENSOR_TYPE_LABELS or _int_value(sensor_values.get(index)) is None:
|
||||
continue
|
||||
kind, unit = SENSOR_TYPE_LABELS[sensor_type]
|
||||
name = _string_value(sensor_names.get(index)) or _string_value(sensor_descriptions.get(index))
|
||||
label = kind if not name else f"{kind} {name}"
|
||||
items.append(
|
||||
DiscoveredSnmpHealthItem(
|
||||
item_id=f"sensor.{index}.value",
|
||||
item_type="sensor_value",
|
||||
group="Environmental",
|
||||
label=label,
|
||||
unit=unit,
|
||||
)
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
def _storage_usage_label(description: str) -> str:
|
||||
normalized = description.strip()
|
||||
if normalized in {"/", "/boot", "/home", "/var"}:
|
||||
return f"Disk {normalized} usage"
|
||||
if "disk" not in normalized.lower() and normalized.startswith("/"):
|
||||
return f"Disk {normalized} usage"
|
||||
return f"{normalized} usage"
|
||||
|
||||
|
||||
def _deduplicate_items(items: list[DiscoveredSnmpHealthItem]) -> list[DiscoveredSnmpHealthItem]:
|
||||
seen: set[tuple[str, str]] = set()
|
||||
deduplicated: list[DiscoveredSnmpHealthItem] = []
|
||||
for item in items:
|
||||
key = (item.item_type, item.label)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
deduplicated.append(item)
|
||||
return deduplicated
|
||||
|
||||
|
||||
def _indexed_values(values: dict[tuple[int, ...], Any]) -> dict[int, Any]:
|
||||
indexed: dict[int, Any] = {}
|
||||
for oid, value in values.items():
|
||||
@@ -317,7 +522,9 @@ def _decode_oid(value: bytes) -> tuple[int, ...]:
|
||||
|
||||
|
||||
def _decode_value(tag: int, value: bytes) -> Any:
|
||||
if tag in {0x02, 0x41, 0x42, 0x43, 0x46}:
|
||||
if tag == 0x02:
|
||||
return _decode_integer(value)
|
||||
if tag in {0x41, 0x42, 0x43, 0x46}:
|
||||
return int.from_bytes(value, "big")
|
||||
if tag == 0x04:
|
||||
return value.decode("utf-8", errors="replace")
|
||||
|
||||
Reference in New Issue
Block a user