Replace all references with generic terms: - 'external YAML files' instead of specific project names - 'device profiles' for database-stored definitions - 'imported OID definitions' for sensor configurations This makes the codebase vendor-neutral and focused on the internal implementation rather than external dependencies.
203 lines
6.5 KiB
Elixir
203 lines
6.5 KiB
Elixir
defmodule Towerops.Snmp.Profiles.Dynamic do
|
|
@moduledoc """
|
|
Dynamic SNMP profile that interprets database-stored device profiles.
|
|
|
|
This profile uses the device_profiles table to dynamically discover
|
|
sensors and processors based on imported OID definitions.
|
|
"""
|
|
|
|
alias Towerops.DeviceProfiles
|
|
alias Towerops.DeviceProfiles.DeviceProfile
|
|
alias Towerops.Snmp.Client
|
|
alias Towerops.Snmp.Discovery
|
|
alias Towerops.Snmp.Profiles.Base
|
|
|
|
require Logger
|
|
|
|
@doc """
|
|
Identifies device using the database profile information.
|
|
Returns manufacturer, model, and firmware info.
|
|
|
|
Polls SNMP OIDs from OS definitions to extract version and serial number.
|
|
"""
|
|
@spec identify_device(DeviceProfile.t(), Client.connection_opts(), Discovery.system_info()) ::
|
|
Discovery.device_info()
|
|
def identify_device(profile, client_opts, _system_info) do
|
|
# Load profile with OS definitions
|
|
profile = DeviceProfiles.get_profile_with_associations(profile.os)
|
|
|
|
# Poll OS definitions for version, serial, etc.
|
|
os_data = poll_os_definitions(profile, client_opts)
|
|
|
|
# Use profile metadata to build device info
|
|
%{
|
|
manufacturer: extract_manufacturer(profile),
|
|
model: profile.text || profile.os,
|
|
firmware_version: os_data[:version] || profile.os,
|
|
serial_number: os_data[:serial]
|
|
}
|
|
end
|
|
|
|
@doc """
|
|
Discovers network interfaces using the base profile.
|
|
Dynamic profiles inherit standard IF-MIB interface discovery.
|
|
"""
|
|
@spec discover_interfaces(DeviceProfile.t(), Client.connection_opts()) ::
|
|
{:ok, [Discovery.interface_data()]} | {:error, term()}
|
|
def discover_interfaces(_profile, client_opts) do
|
|
# Use base profile for standard interface discovery
|
|
Base.discover_interfaces(client_opts)
|
|
end
|
|
|
|
@doc """
|
|
Discovers sensors dynamically using database profile definitions.
|
|
Walks SNMP OIDs defined in the profile's sensor_definitions.
|
|
"""
|
|
@spec discover_sensors(DeviceProfile.t(), Client.connection_opts()) ::
|
|
{:ok, [Discovery.sensor_data()]} | {:error, term()}
|
|
def discover_sensors(profile, client_opts) do
|
|
# Load profile with sensor definitions
|
|
profile = DeviceProfiles.get_profile_with_associations(profile.os)
|
|
|
|
if profile && length(profile.sensor_definitions) > 0 do
|
|
Logger.debug("Discovering sensors for #{profile.os} using #{length(profile.sensor_definitions)} definitions")
|
|
|
|
sensors =
|
|
Enum.flat_map(profile.sensor_definitions, fn sensor_def ->
|
|
discover_sensor_from_definition(client_opts, sensor_def)
|
|
end)
|
|
|
|
{:ok, sensors}
|
|
else
|
|
Logger.debug("No sensor definitions found for profile #{profile.os}")
|
|
{:ok, []}
|
|
end
|
|
end
|
|
|
|
# Private functions
|
|
|
|
defp poll_os_definitions(_profile, _client_opts) do
|
|
# Note: OS definition polling is currently disabled because device profiles
|
|
# use MIB symbolic names (e.g., CAMBIUM-PMP80211-MIB::cambiumCurrentuImageVersion.0)
|
|
# which require MIB compilation to resolve to numeric OIDs.
|
|
# The SNMP client only supports numeric OIDs.
|
|
# TODO: Add MIB resolution or import numeric OIDs during profile import
|
|
%{}
|
|
end
|
|
|
|
defp extract_manufacturer(%DeviceProfile{group: group}) when is_binary(group) do
|
|
# Capitalize manufacturer from group (e.g., "cambium" -> "Cambium")
|
|
String.capitalize(group)
|
|
end
|
|
|
|
defp extract_manufacturer(%DeviceProfile{text: text}) when is_binary(text) do
|
|
# Extract first word from text (e.g., "Ubiquiti AirFiber" -> "Ubiquiti")
|
|
text
|
|
|> String.split(" ")
|
|
|> List.first()
|
|
end
|
|
|
|
defp extract_manufacturer(_), do: "Unknown"
|
|
|
|
defp discover_sensor_from_definition(client_opts, sensor_def) do
|
|
# Try numeric OID first, fall back to named OID
|
|
oid = sensor_def.num_oid || sensor_def.oid
|
|
|
|
if oid do
|
|
# Remove template variables like ".{{ $index }}" from OID
|
|
# For walks, we need the base OID without the index placeholder
|
|
walk_oid = clean_oid_template(oid)
|
|
|
|
case Client.walk(client_opts, walk_oid) do
|
|
{:ok, results} when is_list(results) and results != [] ->
|
|
# Convert walk results to sensor data
|
|
Enum.map(results, fn result ->
|
|
build_sensor_data(sensor_def, result)
|
|
end)
|
|
|
|
{:ok, _empty} ->
|
|
Logger.debug("No data found for sensor OID: #{walk_oid}")
|
|
[]
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("Failed to walk sensor OID #{walk_oid}: #{inspect(reason)}")
|
|
[]
|
|
end
|
|
else
|
|
Logger.warning("Sensor definition missing OID: #{inspect(sensor_def)}")
|
|
[]
|
|
end
|
|
end
|
|
|
|
# Remove template variables from OID
|
|
# Examples:
|
|
# ".1.3.6.1.4.1.17713.21.2.1.36.{{ $index }}" -> ".1.3.6.1.4.1.17713.21.2.1.36"
|
|
# "sysUptime.{{ $index }}" -> "sysUptime"
|
|
defp clean_oid_template(oid) do
|
|
oid
|
|
|> String.replace(~r/\.?\{\{[^}]+\}\}/, "")
|
|
|> String.trim_trailing(".")
|
|
end
|
|
|
|
defp build_sensor_data(sensor_def, %{oid: oid, value: value}) do
|
|
# Build sensor index from OID (extract last component)
|
|
sensor_index = extract_index_from_oid(oid, sensor_def.index)
|
|
|
|
# Apply divisor to value if it's numeric
|
|
last_value =
|
|
cond do
|
|
is_number(value) && sensor_def.divisor && sensor_def.divisor > 1 ->
|
|
value / sensor_def.divisor
|
|
|
|
is_number(value) ->
|
|
value * 1.0
|
|
|
|
true ->
|
|
nil
|
|
end
|
|
|
|
%{
|
|
sensor_type: sensor_def.sensor_class,
|
|
sensor_index: sensor_index,
|
|
sensor_oid: oid,
|
|
sensor_descr: sensor_def.descr || "#{sensor_def.sensor_class} #{sensor_index}",
|
|
sensor_unit: determine_unit(sensor_def.sensor_class),
|
|
sensor_divisor: sensor_def.divisor || 1,
|
|
last_value: last_value,
|
|
status: "ok"
|
|
}
|
|
end
|
|
|
|
defp extract_index_from_oid(oid, configured_index) when is_binary(configured_index) do
|
|
# If index is pre-configured, use it
|
|
if configured_index == "" do
|
|
# Extract last component of OID as index
|
|
oid
|
|
|> String.split(".")
|
|
|> List.last()
|
|
|> to_string()
|
|
else
|
|
configured_index
|
|
end
|
|
end
|
|
|
|
defp extract_index_from_oid(oid, _) do
|
|
# Extract last component of OID as index
|
|
oid
|
|
|> String.split(".")
|
|
|> List.last()
|
|
|> to_string()
|
|
end
|
|
|
|
defp determine_unit("temperature"), do: "°C"
|
|
defp determine_unit("voltage"), do: "V"
|
|
defp determine_unit("current"), do: "A"
|
|
defp determine_unit("power"), do: "W"
|
|
defp determine_unit("fanspeed"), do: "RPM"
|
|
defp determine_unit("humidity"), do: "%"
|
|
defp determine_unit("dbm"), do: "dBm"
|
|
defp determine_unit("snr"), do: "dB"
|
|
defp determine_unit("percent"), do: "%"
|
|
defp determine_unit("frequency"), do: "Hz"
|
|
defp determine_unit(_), do: ""
|
|
end
|