From bf301aa5fcf5dd10a24d3cd45580ff917d263144 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 5 Jan 2026 14:06:46 -0600 Subject: [PATCH] Enhance MikroTik discovery with comprehensive sensor support Based on MikroTik MIB analysis, added discovery for: Device Information: - Board name, display name, build time Health Sensors - Voltages: - Core voltage, 3.3V/5V/12V supplies, input voltage Health Sensors - Temperatures: - Sensor chip, CPU, board, system, processor temperatures Health Sensors - Power & Cooling: - Power consumption (W), current draw (mA) - Processor frequency (MHz) - Active fan status, fan speeds (RPM) - Power supply state, backup PSU state POE Monitoring (per port): - Voltage, current, power consumption - Automatically discovers all POE-enabled ports Optical/SFP Monitoring (per interface): - Temperature, voltage - TX/RX optical power levels (dBm) - Supports fiber transceivers and SFP modules All sensors include proper units, divisors, and descriptive names. --- lib/towerops/snmp/profiles/mikrotik.ex | 250 +++++++++++++++++++++++-- 1 file changed, 232 insertions(+), 18 deletions(-) diff --git a/lib/towerops/snmp/profiles/mikrotik.ex b/lib/towerops/snmp/profiles/mikrotik.ex index 4c6bb339..5b79e236 100644 --- a/lib/towerops/snmp/profiles/mikrotik.ex +++ b/lib/towerops/snmp/profiles/mikrotik.ex @@ -22,17 +22,39 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do # System Information (mtxrSystem) serial_number: "1.3.6.1.4.1.14988.1.1.7.3.0", firmware_version: "1.3.6.1.4.1.14988.1.1.7.4.0", + board_name: "1.3.6.1.4.1.14988.1.1.7.9.0", + display_name: "1.3.6.1.4.1.14988.1.1.7.8.0", + build_time: "1.3.6.1.4.1.14988.1.1.7.6.0", license_version: "1.3.6.1.4.1.14988.1.1.4.4.0", - # Health (mtxrHealth) - temperature: "1.3.6.1.4.1.14988.1.1.3.10.0", - cpu_temperature: "1.3.6.1.4.1.14988.1.1.3.11.0", + # Health - Voltages (mtxrHealth) + core_voltage: "1.3.6.1.4.1.14988.1.1.3.1.0", + three_volt: "1.3.6.1.4.1.14988.1.1.3.2.0", + five_volt: "1.3.6.1.4.1.14988.1.1.3.3.0", + twelve_volt: "1.3.6.1.4.1.14988.1.1.3.4.0", voltage: "1.3.6.1.4.1.14988.1.1.3.8.0", - current: "1.3.6.1.4.1.14988.1.1.3.9.0", - power_supply_state: "1.3.6.1.4.1.14988.1.1.3.12.0", + + # Health - Temperatures + sensor_temperature: "1.3.6.1.4.1.14988.1.1.3.5.0", + cpu_temperature: "1.3.6.1.4.1.14988.1.1.3.6.0", + board_temperature: "1.3.6.1.4.1.14988.1.1.3.7.0", + temperature: "1.3.6.1.4.1.14988.1.1.3.10.0", + processor_temperature: "1.3.6.1.4.1.14988.1.1.3.11.0", + + # Health - Power & Current + current: "1.3.6.1.4.1.14988.1.1.3.13.0", + power: "1.3.6.1.4.1.14988.1.1.3.12.0", + + # Health - Fans & Cooling + active_fan: "1.3.6.1.4.1.14988.1.1.3.9.0", fan_speed_1: "1.3.6.1.4.1.14988.1.1.3.17.0", fan_speed_2: "1.3.6.1.4.1.14988.1.1.3.18.0", + # Health - Power Supply + processor_frequency: "1.3.6.1.4.1.14988.1.1.3.14.0", + power_supply_state: "1.3.6.1.4.1.14988.1.1.3.15.0", + backup_psu_state: "1.3.6.1.4.1.14988.1.1.3.16.0", + # System Resources cpu_load: "1.3.6.1.2.1.25.3.3.1.2.1", total_memory: "1.3.6.1.2.1.25.2.2.0", @@ -41,6 +63,10 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do used_hdd: "1.3.6.1.4.1.14988.1.1.1.7.0" } + # Table OID bases for walking + @poe_table "1.3.6.1.4.1.14988.1.1.15.1" + @optical_table "1.3.6.1.4.1.14988.1.1.16.1" + @doc """ Discovers system information using Base profile and adds MikroTik-specific details. """ @@ -118,15 +144,19 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do oids = [ @mikrotik_oids.serial_number, @mikrotik_oids.firmware_version, + @mikrotik_oids.board_name, + @mikrotik_oids.display_name, @mikrotik_oids.license_version ] case Client.get_multiple(client_opts, oids) do - {:ok, [serial, firmware, license]} -> + {:ok, [serial, firmware, board, display, license]} -> {:ok, %{ serial_number: serial, firmware_version: firmware, + board_name: board, + display_name: display, license_version: license }} @@ -137,14 +167,28 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do end defp discover_health_sensors(client_opts) do - # Try to get all health OIDs + # Try to get all health OIDs - voltages, temperatures, power, fans health_oids = [ - {@mikrotik_oids.temperature, "temperature", "°C", 10}, + # Voltages + {@mikrotik_oids.core_voltage, "core_voltage", "V", 10}, + {@mikrotik_oids.three_volt, "3v3_voltage", "V", 10}, + {@mikrotik_oids.five_volt, "5v_voltage", "V", 10}, + {@mikrotik_oids.twelve_volt, "12v_voltage", "V", 10}, + {@mikrotik_oids.voltage, "input_voltage", "V", 10}, + # Temperatures + {@mikrotik_oids.sensor_temperature, "sensor_temperature", "°C", 10}, {@mikrotik_oids.cpu_temperature, "cpu_temperature", "°C", 10}, - {@mikrotik_oids.voltage, "voltage", "V", 10}, + {@mikrotik_oids.board_temperature, "board_temperature", "°C", 10}, + {@mikrotik_oids.temperature, "temperature", "°C", 10}, + {@mikrotik_oids.processor_temperature, "processor_temperature", "°C", 10}, + # Power & Current {@mikrotik_oids.current, "current", "mA", 1}, + {@mikrotik_oids.power, "power", "W", 10}, + # Fans {@mikrotik_oids.fan_speed_1, "fan1", "RPM", 1}, - {@mikrotik_oids.fan_speed_2, "fan2", "RPM", 1} + {@mikrotik_oids.fan_speed_2, "fan2", "RPM", 1}, + # Processor + {@mikrotik_oids.processor_frequency, "processor_frequency", "MHz", 1} ] health_oids @@ -172,8 +216,10 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do defp discover_resource_sensors(client_opts) do cpu_sensors = discover_cpu_sensors(client_opts) storage_sensors = discover_storage_sensors(client_opts) + poe_sensors = discover_poe_sensors(client_opts) + optical_sensors = discover_optical_sensors(client_opts) - cpu_sensors ++ storage_sensors + cpu_sensors ++ storage_sensors ++ poe_sensors ++ optical_sensors end defp discover_cpu_sensors(client_opts) do @@ -224,18 +270,18 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do if size > 0 do percent = used / size * 100 - {type, descr_name} = + type = cond do - String.contains?(descr, "memory") -> {"memory", "Memory"} - String.contains?(descr, "disk") -> {"disk", "Disk"} - true -> {"storage", descr} + String.contains?(descr, "memory") -> "memory" + String.contains?(descr, "disk") -> "disk" + true -> "storage" end %{ sensor_type: "#{type}_usage", sensor_index: index, sensor_oid: used_oid, - sensor_descr: "#{descr_name} Usage", + sensor_descr: descr, sensor_unit: "%", sensor_divisor: 1, last_value: percent, @@ -253,12 +299,180 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do end end - defp format_sensor_name("temperature"), do: "Board Temperature" + defp discover_poe_sensors(client_opts) do + # Walk POE table to get per-port POE statistics + # Table columns: Name(3), Status(4), Voltage(5), Current(6), Power(7) + with {:ok, name_results} <- Client.walk(client_opts, "#{@poe_table}.1.3"), + {:ok, voltage_results} <- Client.walk(client_opts, "#{@poe_table}.1.5"), + {:ok, current_results} <- Client.walk(client_opts, "#{@poe_table}.1.6"), + {:ok, power_results} <- Client.walk(client_opts, "#{@poe_table}.1.7") do + # Get indices from the name results + indices = + name_results + |> Map.keys() + |> Enum.map(fn oid -> oid |> String.split(".") |> List.last() end) + + # Create sensors for voltage, current, and power per port + Enum.flat_map(indices, fn index -> + name_oid = "#{@poe_table}.1.3.#{index}" + voltage_oid = "#{@poe_table}.1.5.#{index}" + current_oid = "#{@poe_table}.1.6.#{index}" + power_oid = "#{@poe_table}.1.7.#{index}" + + port_name = Map.get(name_results, name_oid, "Port #{index}") + voltage = Map.get(voltage_results, voltage_oid, 0) + current = Map.get(current_results, current_oid, 0) + power = Map.get(power_results, power_oid, 0) + + Enum.reject( + [ + if voltage > 0 do + %{ + sensor_type: "poe_voltage", + sensor_index: "poe_#{index}_voltage", + sensor_oid: voltage_oid, + sensor_descr: "#{port_name} POE Voltage", + sensor_unit: "V", + sensor_divisor: 10, + last_value: voltage / 10.0, + status: "ok" + } + end, + if current > 0 do + %{ + sensor_type: "poe_current", + sensor_index: "poe_#{index}_current", + sensor_oid: current_oid, + sensor_descr: "#{port_name} POE Current", + sensor_unit: "mA", + sensor_divisor: 1, + last_value: current / 1.0, + status: "ok" + } + end, + if power > 0 do + %{ + sensor_type: "poe_power", + sensor_index: "poe_#{index}_power", + sensor_oid: power_oid, + sensor_descr: "#{port_name} POE Power", + sensor_unit: "W", + sensor_divisor: 10, + last_value: power / 10.0, + status: "ok" + } + end + ], + &is_nil/1 + ) + end) + else + _ -> [] + end + end + + defp discover_optical_sensors(client_opts) do + # Walk optical table to get SFP/optical monitoring data + # Table columns: Name(2), Temperature(3), Voltage(5), TxBiasCurrent(6), TxPower(7), RxPower(8) + with {:ok, name_results} <- Client.walk(client_opts, "#{@optical_table}.1.2"), + {:ok, temp_results} <- Client.walk(client_opts, "#{@optical_table}.1.3"), + {:ok, voltage_results} <- Client.walk(client_opts, "#{@optical_table}.1.5"), + {:ok, tx_power_results} <- Client.walk(client_opts, "#{@optical_table}.1.7"), + {:ok, rx_power_results} <- Client.walk(client_opts, "#{@optical_table}.1.8") do + # Get indices from the name results + indices = + name_results + |> Map.keys() + |> Enum.map(fn oid -> oid |> String.split(".") |> List.last() end) + + # Create sensors for each optical metric + Enum.flat_map(indices, fn index -> + name_oid = "#{@optical_table}.1.2.#{index}" + temp_oid = "#{@optical_table}.1.3.#{index}" + voltage_oid = "#{@optical_table}.1.5.#{index}" + tx_power_oid = "#{@optical_table}.1.7.#{index}" + rx_power_oid = "#{@optical_table}.1.8.#{index}" + + sfp_name = Map.get(name_results, name_oid, "SFP #{index}") + temperature = Map.get(temp_results, temp_oid) + voltage = Map.get(voltage_results, voltage_oid) + tx_power = Map.get(tx_power_results, tx_power_oid) + rx_power = Map.get(rx_power_results, rx_power_oid) + + Enum.reject( + [ + if temperature do + %{ + sensor_type: "sfp_temperature", + sensor_index: "sfp_#{index}_temp", + sensor_oid: temp_oid, + sensor_descr: "#{sfp_name} Temperature", + sensor_unit: "°C", + sensor_divisor: 1000, + last_value: temperature / 1000.0, + status: "ok" + } + end, + if voltage do + %{ + sensor_type: "sfp_voltage", + sensor_index: "sfp_#{index}_voltage", + sensor_oid: voltage_oid, + sensor_descr: "#{sfp_name} Voltage", + sensor_unit: "V", + sensor_divisor: 1000, + last_value: voltage / 1000.0, + status: "ok" + } + end, + if tx_power do + %{ + sensor_type: "sfp_tx_power", + sensor_index: "sfp_#{index}_tx", + sensor_oid: tx_power_oid, + sensor_descr: "#{sfp_name} TX Power", + sensor_unit: "dBm", + sensor_divisor: 1000, + last_value: tx_power / 1000.0, + status: "ok" + } + end, + if rx_power do + %{ + sensor_type: "sfp_rx_power", + sensor_index: "sfp_#{index}_rx", + sensor_oid: rx_power_oid, + sensor_descr: "#{sfp_name} RX Power", + sensor_unit: "dBm", + sensor_divisor: 1000, + last_value: rx_power / 1000.0, + status: "ok" + } + end + ], + &is_nil/1 + ) + end) + else + _ -> [] + end + end + + defp format_sensor_name("core_voltage"), do: "Core Voltage" + defp format_sensor_name("3v3_voltage"), do: "3.3V Supply" + defp format_sensor_name("5v_voltage"), do: "5V Supply" + defp format_sensor_name("12v_voltage"), do: "12V Supply" + defp format_sensor_name("input_voltage"), do: "Input Voltage" + defp format_sensor_name("sensor_temperature"), do: "Sensor Chip Temperature" defp format_sensor_name("cpu_temperature"), do: "CPU Temperature" - defp format_sensor_name("voltage"), do: "Input Voltage" + defp format_sensor_name("board_temperature"), do: "Board Temperature" + defp format_sensor_name("temperature"), do: "System Temperature" + defp format_sensor_name("processor_temperature"), do: "Processor Temperature" defp format_sensor_name("current"), do: "Current Draw" + defp format_sensor_name("power"), do: "Power Consumption" defp format_sensor_name("fan1"), do: "Fan 1 Speed" defp format_sensor_name("fan2"), do: "Fan 2 Speed" + defp format_sensor_name("processor_frequency"), do: "Processor Frequency" defp format_sensor_name(other), do: String.capitalize(other) defp cpu_status(load) when load < 70, do: "ok"