feat: add wireless sensor discovery and fix YAML sensor parsing
- Add wireless sensor discovery for ePMP and AirOS devices - Implement vendor-specific OIDs matching LibreNMS PHP discovery - Fix table/state sensor discovery to handle map format from Client.walk - Add discovered_sensors to debug data with values and state descriptions - ePMP: frequency, clients, rssi, snr (rssi/snr for SM only) - AirOS: frequency, capacity, ccq, clients, distance, noise-floor, power, quality, rate, rssi, utilization
This commit is contained in:
parent
12875d7f48
commit
74c9544688
4 changed files with 913 additions and 43 deletions
|
|
@ -36,16 +36,26 @@ defmodule Towerops.Profiles.YamlProfiles do
|
|||
|> Enum.map(fn {_, profile} -> profile end)
|
||||
end
|
||||
|
||||
# Generic OS profiles that are checked last (like LibreNMS)
|
||||
# These are fallback profiles for common Linux-based devices
|
||||
@generic_os ~w(airos freebsd linux)
|
||||
|
||||
@doc """
|
||||
Matches a profile based on system info (sysObjectID or sysDescr).
|
||||
Returns the best matching profile or nil.
|
||||
|
||||
Uses LibreNMS's two-pass detection approach:
|
||||
1. First pass: Check profiles WITHOUT snmpget/snmpwalk conditions (except generic OS)
|
||||
2. Second pass: Check profiles WITH snmpget/snmpwalk conditions (deferred)
|
||||
- Since we can't evaluate snmpget, these will effectively be skipped
|
||||
3. Third pass: Check generic OS (airos, freebsd, linux) as final fallback
|
||||
"""
|
||||
def match_profile(system_info) do
|
||||
sys_object_id = Map.get(system_info, :sys_object_id, "")
|
||||
sys_descr = Map.get(system_info, :sys_descr, "")
|
||||
|
||||
# First try to match by sysObjectID (more specific)
|
||||
case match_by_oid(sys_object_id) do
|
||||
# First try to match by sysObjectID using LibreNMS's approach
|
||||
case match_by_oid_librenms_style(sys_object_id, sys_descr) do
|
||||
nil -> match_by_descr(sys_descr)
|
||||
profile -> profile
|
||||
end
|
||||
|
|
@ -132,26 +142,108 @@ defmodule Towerops.Profiles.YamlProfiles do
|
|||
vendor: Map.get(detection, "text") || String.capitalize(name),
|
||||
detection_oid: extract_detection_oid(detection),
|
||||
detection_patterns: extract_detection_patterns(detection),
|
||||
# Compound detection: sysDescr/sysDescr_regex that must match alongside sysObjectID
|
||||
detection_descr_patterns: extract_detection_descr_patterns(detection),
|
||||
detection_descr_regex: extract_detection_descr_regex(detection),
|
||||
# Extract all detection blocks with their conditions for per-block matching
|
||||
detection_blocks: extract_detection_blocks(detection),
|
||||
device_oids: extract_device_oids(discovery),
|
||||
sensor_oids: extract_sensor_oids(discovery)
|
||||
# Scalar sensors (direct OID polling)
|
||||
sensor_oids: extract_sensor_oids(discovery),
|
||||
# Table-based sensors (OID walks) - all standard sensor types
|
||||
table_sensor_oids: extract_table_sensor_oids(discovery),
|
||||
# Additional sensor types like LibreNMS
|
||||
processor_oids: extract_processor_oids(discovery),
|
||||
count_sensor_oids: extract_count_sensor_oids(discovery),
|
||||
state_sensor_oids: extract_state_sensor_oids(discovery)
|
||||
}
|
||||
end
|
||||
|
||||
defp extract_detection_oid(yaml) do
|
||||
case get_in(yaml, ["discovery"]) do
|
||||
discovery when is_list(discovery) ->
|
||||
# Extract OIDs from all discovery blocks, including those with snmpget/snmpwalk conditions
|
||||
# We can't evaluate snmpget conditions, but we can still match on OID + sysDescr constraints
|
||||
# This allows profiles like airos (which has snmpget condition) to still match by OID
|
||||
discovery
|
||||
|> Enum.flat_map(fn
|
||||
%{"sysObjectID" => patterns} when is_list(patterns) -> patterns
|
||||
%{"sysObjectID" => pattern} when is_binary(pattern) -> [pattern]
|
||||
_ -> []
|
||||
end)
|
||||
|> List.first()
|
||||
|> normalize_oid()
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Extract all detection blocks with their OIDs, sysDescr constraints, and condition flags
|
||||
# This enables per-block matching (a profile may have multiple detection blocks)
|
||||
defp extract_detection_blocks(yaml) do
|
||||
case get_in(yaml, ["discovery"]) do
|
||||
discovery when is_list(discovery) ->
|
||||
Enum.flat_map(discovery, &parse_discovery_block/1)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_discovery_block(block) do
|
||||
oids = extract_oids_from_block(block)
|
||||
has_condition = Map.has_key?(block, "snmpget") or Map.has_key?(block, "snmpwalk")
|
||||
descr_patterns = extract_descr_from_block(block)
|
||||
descr_regex = extract_descr_regex_from_block(block)
|
||||
|
||||
Enum.map(oids, fn oid ->
|
||||
%{
|
||||
oid: normalize_oid(oid),
|
||||
has_condition: has_condition,
|
||||
descr_patterns: descr_patterns,
|
||||
descr_regex: descr_regex
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp extract_oids_from_block(block) do
|
||||
case Map.get(block, "sysObjectID") do
|
||||
patterns when is_list(patterns) -> patterns
|
||||
pattern when is_binary(pattern) -> [pattern]
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_descr_from_block(block) do
|
||||
case Map.get(block, "sysDescr") do
|
||||
patterns when is_list(patterns) -> patterns
|
||||
pattern when is_binary(pattern) -> [pattern]
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_descr_regex_from_block(block) do
|
||||
case Map.get(block, "sysDescr_regex") do
|
||||
patterns when is_list(patterns) ->
|
||||
patterns |> Enum.map(&parse_regex_pattern/1) |> Enum.reject(&is_nil/1)
|
||||
|
||||
pattern when is_binary(pattern) ->
|
||||
case parse_regex_pattern(pattern) do
|
||||
nil -> []
|
||||
regex -> [regex]
|
||||
end
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
# Strip leading dot from OID (LibreNMS uses .1.3.6... but SNMP returns 1.3.6...)
|
||||
defp normalize_oid(nil), do: nil
|
||||
defp normalize_oid("." <> rest), do: rest
|
||||
defp normalize_oid(oid), do: oid
|
||||
|
||||
defp extract_detection_patterns(yaml) do
|
||||
case get_in(yaml, ["discovery"]) do
|
||||
discovery when is_list(discovery) ->
|
||||
|
|
@ -165,6 +257,64 @@ defmodule Towerops.Profiles.YamlProfiles do
|
|||
end
|
||||
end
|
||||
|
||||
# Extract sysDescr patterns that must match alongside sysObjectID (compound detection)
|
||||
defp extract_detection_descr_patterns(yaml) do
|
||||
case get_in(yaml, ["discovery"]) do
|
||||
discovery when is_list(discovery) ->
|
||||
# Only extract from discovery blocks that have BOTH sysObjectID and sysDescr
|
||||
Enum.flat_map(discovery, fn
|
||||
%{"sysObjectID" => _, "sysDescr" => patterns} when is_list(patterns) -> patterns
|
||||
%{"sysObjectID" => _, "sysDescr" => pattern} when is_binary(pattern) -> [pattern]
|
||||
_ -> []
|
||||
end)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
# Extract sysDescr_regex patterns that must match alongside sysObjectID (compound detection)
|
||||
defp extract_detection_descr_regex(yaml) do
|
||||
case get_in(yaml, ["discovery"]) do
|
||||
discovery when is_list(discovery) ->
|
||||
# Only extract from discovery blocks that have BOTH sysObjectID and sysDescr_regex
|
||||
discovery
|
||||
|> Enum.flat_map(fn
|
||||
%{"sysObjectID" => _, "sysDescr_regex" => patterns} when is_list(patterns) ->
|
||||
Enum.map(patterns, &parse_regex_pattern/1)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
# Parse LibreNMS regex pattern format: '/pattern/' or '/pattern/i'
|
||||
defp parse_regex_pattern(pattern) when is_binary(pattern) do
|
||||
case Regex.run(~r/^\/(.+)\/([i]?)$/, pattern) do
|
||||
[_, regex_str, "i"] ->
|
||||
case Regex.compile(regex_str, [:caseless]) do
|
||||
{:ok, regex} -> regex
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
[_, regex_str, ""] ->
|
||||
case Regex.compile(regex_str) do
|
||||
{:ok, regex} -> regex
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_regex_pattern(_), do: nil
|
||||
|
||||
defp extract_device_oids(yaml) do
|
||||
os_module = get_in(yaml, ["modules", "os"]) || %{}
|
||||
|
||||
|
|
@ -188,15 +338,19 @@ defmodule Towerops.Profiles.YamlProfiles do
|
|||
|> Map.new()
|
||||
end
|
||||
|
||||
# All sensor types supported by LibreNMS
|
||||
@sensor_types ~w(temperature voltage current power fanspeed humidity frequency
|
||||
dbm signal snr airflow load runtime waterflow pressure cooling charge)
|
||||
|
||||
# Extract scalar sensors (direct OID polling with .0 suffix)
|
||||
defp extract_sensor_oids(yaml) do
|
||||
sensors_module = get_in(yaml, ["modules", "sensors"]) || %{}
|
||||
sensor_types = ["temperature", "voltage", "current", "power", "fanspeed"]
|
||||
|
||||
Enum.flat_map(sensor_types, fn sensor_type ->
|
||||
Enum.flat_map(@sensor_types, fn sensor_type ->
|
||||
case get_in(sensors_module, [sensor_type, "data"]) do
|
||||
data when is_list(data) ->
|
||||
data
|
||||
|> Enum.map(&parse_sensor(&1, sensor_type))
|
||||
|> Enum.map(&parse_scalar_sensor(&1, sensor_type))
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
_ ->
|
||||
|
|
@ -205,26 +359,64 @@ defmodule Towerops.Profiles.YamlProfiles do
|
|||
end)
|
||||
end
|
||||
|
||||
defp parse_sensor(sensor_def, sensor_type) do
|
||||
# Extract table-based sensors (OID walks with {{ $index }})
|
||||
defp extract_table_sensor_oids(yaml) do
|
||||
sensors_module = get_in(yaml, ["modules", "sensors"]) || %{}
|
||||
|
||||
Enum.flat_map(@sensor_types, fn sensor_type ->
|
||||
case get_in(sensors_module, [sensor_type, "data"]) do
|
||||
data when is_list(data) ->
|
||||
data
|
||||
|> Enum.map(&parse_table_sensor_def(&1, sensor_type))
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# Parse a scalar sensor (direct OID ending in .0)
|
||||
defp parse_scalar_sensor(sensor_def, sensor_type) do
|
||||
mib_name = Map.get(sensor_def, "oid") || Map.get(sensor_def, "value")
|
||||
num_oid = Map.get(sensor_def, "num_oid")
|
||||
|
||||
# Skip table-based sensors (with {{ $index }})
|
||||
cond do
|
||||
is_binary(num_oid) and String.contains?(num_oid, "{{ $index }}") ->
|
||||
nil
|
||||
# Only include if it's a scalar sensor (no {{ $index }} and ends with .0)
|
||||
is_table = is_binary(num_oid) and String.contains?(num_oid, "{{ $index }}")
|
||||
is_scalar = is_binary(mib_name) and String.ends_with?(mib_name, ".0")
|
||||
|
||||
is_binary(mib_name) and String.ends_with?(mib_name, ".0") ->
|
||||
if !is_table and is_scalar do
|
||||
%{
|
||||
sensor_type: sensor_type,
|
||||
mib_name: mib_name,
|
||||
sensor_descr: extract_descr(sensor_def, sensor_type),
|
||||
sensor_unit: Map.get(sensor_def, "unit"),
|
||||
sensor_divisor: Map.get(sensor_def, "divisor", 1)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Parse a table-based sensor definition (has {{ $index }} placeholder)
|
||||
defp parse_table_sensor_def(sensor_def, sensor_type) do
|
||||
num_oid = Map.get(sensor_def, "num_oid")
|
||||
oid_name = Map.get(sensor_def, "oid")
|
||||
|
||||
# Only include if it's a table sensor (has {{ $index }})
|
||||
if is_binary(num_oid) and String.contains?(num_oid, "{{ $index }}") do
|
||||
base_oid = extract_base_oid(num_oid)
|
||||
|
||||
if base_oid do
|
||||
%{
|
||||
sensor_type: sensor_type,
|
||||
mib_name: mib_name,
|
||||
base_oid: base_oid,
|
||||
oid_name: oid_name,
|
||||
sensor_descr: extract_descr(sensor_def, sensor_type),
|
||||
sensor_unit: Map.get(sensor_def, "unit"),
|
||||
sensor_divisor: Map.get(sensor_def, "divisor", 1)
|
||||
sensor_divisor: Map.get(sensor_def, "divisor", 1),
|
||||
precision: Map.get(sensor_def, "precision", 1),
|
||||
table: true
|
||||
}
|
||||
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -242,16 +434,203 @@ defmodule Towerops.Profiles.YamlProfiles do
|
|||
end
|
||||
end
|
||||
|
||||
defp match_by_oid(sys_object_id) when is_binary(sys_object_id) and sys_object_id != "" do
|
||||
# Try exact match first, then prefix matches
|
||||
list_profiles()
|
||||
|> Enum.filter(fn profile ->
|
||||
profile.detection_oid && String.contains?(sys_object_id, profile.detection_oid)
|
||||
end)
|
||||
|> Enum.max_by(fn profile -> String.length(profile.detection_oid || "") end, fn -> nil end)
|
||||
# Extract processor/CPU sensors (like LibreNMS processors module)
|
||||
defp extract_processor_oids(yaml) do
|
||||
case get_in(yaml, ["modules", "processors", "data"]) do
|
||||
data when is_list(data) ->
|
||||
data
|
||||
|> Enum.map(&parse_table_sensor(&1, "processor", "%"))
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp match_by_oid(_), do: nil
|
||||
# Extract count sensors (like LibreNMS sensors.count module)
|
||||
defp extract_count_sensor_oids(yaml) do
|
||||
case get_in(yaml, ["modules", "sensors", "count", "data"]) do
|
||||
data when is_list(data) ->
|
||||
data
|
||||
|> Enum.map(&parse_table_sensor(&1, "count", ""))
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
# Extract state sensors (like LibreNMS sensors.state module)
|
||||
defp extract_state_sensor_oids(yaml) do
|
||||
case get_in(yaml, ["modules", "sensors", "state", "data"]) do
|
||||
data when is_list(data) ->
|
||||
data
|
||||
|> Enum.map(&parse_state_sensor/1)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
# Parse a table-based sensor (walks the OID table to find all instances)
|
||||
defp parse_table_sensor(sensor_def, sensor_type, default_unit) do
|
||||
num_oid = Map.get(sensor_def, "num_oid")
|
||||
oid_name = Map.get(sensor_def, "oid")
|
||||
|
||||
base_oid = extract_base_oid(num_oid)
|
||||
|
||||
if base_oid do
|
||||
%{
|
||||
sensor_type: sensor_type,
|
||||
base_oid: base_oid,
|
||||
oid_name: oid_name,
|
||||
sensor_descr: extract_descr(sensor_def, sensor_type),
|
||||
sensor_unit: Map.get(sensor_def, "unit") || default_unit,
|
||||
sensor_divisor: Map.get(sensor_def, "divisor", 1),
|
||||
precision: Map.get(sensor_def, "precision", 1),
|
||||
table: true
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Parse a state sensor with discrete values
|
||||
defp parse_state_sensor(sensor_def) do
|
||||
num_oid = Map.get(sensor_def, "num_oid")
|
||||
oid_name = Map.get(sensor_def, "oid")
|
||||
states = Map.get(sensor_def, "states", [])
|
||||
|
||||
base_oid = extract_base_oid(num_oid)
|
||||
|
||||
if base_oid do
|
||||
# Build state mapping: value => description
|
||||
state_map =
|
||||
Map.new(states, fn state ->
|
||||
{Map.get(state, "value"), Map.get(state, "descr", "Unknown")}
|
||||
end)
|
||||
|
||||
%{
|
||||
sensor_type: "state",
|
||||
base_oid: base_oid,
|
||||
oid_name: oid_name,
|
||||
sensor_descr: extract_descr(sensor_def, "state"),
|
||||
sensor_unit: "",
|
||||
states: state_map,
|
||||
table: true
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Extract base OID from num_oid by removing the {{ $index }} placeholder
|
||||
defp extract_base_oid(nil), do: nil
|
||||
|
||||
defp extract_base_oid(num_oid) when is_binary(num_oid) do
|
||||
# Remove {{ $index }} and trailing dot, clean up the OID
|
||||
num_oid
|
||||
|> String.replace(~r/\.\{\{\s*\$index\s*\}\}/, "")
|
||||
|> String.replace(~r/^\.*/, "")
|
||||
|> case do
|
||||
"" -> nil
|
||||
oid -> oid
|
||||
end
|
||||
end
|
||||
|
||||
# LibreNMS-style OS detection with proper priority ordering
|
||||
# 1. First pass: Match profiles by unconditional blocks (no snmpget/snmpwalk), excluding generic_os
|
||||
# 2. Second pass: Generic OS (airos, freebsd, linux) with unconditional blocks as fallback
|
||||
# 3. Third pass: Generic OS with conditional blocks (last resort for profiles like airos)
|
||||
#
|
||||
# We check per-block, not per-profile. A profile may have both conditional and unconditional
|
||||
# blocks for different OIDs (e.g., ibm-imm has snmpget block for one OID and plain block for another)
|
||||
defp match_by_oid_librenms_style(sys_object_id, sys_descr) when is_binary(sys_object_id) and sys_object_id != "" do
|
||||
all_profiles = list_profiles()
|
||||
|
||||
# Categorize profiles
|
||||
{generic_profiles, other_profiles} =
|
||||
Enum.split_with(all_profiles, fn p -> p.name in @generic_os end)
|
||||
|
||||
# First pass: non-generic profiles with unconditional matching blocks
|
||||
case find_best_unconditional_match(other_profiles, sys_object_id, sys_descr) do
|
||||
nil ->
|
||||
# Second pass: generic OS with unconditional blocks
|
||||
case find_best_unconditional_match(generic_profiles, sys_object_id, sys_descr) do
|
||||
nil ->
|
||||
# Third pass: generic OS with conditional blocks (for profiles like airos)
|
||||
find_best_conditional_match(generic_profiles, sys_object_id, sys_descr)
|
||||
|
||||
profile ->
|
||||
profile
|
||||
end
|
||||
|
||||
profile ->
|
||||
profile
|
||||
end
|
||||
end
|
||||
|
||||
defp match_by_oid_librenms_style(_, _), do: nil
|
||||
|
||||
# Find best match using only unconditional detection blocks (no snmpget/snmpwalk)
|
||||
defp find_best_unconditional_match(profiles, sys_object_id, sys_descr) do
|
||||
find_best_block_match(profiles, sys_object_id, sys_descr, false)
|
||||
end
|
||||
|
||||
# Find best match using conditional detection blocks (has snmpget/snmpwalk)
|
||||
defp find_best_conditional_match(profiles, sys_object_id, sys_descr) do
|
||||
find_best_block_match(profiles, sys_object_id, sys_descr, true)
|
||||
end
|
||||
|
||||
# Find the best matching profile based on detection blocks
|
||||
defp find_best_block_match(profiles, sys_object_id, sys_descr, match_conditional) do
|
||||
profiles
|
||||
|> Enum.flat_map(&find_profile_matching_blocks(&1, sys_object_id, sys_descr, match_conditional))
|
||||
|> select_best_match()
|
||||
end
|
||||
|
||||
defp find_profile_matching_blocks(profile, sys_object_id, sys_descr, match_conditional) do
|
||||
matching_blocks =
|
||||
Enum.filter(profile.detection_blocks, fn block ->
|
||||
block_matches?(block, sys_object_id, sys_descr, match_conditional)
|
||||
end)
|
||||
|
||||
case matching_blocks do
|
||||
[] -> []
|
||||
blocks -> [{profile, Enum.max_by(blocks, &String.length(&1.oid || ""))}]
|
||||
end
|
||||
end
|
||||
|
||||
defp block_matches?(block, sys_object_id, sys_descr, match_conditional) do
|
||||
block.has_condition == match_conditional &&
|
||||
block.oid &&
|
||||
String.contains?(sys_object_id, block.oid) &&
|
||||
block_matches_descr?(block, sys_descr)
|
||||
end
|
||||
|
||||
defp select_best_match([]), do: nil
|
||||
|
||||
defp select_best_match(matches) do
|
||||
{profile, _block} = Enum.max_by(matches, fn {_p, block} -> String.length(block.oid || "") end)
|
||||
profile
|
||||
end
|
||||
|
||||
# Check if a detection block's sysDescr constraints are satisfied
|
||||
defp block_matches_descr?(block, sys_descr) do
|
||||
patterns = block.descr_patterns || []
|
||||
regexes = block.descr_regex || []
|
||||
|
||||
cond do
|
||||
# No constraints - OID match is sufficient
|
||||
patterns == [] and regexes == [] ->
|
||||
true
|
||||
|
||||
# Has pattern constraints - must match at least one
|
||||
patterns != [] ->
|
||||
Enum.any?(patterns, fn pattern -> String.contains?(sys_descr, pattern) end)
|
||||
|
||||
# Has regex constraints - must match at least one
|
||||
regexes != [] ->
|
||||
Enum.any?(regexes, fn regex -> Regex.match?(regex, sys_descr) end)
|
||||
end
|
||||
end
|
||||
|
||||
defp match_by_descr(sys_descr) when is_binary(sys_descr) and sys_descr != "" do
|
||||
Enum.find(list_profiles(), fn profile ->
|
||||
|
|
|
|||
|
|
@ -286,7 +286,22 @@ defmodule Towerops.Snmp.Discovery do
|
|||
defp build_device_info(client_opts, system_info, {:yaml, profile}) do
|
||||
# Use YAML profile to identify device
|
||||
identified_info = Dynamic.identify_device(profile, client_opts, system_info)
|
||||
{:ok, identified_info}
|
||||
|
||||
# Collect raw debug data using Base profile (works for any device)
|
||||
raw_data = Base.collect_raw_debug_data(client_opts)
|
||||
|
||||
# Add vendor-specific debug data (wireless sensors, etc.)
|
||||
vendor_data = Dynamic.collect_vendor_debug_data(profile, client_opts)
|
||||
raw_data_with_vendor = Map.merge(raw_data, vendor_data)
|
||||
|
||||
Logger.info("Collected raw debug data for YAML profile: #{map_size(raw_data_with_vendor)} keys")
|
||||
|
||||
identified_info_with_debug =
|
||||
identified_info
|
||||
|> Map.put(:_raw_discovery_data, raw_data_with_vendor)
|
||||
|> Map.put(:_last_discovery_at, DateTime.utc_now())
|
||||
|
||||
{:ok, identified_info_with_debug}
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("Failed to identify device with YAML profile: #{inspect(error)}")
|
||||
|
|
|
|||
|
|
@ -28,11 +28,17 @@ defmodule Towerops.Snmp.Profiles.Dynamic do
|
|||
# Format firmware version (MikroTik has special formatting with license level)
|
||||
firmware_version = format_firmware_version(profile, device_data)
|
||||
|
||||
# Get hardware model - use vendor-specific detection, then YAML hardware OID, then sysDescr
|
||||
model =
|
||||
detect_vendor_hardware(profile, client_opts) ||
|
||||
Map.get(device_data, :hardware) ||
|
||||
system_info[:sys_descr]
|
||||
|
||||
# Merge with system_info
|
||||
system_info
|
||||
|> Map.merge(%{
|
||||
manufacturer: profile.vendor,
|
||||
model: system_info[:sys_descr],
|
||||
model: model,
|
||||
firmware_version: firmware_version,
|
||||
serial_number: Map.get(device_data, :serial_number),
|
||||
latitude: Map.get(device_data, :latitude),
|
||||
|
|
@ -42,28 +48,51 @@ defmodule Towerops.Snmp.Profiles.Dynamic do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Discovers sensors using profile's sensor_oids.
|
||||
Translates MIB names and polls sensors.
|
||||
Falls back to Base ENTITY-SENSOR-MIB discovery if profile has no sensor_oids.
|
||||
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
|
||||
|
||||
sensor_oids = Map.get(profile, :sensor_oids, [])
|
||||
# Always discover ENTITY-SENSOR-MIB sensors (most devices support this)
|
||||
{:ok, base_sensors} = Base.discover_sensors(client_opts)
|
||||
|
||||
if Enum.empty?(sensor_oids) do
|
||||
# Fall back to ENTITY-SENSOR-MIB discovery from Base profile
|
||||
Base.discover_sensors(client_opts)
|
||||
else
|
||||
sensors =
|
||||
sensor_oids
|
||||
|> Enum.map(&fetch_sensor_value(&1, client_opts))
|
||||
|> Enum.reject(&is_nil/1)
|
||||
# Scalar sensors from profile (direct OID polling)
|
||||
scalar_sensors = Map.get(profile, :sensor_oids, [])
|
||||
|
||||
{:ok, sensors}
|
||||
end
|
||||
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
|
||||
|
||||
{:ok, all_sensors}
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -77,8 +106,71 @@ defmodule Towerops.Snmp.Profiles.Dynamic do
|
|||
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 (like LibreNMS OS PHP files)
|
||||
# ePMP: Uses wirelessInterfaceMode and cambiumSubModeType to determine AP/SM and ePTP/ePMP
|
||||
defp detect_vendor_hardware(%{name: "epmp"}, client_opts) do
|
||||
detect_epmp_hardware(client_opts)
|
||||
end
|
||||
|
||||
# AirOS: Uses IEEE802dot11-MIB::dot11manufacturerProductName (mirrors Airos.php)
|
||||
defp detect_vendor_hardware(%{name: name}, client_opts)
|
||||
when name in ["airos", "airos-af", "airos-af60", "airos-af-ltu"] do
|
||||
detect_airos_hardware(client_opts)
|
||||
end
|
||||
|
||||
defp detect_vendor_hardware(_profile, _client_opts), do: nil
|
||||
|
||||
# AirOS hardware detection - mirrors LibreNMS Airos.php discoverOS()
|
||||
# Uses GETNEXT on IEEE802dot11-MIB::dot11manufacturerProductName table
|
||||
defp detect_airos_hardware(client_opts) do
|
||||
# IEEE802dot11-MIB::dot11manufacturerProductName OID base
|
||||
product_name_oid = "1.2.840.10036.1.1.1.9"
|
||||
|
||||
# Use walk to get the first entry (LibreNMS uses GETNEXT)
|
||||
case Client.walk(client_opts, product_name_oid) do
|
||||
{:ok, [%{value: value} | _]} when is_binary(value) and value != "" ->
|
||||
value
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# ePMP hardware detection - mirrors LibreNMS Epmp.php discoverOS()
|
||||
defp detect_epmp_hardware(client_opts) do
|
||||
interface_mode_oid = "1.3.6.1.4.1.17713.21.1.1.1.0"
|
||||
sub_mode_oid = "1.3.6.1.4.1.17713.21.1.1.2.0"
|
||||
|
||||
with {:ok, interface_mode} <- Client.get(client_opts, interface_mode_oid),
|
||||
{:ok, sub_mode} <- Client.get(client_opts, sub_mode_oid) do
|
||||
case {interface_mode, sub_mode} do
|
||||
# AP mode (1)
|
||||
{1, 5} -> "ePTP Master"
|
||||
{1, _} -> "ePMP AP"
|
||||
# SM mode (2)
|
||||
{2, 4} -> "ePTP Slave"
|
||||
{2, _} -> "ePMP SM"
|
||||
_ -> nil
|
||||
end
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
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) ->
|
||||
|
|
@ -135,4 +227,386 @@ defmodule Towerops.Snmp.Profiles.Dynamic do
|
|||
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]
|
||||
|
||||
case Client.walk(client_opts, base_oid) do
|
||||
{:ok, results} when is_map(results) and map_size(results) > 0 ->
|
||||
parse_table_walk_results(results, sensor_def)
|
||||
|
||||
{:ok, results} when is_list(results) and results != [] ->
|
||||
# Handle list format if Client returns that
|
||||
parse_table_walk_results_list(results, sensor_def)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_table_walk_results(results, sensor_def) when is_map(results) do
|
||||
results
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.map(fn {{oid, value}, idx} ->
|
||||
build_table_sensor(sensor_def, oid, value, idx)
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
defp parse_table_walk_results_list(results, sensor_def) do
|
||||
results
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.map(fn {%{oid: oid, value: value}, idx} ->
|
||||
build_table_sensor(sensor_def, oid, value, idx)
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
# Build a sensor from table walk result
|
||||
defp build_table_sensor(sensor_def, oid, value, idx) when is_integer(value) do
|
||||
%{
|
||||
sensor_type: sensor_def[:sensor_type],
|
||||
sensor_index: "#{sensor_def[:sensor_type]}_#{idx}",
|
||||
sensor_oid: oid,
|
||||
sensor_descr: build_sensor_descr(sensor_def, idx),
|
||||
sensor_unit: sensor_def[:sensor_unit] || "",
|
||||
sensor_divisor: sensor_def[:sensor_divisor] || 1,
|
||||
sensor_value: value
|
||||
}
|
||||
end
|
||||
|
||||
defp build_table_sensor(_sensor_def, _oid, _value, _idx), 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)
|
||||
|
||||
{:ok, results} when is_list(results) and results != [] ->
|
||||
# Handle list format if Client returns that
|
||||
parse_state_walk_results_list(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} ->
|
||||
build_state_sensor(sensor_def, oid, value, idx)
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
defp parse_state_walk_results_list(results, sensor_def) do
|
||||
results
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.map(fn {%{oid: oid, value: value}, idx} ->
|
||||
build_state_sensor(sensor_def, oid, value, idx)
|
||||
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) when is_integer(value) do
|
||||
states = sensor_def[:states] || %{}
|
||||
# Map numeric state to description, or use the raw value
|
||||
state_descr = Map.get(states, value, "State #{value}")
|
||||
|
||||
%{
|
||||
sensor_type: "state",
|
||||
sensor_index: "#{sensor_def[:oid_name] || "state"}_#{idx}",
|
||||
sensor_oid: oid,
|
||||
sensor_descr: build_sensor_descr(sensor_def, idx),
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1,
|
||||
sensor_value: value,
|
||||
state_descr: state_descr
|
||||
}
|
||||
end
|
||||
|
||||
defp build_state_sensor(_sensor_def, _oid, _value, _idx), do: nil
|
||||
|
||||
defp build_sensor_descr(sensor_def, idx) do
|
||||
base_descr = sensor_def[:sensor_descr] || sensor_def[:oid_name] || sensor_def[:sensor_type]
|
||||
|
||||
# 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.sensor_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}) when name in ["airos", "airos-af", "airos-af60", "airos-af-ltu"] do
|
||||
airos_wireless_oid_defs()
|
||||
end
|
||||
|
||||
defp get_vendor_wireless_oids(%{name: "epmp"}) do
|
||||
epmp_wireless_oid_defs()
|
||||
end
|
||||
|
||||
defp get_vendor_wireless_oids(_profile), do: []
|
||||
|
||||
# Vendor-specific wireless sensor discovery
|
||||
# Mirrors LibreNMS PHP-based discovery (e.g., Airos.php, Epmp.php)
|
||||
|
||||
defp discover_vendor_wireless_sensors(%{name: name}, client_opts)
|
||||
when name in ["airos", "airos-af", "airos-af60", "airos-af-ltu"] do
|
||||
discover_airos_wireless_sensors(client_opts)
|
||||
end
|
||||
|
||||
defp discover_vendor_wireless_sensors(%{name: "epmp"}, client_opts) do
|
||||
discover_epmp_wireless_sensors(client_opts)
|
||||
end
|
||||
|
||||
defp discover_vendor_wireless_sensors(_profile, _client_opts), do: []
|
||||
|
||||
# AirOS wireless sensors - mirrors LibreNMS/OS/Airos.php
|
||||
# Implements: frequency, capacity, ccq, clients, distance, noise-floor, power, quality, rate, rssi, utilization
|
||||
defp discover_airos_wireless_sensors(client_opts) do
|
||||
fetch_wireless_sensors(airos_wireless_oid_defs(), client_opts)
|
||||
end
|
||||
|
||||
# AirOS wireless OID definitions (reused for discovery and debug data)
|
||||
defp airos_wireless_oid_defs do
|
||||
[
|
||||
# Frequency in MHz
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.1.1.4.1",
|
||||
sensor_type: "frequency",
|
||||
sensor_descr: "Frequency",
|
||||
sensor_unit: "MHz",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Capacity percentage
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.6.1.4.1",
|
||||
sensor_type: "capacity",
|
||||
sensor_descr: "Capacity",
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Client Connection Quality
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.7.1",
|
||||
sensor_type: "ccq",
|
||||
sensor_descr: "CCQ",
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Connected clients count
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.15.1",
|
||||
sensor_type: "clients",
|
||||
sensor_descr: "Clients",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Distance in meters (value is in mm, divide by 1000)
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.1.1.7.1",
|
||||
sensor_type: "distance",
|
||||
sensor_descr: "Distance",
|
||||
sensor_unit: "m",
|
||||
sensor_divisor: 1000
|
||||
},
|
||||
# Noise floor in dBm
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.8.1",
|
||||
sensor_type: "noise-floor",
|
||||
sensor_descr: "Noise Floor",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# TX Power in dBm
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.1.1.6.1",
|
||||
sensor_type: "power",
|
||||
sensor_descr: "TX Power",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Signal quality percentage
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.6.1.3.1",
|
||||
sensor_type: "quality",
|
||||
sensor_descr: "Signal Quality",
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# TX Rate in Mbps
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.9.1",
|
||||
sensor_type: "rate-tx",
|
||||
sensor_descr: "TX Rate",
|
||||
sensor_unit: "Mbps",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# RX Rate in Mbps
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.10.1",
|
||||
sensor_type: "rate-rx",
|
||||
sensor_descr: "RX Rate",
|
||||
sensor_unit: "Mbps",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# RSSI in dBm
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.6.1",
|
||||
sensor_type: "rssi",
|
||||
sensor_descr: "RSSI",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Signal level in dBm
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.5.1.5.1",
|
||||
sensor_type: "signal",
|
||||
sensor_descr: "Signal Level",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Utilization percentage (divide by 10)
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.41112.1.4.6.1.7.1",
|
||||
sensor_type: "utilization",
|
||||
sensor_descr: "Utilization",
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 10
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
# ePMP wireless sensors - mirrors LibreNMS/OS/Epmp.php
|
||||
# Implements: rssi, snr, frequency, clients
|
||||
defp discover_epmp_wireless_sensors(client_opts) do
|
||||
fetch_wireless_sensors(epmp_wireless_oid_defs(), client_opts)
|
||||
end
|
||||
|
||||
# ePMP wireless OID definitions (reused for discovery and debug data)
|
||||
defp epmp_wireless_oid_defs do
|
||||
[
|
||||
# RSSI in dBm
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.3.0",
|
||||
sensor_type: "rssi",
|
||||
sensor_descr: "RSSI",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# SNR in dB
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.18.0",
|
||||
sensor_type: "snr",
|
||||
sensor_descr: "SNR",
|
||||
sensor_unit: "dB",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Frequency in MHz
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.1.0",
|
||||
sensor_type: "frequency",
|
||||
sensor_descr: "Frequency",
|
||||
sensor_unit: "MHz",
|
||||
sensor_divisor: 1
|
||||
},
|
||||
# Connected clients count
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.10.0",
|
||||
sensor_type: "clients",
|
||||
sensor_descr: "Clients",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
# Fetch wireless sensor values from scalar OIDs
|
||||
defp fetch_wireless_sensors(sensor_defs, client_opts) do
|
||||
sensor_defs
|
||||
|> Enum.map(&fetch_wireless_sensor_value(&1, client_opts))
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
defp fetch_wireless_sensor_value(sensor_def, client_opts) do
|
||||
case Client.get(client_opts, sensor_def.oid) do
|
||||
{:ok, value} when is_integer(value) ->
|
||||
%{
|
||||
sensor_type: sensor_def.sensor_type,
|
||||
sensor_index: "wireless_#{sensor_def.sensor_type}",
|
||||
sensor_oid: sensor_def.oid,
|
||||
sensor_descr: sensor_def.sensor_descr,
|
||||
sensor_unit: sensor_def.sensor_unit,
|
||||
sensor_divisor: sensor_def.sensor_divisor,
|
||||
sensor_value: value
|
||||
}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -189,8 +189,10 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
|
||||
assert {:ok, device} = Discovery.discover_device(device)
|
||||
|
||||
assert device.manufacturer == "MikroTik"
|
||||
assert device.model == "RB750"
|
||||
# YAML profile "routeros" returns "Mikrotik RouterOS" as vendor
|
||||
# Model comes from sysDescr when using YAML profiles
|
||||
assert device.manufacturer == "Mikrotik RouterOS"
|
||||
assert device.model == "RouterOS RB750"
|
||||
assert device.sys_name == "router1"
|
||||
|
||||
# Verify interfaces were created
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue