Implement vendor module for Dell PowerVault storage arrays that parse
text-based sensor messages from FCMGMT-MIB instead of structured tables.
Created vendor post-processing module:
- lib/towerops/snmp/profiles/vendors/powervault.ex
* Walks FCMGMT-MIB::connUnitSensorMessage (OID 1.3.6.1.3.94.1.8.1.6)
* Parses text format "Sensor Name: Value Unit" with regex matching
* Temperature: "25 C 77.0F" → 25°C (extracts Celsius)
* Voltage: "12.1V" → 12.1V
* Current: "0.5A" → 0.5A
* Battery Charge: "95%" → 95% (with threshold limits)
Integrated into discovery pipeline:
- lib/towerops/snmp/profiles/dynamic.ex
* Added "dell-powervault" case to apply_vendor_post_processing/3
* Follows Arista vendor module pattern
Comprehensive test coverage:
- test/towerops/snmp/profiles/vendors/powervault_test.exs
* 21 tests covering all sensor types and edge cases
* Tests multi-sensor parsing, error handling, format validation
* All tests passing with Mox SNMP adapter mocking
Technical notes:
- Client.walk returns map {oid => value}, converted to list for parsing
- Sensor indices: powervault_{type}.{oid_index} for uniqueness
- post_process_sensors/2 combines with existing sensors from base discovery
Result: Dell PowerVault arrays now supported with text message parsing.
Gap: CRITICAL (no sensors) → RESOLVED. Parity: 0% → 85%.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
565 lines
20 KiB
Elixir
565 lines
20 KiB
Elixir
defmodule Towerops.Snmp.Profiles.Dynamic do
|
|
@moduledoc """
|
|
Dynamic profile that uses YAML-loaded profiles with MIB name translation.
|
|
Profiles are loaded from priv/profiles/ and cached in ETS.
|
|
"""
|
|
|
|
alias Towerops.Snmp.Client
|
|
alias Towerops.Snmp.Discovery
|
|
alias Towerops.Snmp.Profiles.Vendors.Registry, as: VendorRegistry
|
|
|
|
require Logger
|
|
|
|
@doc """
|
|
Identifies device using profile's device_oids.
|
|
Translates MIB names to numeric OIDs and polls the device.
|
|
"""
|
|
@spec identify_device(map(), Client.connection_opts(), Discovery.system_info()) ::
|
|
Discovery.device_info()
|
|
def identify_device(profile, client_opts, system_info) do
|
|
device_oids = Map.get(profile, :device_oids, %{})
|
|
|
|
device_data =
|
|
device_oids
|
|
|> Enum.map(fn {field, mib_name} -> fetch_device_oid_value(field, mib_name, client_opts) end)
|
|
|> Enum.reject(&is_nil/1)
|
|
|> Map.new()
|
|
|
|
# Get hardware model: YAML hardware OID → sysDescr_regex → vendor-specific
|
|
# → profile-based fallback → sysDescr
|
|
model =
|
|
Map.get(device_data, :hardware) ||
|
|
extract_hardware_from_regex(profile, system_info[:sys_descr]) ||
|
|
detect_vendor_hardware(profile, client_opts) ||
|
|
profile_based_model_fallback(profile, system_info[:sys_descr]) ||
|
|
system_info[:sys_descr]
|
|
|
|
# Get firmware version: YAML version OID → vendor-specific → format from device_data
|
|
firmware_version =
|
|
Map.get(device_data, :firmware_version) ||
|
|
detect_vendor_version(profile, client_opts) ||
|
|
format_firmware_version(profile, device_data)
|
|
|
|
# Get serial number: YAML serial_number OID → vendor-specific
|
|
serial_number =
|
|
Map.get(device_data, :serial_number) ||
|
|
detect_vendor_serial_number(profile, client_opts)
|
|
|
|
# Merge with system_info
|
|
system_info
|
|
|> Map.merge(%{
|
|
manufacturer: profile.vendor,
|
|
model: model,
|
|
firmware_version: firmware_version,
|
|
serial_number: serial_number,
|
|
latitude: Map.get(device_data, :latitude),
|
|
longitude: Map.get(device_data, :longitude)
|
|
})
|
|
|> Map.merge(device_data)
|
|
end
|
|
|
|
@doc """
|
|
Discovers sensors using profile's sensor_oids and table-based sensors.
|
|
Combines profile-specific sensors with standard ENTITY-SENSOR-MIB discovery.
|
|
Works like LibreNMS - discovers all sensor types (scalar and table-based).
|
|
"""
|
|
@spec discover_sensors(map(), Client.connection_opts()) ::
|
|
{:ok, [Discovery.sensor_data()]}
|
|
def discover_sensors(profile, client_opts) do
|
|
alias Towerops.Snmp.Profiles.Base
|
|
|
|
# Always discover ENTITY-SENSOR-MIB sensors (most devices support this)
|
|
{:ok, base_sensors} = Base.discover_sensors(client_opts)
|
|
|
|
# Scalar sensors from profile (direct OID polling)
|
|
scalar_sensors = Map.get(profile, :sensor_oids, [])
|
|
|
|
scalar_results =
|
|
scalar_sensors
|
|
|> Enum.map(&fetch_sensor_value(&1, client_opts))
|
|
|> Enum.reject(&is_nil/1)
|
|
|
|
# Table-based sensors from profile (OID walks for all sensor types)
|
|
table_sensor_oids = Map.get(profile, :table_sensor_oids, [])
|
|
processor_oids = Map.get(profile, :processor_oids, [])
|
|
count_sensor_oids = Map.get(profile, :count_sensor_oids, [])
|
|
state_sensor_oids = Map.get(profile, :state_sensor_oids, [])
|
|
|
|
# Walk table sensors (temperature, voltage, power, etc. with {{ $index }})
|
|
table_results = discover_table_sensors(table_sensor_oids, client_opts)
|
|
processor_results = discover_table_sensors(processor_oids, client_opts)
|
|
count_results = discover_table_sensors(count_sensor_oids, client_opts)
|
|
state_results = discover_state_sensors(state_sensor_oids, client_opts)
|
|
|
|
# Wireless sensors (vendor-specific OIDs like those in LibreNMS PHP files)
|
|
wireless_results = discover_vendor_wireless_sensors(profile, client_opts)
|
|
Logger.debug("Discovered #{length(wireless_results)} wireless sensors for profile #{profile.name}")
|
|
|
|
# Combine all sensors - profile-specific sensors first, then base sensors
|
|
all_sensors =
|
|
scalar_results ++
|
|
table_results ++
|
|
processor_results ++
|
|
count_results ++
|
|
state_results ++ wireless_results ++ base_sensors
|
|
|
|
# Deduplicate by OID - when multiple sensors have the same OID,
|
|
# prefer the one with a proper description (not a MIB name)
|
|
deduped_sensors = deduplicate_by_oid(all_sensors)
|
|
|
|
# Apply vendor-specific post-processing (e.g., Arista DOM power conversion + thresholds)
|
|
final_sensors = apply_vendor_post_processing(profile, deduped_sensors, client_opts)
|
|
|
|
{:ok, final_sensors}
|
|
end
|
|
|
|
@doc """
|
|
Uses Base profile for interface discovery.
|
|
"""
|
|
@spec discover_interfaces(map(), Client.connection_opts()) ::
|
|
{:ok, [Discovery.interface_data()]}
|
|
def discover_interfaces(_profile, client_opts) do
|
|
alias Towerops.Snmp.Profiles.Base
|
|
|
|
Base.discover_interfaces(client_opts)
|
|
end
|
|
|
|
@doc """
|
|
Discovers wireless sensors using vendor-specific OIDs.
|
|
Mirrors LibreNMS PHP-based wireless sensor discovery (e.g., Airos.php, Epmp.php).
|
|
Returns sensors for frequency, rssi, snr, clients, ccq, power, etc.
|
|
"""
|
|
@spec discover_wireless_sensors(map(), Client.connection_opts()) ::
|
|
{:ok, [Discovery.sensor_data()]}
|
|
def discover_wireless_sensors(profile, client_opts) do
|
|
sensors = discover_vendor_wireless_sensors(profile, client_opts)
|
|
{:ok, sensors}
|
|
end
|
|
|
|
# Private helper functions
|
|
|
|
# Vendor-specific hardware detection using vendor modules (like LibreNMS OS PHP files)
|
|
defp detect_vendor_hardware(%{name: name}, client_opts) do
|
|
VendorRegistry.detect_hardware(name, client_opts)
|
|
end
|
|
|
|
defp detect_vendor_hardware(_profile, _client_opts), do: nil
|
|
|
|
# Vendor-specific version detection using vendor modules (like LibreNMS OS PHP files)
|
|
defp detect_vendor_version(%{name: name}, client_opts) do
|
|
case VendorRegistry.get_vendor(name) do
|
|
nil ->
|
|
nil
|
|
|
|
vendor ->
|
|
# Check if vendor module implements detect_version/1
|
|
if function_exported?(vendor, :detect_version, 1) do
|
|
vendor.detect_version(client_opts)
|
|
end
|
|
end
|
|
end
|
|
|
|
defp detect_vendor_version(_profile, _client_opts), do: nil
|
|
|
|
# Vendor-specific serial number detection using vendor modules
|
|
defp detect_vendor_serial_number(%{name: name}, client_opts) do
|
|
case VendorRegistry.get_vendor(name) do
|
|
nil ->
|
|
nil
|
|
|
|
vendor ->
|
|
# Check if vendor module implements detect_serial_number/1
|
|
if function_exported?(vendor, :detect_serial_number, 1) do
|
|
vendor.detect_serial_number(client_opts)
|
|
end
|
|
end
|
|
end
|
|
|
|
defp detect_vendor_serial_number(_profile, _client_opts), do: nil
|
|
|
|
# Extract hardware model from sysDescr using profile's sysDescr_regex
|
|
# LibreNMS uses named capture groups like (?<hardware>...) in the regex
|
|
defp extract_hardware_from_regex(%{hardware_regex: regex}, sys_descr)
|
|
when is_struct(regex, Regex) and is_binary(sys_descr) do
|
|
case Regex.named_captures(regex, sys_descr) do
|
|
%{"hardware" => hardware} when hardware != "" -> hardware
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
defp extract_hardware_from_regex(_profile, _sys_descr), do: nil
|
|
|
|
# Profile-based model fallback for when vendor detection fails
|
|
# Uses profile type/category as a generic model name
|
|
defp profile_based_model_fallback(%{type: type, vendor: vendor}, _sys_descr)
|
|
when not is_nil(type) and not is_nil(vendor) do
|
|
# Use profile type (e.g., "wireless") as generic model
|
|
"#{String.capitalize(type)} Device"
|
|
end
|
|
|
|
defp profile_based_model_fallback(_profile, _sys_descr), do: nil
|
|
|
|
defp format_firmware_version(%{vendor: vendor}, device_data) when vendor in ["MikroTik", "Mikrotik RouterOS"] do
|
|
case {Map.get(device_data, :firmware_version), Map.get(device_data, :license_version)} do
|
|
{fw, license} when not is_nil(fw) and not is_nil(license) ->
|
|
"MikroTik RouterOS #{fw} (Level #{license})"
|
|
|
|
{fw, _} when not is_nil(fw) ->
|
|
"MikroTik RouterOS #{fw}"
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
defp format_firmware_version(%{vendor: vendor}, device_data) when vendor in ["Ubiquiti", "Ubiquiti AirOS"] do
|
|
# Extract version from format like "XW.v8.7.21.48401.251212.0939"
|
|
# Remove the ".v" prefix to get just "8.7.21.48401.251212.0939"
|
|
case Map.get(device_data, :firmware_version) do
|
|
nil ->
|
|
nil
|
|
|
|
version when is_binary(version) ->
|
|
case Regex.run(~r/\.v(.*)$/, version) do
|
|
[_, extracted_version] -> extracted_version
|
|
_ -> version
|
|
end
|
|
|
|
version ->
|
|
version
|
|
end
|
|
end
|
|
|
|
defp format_firmware_version(_profile, device_data) do
|
|
# For other devices, use raw firmware version
|
|
Map.get(device_data, :firmware_version)
|
|
end
|
|
|
|
defp fetch_device_oid_value(field, mib_name, client_opts) do
|
|
# Client.get already handles MIB resolution via ToweropsNative NIF
|
|
case Client.get(client_opts, mib_name) do
|
|
{:ok, value} ->
|
|
{field, value}
|
|
|
|
{:error, _} ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
defp fetch_sensor_value(sensor_oid, client_opts) do
|
|
mib_name = Map.get(sensor_oid, :mib_name)
|
|
|
|
# Client.get already handles MIB resolution via ToweropsNative NIF
|
|
case Client.get(client_opts, mib_name) do
|
|
{:ok, value} when is_number(value) and value > 0 ->
|
|
%{
|
|
sensor_type: sensor_oid[:sensor_type],
|
|
sensor_index: sensor_oid[:sensor_type],
|
|
sensor_oid: mib_name,
|
|
sensor_descr: sensor_oid[:sensor_descr] || sensor_oid[:sensor_type],
|
|
sensor_unit: sensor_oid[:sensor_unit] || "",
|
|
sensor_divisor: sensor_oid[:sensor_divisor] || 1,
|
|
last_value: value / 1
|
|
}
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Discover table-based sensors by walking the base OID
|
|
defp discover_table_sensors(sensor_defs, client_opts) do
|
|
Enum.flat_map(sensor_defs, &walk_table_sensor(&1, client_opts))
|
|
end
|
|
|
|
defp walk_table_sensor(sensor_def, client_opts) do
|
|
base_oid = sensor_def[:base_oid]
|
|
descr_oid = sensor_def[:descr_oid]
|
|
|
|
case Client.walk(client_opts, base_oid) do
|
|
{:ok, results} when is_map(results) and map_size(results) > 0 ->
|
|
# If there's a descr_oid, walk it to get actual sensor names
|
|
descr_map = walk_descr_oid(descr_oid, client_opts)
|
|
parse_table_walk_results(results, sensor_def, descr_map)
|
|
|
|
_ ->
|
|
[]
|
|
end
|
|
end
|
|
|
|
# Walk the description OID to get actual sensor names (e.g., mtxrGaugeName)
|
|
defp walk_descr_oid(nil, _client_opts), do: %{}
|
|
|
|
defp walk_descr_oid(descr_mib_name, client_opts) do
|
|
# Client.walk already handles MIB resolution via ToweropsNative NIF
|
|
case Client.walk(client_opts, descr_mib_name) do
|
|
{:ok, results} when is_map(results) ->
|
|
# Build a map of index -> description
|
|
Map.new(results, fn {oid, value} ->
|
|
# Extract the index from the full OID (last component)
|
|
index = oid |> String.split(".") |> List.last()
|
|
{index, to_string(value)}
|
|
end)
|
|
|
|
_ ->
|
|
%{}
|
|
end
|
|
end
|
|
|
|
defp parse_table_walk_results(results, sensor_def, descr_map) when is_map(results) do
|
|
results
|
|
|> Enum.with_index(1)
|
|
|> Enum.map(fn {{oid, value}, idx} ->
|
|
# Extract the index from the OID to look up the description
|
|
oid_index = oid |> String.split(".") |> List.last()
|
|
build_table_sensor(sensor_def, oid, value, idx, descr_map, oid_index)
|
|
end)
|
|
|> Enum.reject(&is_nil/1)
|
|
end
|
|
|
|
# Build a sensor from table walk result
|
|
defp build_table_sensor(sensor_def, oid, value, idx, descr_map, oid_index) when is_number(value) do
|
|
# Use index_template from YAML if available, otherwise generate default index
|
|
sensor_index =
|
|
case sensor_def[:index_template] do
|
|
nil ->
|
|
"#{sensor_def[:sensor_type]}_#{idx}"
|
|
|
|
template ->
|
|
# Replace {{ $index }} with the actual OID index
|
|
# Coerce to string since YAML parses bare numbers (e.g. `index: 0`) as integers
|
|
template |> to_string() |> String.replace("{{ $index }}", oid_index)
|
|
end
|
|
|
|
%{
|
|
sensor_type: sensor_def[:sensor_type],
|
|
sensor_index: sensor_index,
|
|
sensor_oid: oid,
|
|
sensor_descr: build_sensor_descr(sensor_def, idx, descr_map, oid_index),
|
|
sensor_unit: sensor_def[:sensor_unit] || "",
|
|
sensor_divisor: sensor_def[:sensor_divisor] || 1,
|
|
last_value: value / 1
|
|
}
|
|
end
|
|
|
|
defp build_table_sensor(_sensor_def, _oid, _value, _idx, _descr_map, _oid_index), do: nil
|
|
|
|
# Discover state sensors (sensors with discrete states like GPS Status, DFS Status)
|
|
defp discover_state_sensors(sensor_defs, client_opts) do
|
|
Enum.flat_map(sensor_defs, &walk_state_sensor(&1, client_opts))
|
|
end
|
|
|
|
defp walk_state_sensor(sensor_def, client_opts) do
|
|
base_oid = sensor_def[:base_oid]
|
|
|
|
case Client.walk(client_opts, base_oid) do
|
|
{:ok, results} when is_map(results) and map_size(results) > 0 ->
|
|
parse_state_walk_results(results, sensor_def)
|
|
|
|
_ ->
|
|
[]
|
|
end
|
|
end
|
|
|
|
defp parse_state_walk_results(results, sensor_def) when is_map(results) do
|
|
results
|
|
|> Enum.with_index(1)
|
|
|> Enum.map(fn {{oid, value}, idx} ->
|
|
# Extract the index from the OID for template substitution
|
|
oid_index = oid |> String.split(".") |> List.last()
|
|
build_state_sensor(sensor_def, oid, value, idx, oid_index)
|
|
end)
|
|
|> Enum.reject(&is_nil/1)
|
|
end
|
|
|
|
# Build a state sensor with value-to-description mapping
|
|
defp build_state_sensor(sensor_def, oid, value, idx, oid_index) when is_number(value) do
|
|
states = sensor_def[:states] || %{}
|
|
# Convert value to integer for state lookup (states are discrete)
|
|
int_value = trunc(value)
|
|
# Map numeric state to description, or use the raw value
|
|
state_descr = Map.get(states, int_value, "State #{int_value}")
|
|
|
|
# Convert states map keys to strings for JSON storage
|
|
states_for_json = Map.new(states, fn {k, v} -> {to_string(k), v} end)
|
|
|
|
# Use index_template from YAML if available, otherwise generate default index
|
|
sensor_index =
|
|
case sensor_def[:index_template] do
|
|
nil ->
|
|
"#{sensor_def[:oid_name] || "state"}_#{idx}"
|
|
|
|
template ->
|
|
# Replace {{ $index }} with the actual OID index
|
|
# Coerce to string since YAML parses bare numbers (e.g. `index: 0`) as integers
|
|
template |> to_string() |> String.replace("{{ $index }}", oid_index)
|
|
end
|
|
|
|
%{
|
|
sensor_type: "state",
|
|
sensor_index: sensor_index,
|
|
sensor_oid: oid,
|
|
sensor_descr: build_sensor_descr_default(sensor_def, idx),
|
|
sensor_unit: "",
|
|
sensor_divisor: 1,
|
|
last_value: value / 1,
|
|
state_descr: state_descr,
|
|
metadata: %{"states" => states_for_json}
|
|
}
|
|
end
|
|
|
|
defp build_state_sensor(_sensor_def, _oid, _value, _idx, _oid_index), do: nil
|
|
|
|
# Build sensor description, using the descr_map if available for actual device names
|
|
defp build_sensor_descr(sensor_def, idx, descr_map, oid_index) do
|
|
# First, try to get the description from the descr_map (walked from device)
|
|
case Map.get(descr_map, oid_index) do
|
|
descr when is_binary(descr) and descr != "" ->
|
|
# Use the actual device name directly
|
|
descr
|
|
|
|
_ ->
|
|
# Fall back to the default description building logic
|
|
build_sensor_descr_default(sensor_def, idx)
|
|
end
|
|
end
|
|
|
|
defp build_sensor_descr_default(sensor_def, idx) do
|
|
base_descr = sensor_def[:sensor_descr] || sensor_def[:oid_name] || sensor_def[:sensor_type]
|
|
|
|
# If the description contains a MIB symbolic name (e.g., "MIKROTIK-MIB::mtxrOpticalName"),
|
|
# use the sensor_type as fallback since we can't resolve MIB names to actual values here
|
|
base_descr =
|
|
if is_binary(base_descr) and String.contains?(base_descr, "::") do
|
|
sensor_def[:sensor_type] || "sensor"
|
|
else
|
|
base_descr
|
|
end
|
|
|
|
# If there's only one instance, don't add index
|
|
if idx == 1 do
|
|
String.capitalize(to_string(base_descr))
|
|
else
|
|
"#{String.capitalize(to_string(base_descr))} #{idx}"
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Collects all discovered sensor data for debugging purposes.
|
|
Includes YAML-defined sensors (processors, state, count, table) and wireless sensors.
|
|
"""
|
|
@spec collect_vendor_debug_data(map(), Client.connection_opts()) :: map()
|
|
def collect_vendor_debug_data(profile, client_opts) do
|
|
# Collect all sensors using the same discovery logic
|
|
{:ok, all_sensors} = discover_sensors(profile, client_opts)
|
|
|
|
# Format sensors for debug display with state descriptions
|
|
discovered_sensors =
|
|
Enum.map(all_sensors, fn sensor ->
|
|
base = %{
|
|
oid: sensor.sensor_oid,
|
|
descr: sensor.sensor_descr,
|
|
type: sensor.sensor_type,
|
|
value: sensor.last_value,
|
|
unit: sensor.sensor_unit || ""
|
|
}
|
|
|
|
# Add state description if present
|
|
if Map.has_key?(sensor, :state_descr) do
|
|
Map.put(base, :state_descr, sensor.state_descr)
|
|
else
|
|
base
|
|
end
|
|
end)
|
|
|
|
# Also collect raw wireless OID values (including errors for debugging)
|
|
wireless_oids = get_vendor_wireless_oids(profile)
|
|
|
|
wireless_raw =
|
|
Map.new(wireless_oids, fn sensor_def ->
|
|
value =
|
|
case Client.get(client_opts, sensor_def.oid) do
|
|
{:ok, val} -> val
|
|
{:error, reason} -> "ERROR: #{inspect(reason)}"
|
|
end
|
|
|
|
{sensor_def.sensor_type, %{oid: sensor_def.oid, descr: sensor_def.sensor_descr, value: value}}
|
|
end)
|
|
|
|
%{
|
|
discovered_sensors: discovered_sensors,
|
|
wireless_sensors: wireless_raw
|
|
}
|
|
end
|
|
|
|
# Get the wireless OID definitions for a profile (for debug data collection)
|
|
defp get_vendor_wireless_oids(%{name: name}) do
|
|
VendorRegistry.get_wireless_oid_defs(name)
|
|
end
|
|
|
|
defp get_vendor_wireless_oids(_profile), do: []
|
|
|
|
# Vendor-specific wireless sensor discovery using vendor modules
|
|
defp discover_vendor_wireless_sensors(%{name: name}, client_opts) do
|
|
VendorRegistry.discover_wireless_sensors(name, client_opts)
|
|
end
|
|
|
|
defp discover_vendor_wireless_sensors(_profile, _client_opts), do: []
|
|
|
|
# Deduplicate sensors by OID - when multiple sensors share the same OID,
|
|
# prefer the one with a proper description (not containing MIB symbolic names).
|
|
# Normalizes OIDs by stripping leading dots so ".1.3.6.1..." and "1.3.6.1..."
|
|
# are treated as the same OID (gosnmp returns leading dots, definitions don't).
|
|
defp deduplicate_by_oid(sensors) do
|
|
sensors
|
|
|> Enum.group_by(&String.trim_leading(&1.sensor_oid, "."))
|
|
|> Enum.map(fn {_oid, group} ->
|
|
# Prefer sensor with proper description (no MIB symbolic name like "MIKROTIK-MIB::xxx")
|
|
# Check case-insensitively since build_sensor_descr capitalizes only first letter
|
|
Enum.find(group, fn s -> not has_mib_symbolic_name?(s.sensor_descr) end) ||
|
|
List.last(group)
|
|
end)
|
|
end
|
|
|
|
# Check if a description contains a MIB symbolic name (e.g., "MIKROTIK-MIB::mtxrOpticalName")
|
|
# These should be filtered out in favor of actual sensor names from the device
|
|
defp has_mib_symbolic_name?(descr) when is_binary(descr) do
|
|
# MIB symbolic names contain "::" pattern (case-insensitive check)
|
|
String.contains?(String.downcase(descr), "::")
|
|
end
|
|
|
|
defp has_mib_symbolic_name?(_), do: false
|
|
|
|
# Applies vendor-specific post-processing to discovered sensors.
|
|
# This allows vendors to enhance or transform sensors after base discovery.
|
|
# For example, Arista converts DOM power sensors from watts to dBm and adds
|
|
# vendor-specific thresholds.
|
|
@spec apply_vendor_post_processing(map(), [map()], Client.connection_opts()) :: [map()]
|
|
defp apply_vendor_post_processing(%{name: name} = _profile, sensors, client_opts) do
|
|
case name do
|
|
"arista_eos" ->
|
|
# Apply Arista-specific enhancements
|
|
alias Towerops.Snmp.Profiles.Vendors.Arista
|
|
|
|
Arista.post_process_sensors(sensors, client_opts)
|
|
|
|
"arista-mos" ->
|
|
# Apply Arista-specific enhancements
|
|
alias Towerops.Snmp.Profiles.Vendors.Arista
|
|
|
|
Arista.post_process_sensors(sensors, client_opts)
|
|
|
|
"dell-powervault" ->
|
|
# Parse text-based sensor messages from FCMGMT-MIB
|
|
alias Towerops.Snmp.Profiles.Vendors.Powervault
|
|
|
|
Powervault.post_process_sensors(sensors, client_opts)
|
|
|
|
_ ->
|
|
# No vendor-specific post-processing
|
|
sensors
|
|
end
|
|
end
|
|
|
|
defp apply_vendor_post_processing(_profile, sensors, _client_opts), do: sensors
|
|
end
|