- Bug #12: Lower rate limit to 15/min - Bug #13: Wrap model loading in Task.start - Bug #14: Fix classify_time_period guard gap at -3.0 - Bug #15: Not applicable (Elixir has no ?? operator) - Bug #16: Remove fallback Repo.get for station preload - Bug #17: Extract cache_key() in ContactMapController - Bug #18: Add has_many :contacts and :beacons to User schema - A&D #4: Move serve_markdown_if_requested after secure headers - Config #1: Move signing_salt to runtime.exs env var - Config #2: Use --check-unused instead of --unused - Config #3: Re-enable UnsafeToAtom Credo check - Config #5: Re-enable LeakyEnvironment Credo check - Add @spec annotations to fix re-enabled Specs violations - Replace String.to_atom with to_existing_atom where guarded
207 lines
6.3 KiB
Elixir
207 lines
6.3 KiB
Elixir
defmodule Microwaveprop.Commercial.SnmpClient do
|
|
@moduledoc false
|
|
|
|
require Logger
|
|
|
|
# UBNT-AirFIBER-MIB OIDs (base 1.3.6.1.4.1.41112.1.3)
|
|
@af_base "1.3.6.1.4.1.41112.1.3"
|
|
@af11x_oids %{
|
|
"#{@af_base}.2.1.11.1" => :rx_power_0,
|
|
"#{@af_base}.2.1.14.1" => :rx_power_1,
|
|
"#{@af_base}.1.1.9.1" => :tx_power,
|
|
"#{@af_base}.2.1.2.1" => :cur_tx_mod_rate,
|
|
"#{@af_base}.2.1.18.1" => :remote_tx_mod_rate,
|
|
"#{@af_base}.2.1.5.1" => :rx_capacity,
|
|
"#{@af_base}.2.1.6.1" => :tx_capacity,
|
|
"#{@af_base}.2.1.17.1" => :remote_tx_power,
|
|
"#{@af_base}.2.1.19.1" => :remote_rx_power_0,
|
|
"#{@af_base}.2.1.22.1" => :remote_rx_power_1,
|
|
"#{@af_base}.2.1.26.1" => :link_state,
|
|
"#{@af_base}.2.1.44.1" => :link_uptime,
|
|
"#{@af_base}.2.1.8.1" => :radio_temp_0_c,
|
|
"#{@af_base}.2.1.10.1" => :radio_temp_1_c,
|
|
"#{@af_base}.1.1.5.1" => :tx_freq_mhz,
|
|
"#{@af_base}.1.1.6.1" => :rx_freq_mhz,
|
|
"#{@af_base}.2.1.4.1" => :link_distance_m
|
|
}
|
|
|
|
# AF60-LR static OIDs
|
|
@af60_base "1.3.6.1.4.1.41112.1.11.1"
|
|
@af60_static_oids %{
|
|
"#{@af60_base}.2.5.1" => :radio_temp_0_c,
|
|
"#{@af60_base}.2.6.1" => :link_state,
|
|
"#{@af60_base}.1.2.1" => :tx_freq_mhz
|
|
}
|
|
|
|
# AF60 station table column -> field name + unit conversion
|
|
@af60_station_cols %{
|
|
3 => {:rx_power_0, :raw},
|
|
4 => {:tx_power, :raw},
|
|
5 => {:cur_tx_mod_rate, :raw},
|
|
6 => {:remote_tx_mod_rate, :raw},
|
|
7 => {:tx_capacity, :kbps_to_mbps},
|
|
8 => {:rx_capacity, :kbps_to_mbps},
|
|
15 => {:link_distance_m, :raw},
|
|
17 => {:link_uptime, :centiseconds_to_seconds},
|
|
18 => {:remote_rx_power_0, :raw},
|
|
19 => {:remote_tx_power, :raw}
|
|
}
|
|
|
|
@spec poll(String.t(), String.t(), String.t()) :: {:ok, map()} | {:error, term()}
|
|
def poll(host, community, "af11x"), do: poll_af11x(host, community)
|
|
def poll(host, community, "af60"), do: poll_af60(host, community)
|
|
|
|
@spec poll_af11x(String.t(), String.t()) :: {:ok, map()} | {:error, term()}
|
|
def poll_af11x(host, community) do
|
|
oids = Map.keys(@af11x_oids)
|
|
args = ["-v1", "-c", community, "-t", "5", "-r", "1", host | oids]
|
|
|
|
case run_cmd("snmpget", args) do
|
|
{output, 0} ->
|
|
result = parse_snmpget_output(output, :af11x)
|
|
if map_size(result) > 0, do: {:ok, result}, else: {:error, :empty_response}
|
|
|
|
{output, _exit_code} ->
|
|
Logger.warning("snmpget failed for #{host}: #{String.trim(output)}")
|
|
{:error, :snmp_failed}
|
|
end
|
|
end
|
|
|
|
@spec poll_af60(String.t(), String.t()) :: {:ok, map()} | {:error, term()}
|
|
def poll_af60(host, community) do
|
|
static_oids = Map.keys(@af60_static_oids)
|
|
static_args = ["-v1", "-c", community, "-t", "5", "-r", "1", host | static_oids]
|
|
|
|
station_oids = Enum.map(Map.keys(@af60_station_cols), &"#{@af60_base}.3.1.#{&1}")
|
|
station_args = ["-v1", "-c", community, "-t", "5", "-r", "1", host | station_oids]
|
|
|
|
with {static_out, 0} <- run_cmd("snmpget", static_args),
|
|
{station_out, 0} <- run_cmd("snmpgetnext", station_args) do
|
|
result = parse_af60_output(static_out, station_out)
|
|
if map_size(result) > 0, do: {:ok, result}, else: {:error, :empty_response}
|
|
else
|
|
{output, _} ->
|
|
Logger.warning("snmp failed for AF60 #{host}: #{String.trim(output)}")
|
|
{:error, :snmp_failed}
|
|
end
|
|
end
|
|
|
|
@spec parse_snmpget_output(String.t(), :af11x) :: map()
|
|
def parse_snmpget_output(output, :af11x) do
|
|
output
|
|
|> parse_lines()
|
|
|> Enum.reduce(%{}, fn {oid, value}, acc ->
|
|
case find_af11x_field(oid) do
|
|
nil -> acc
|
|
field -> Map.put(acc, field, value)
|
|
end
|
|
end)
|
|
end
|
|
|
|
@spec parse_af60_output(String.t(), String.t()) :: map()
|
|
def parse_af60_output(static_output, station_output) do
|
|
static =
|
|
static_output
|
|
|> parse_lines()
|
|
|> Enum.reduce(%{}, fn {oid, value}, acc ->
|
|
case find_af60_static_field(oid) do
|
|
nil -> acc
|
|
field -> Map.put(acc, field, value)
|
|
end
|
|
end)
|
|
|
|
station =
|
|
station_output
|
|
|> parse_lines()
|
|
|> Enum.reduce(%{}, fn {oid, value}, acc ->
|
|
case find_af60_station_field(oid) do
|
|
nil -> acc
|
|
{field, conversion} -> Map.put(acc, field, convert_value(value, conversion))
|
|
end
|
|
end)
|
|
|
|
Map.merge(static, station)
|
|
end
|
|
|
|
# --- Private ---
|
|
|
|
defp parse_lines(output) do
|
|
output
|
|
|> String.split("\n", trim: true)
|
|
|> Enum.flat_map(fn line ->
|
|
case parse_snmp_line(line) do
|
|
{_oid, _value} = pair -> [pair]
|
|
nil -> []
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp run_cmd(cmd, args) do
|
|
runner = Application.get_env(:microwaveprop, :snmp_runner, &default_run_cmd/2)
|
|
runner.(cmd, args)
|
|
end
|
|
|
|
defp default_run_cmd(cmd, args) do
|
|
System.cmd(cmd, args, stderr_to_stdout: true)
|
|
rescue
|
|
ErlangError -> {"command not found: #{cmd}", 127}
|
|
end
|
|
|
|
defp parse_snmp_line(line) do
|
|
line = String.trim(line)
|
|
|
|
with [oid_part, value_part] <- String.split(line, " = ", parts: 2),
|
|
false <- String.contains?(value_part, "No Such"),
|
|
oid = oid_part |> String.trim() |> normalize_oid(),
|
|
value when not is_nil(value) <- parse_snmp_value(value_part) do
|
|
{oid, value}
|
|
else
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
defp normalize_oid(oid) do
|
|
oid
|
|
|> String.replace(~r/^SNMPv2-SMI::enterprises\./, "1.3.6.1.4.1.")
|
|
|> String.replace(~r/^iso\./, "1.")
|
|
|> String.replace(~r/^\./, "")
|
|
end
|
|
|
|
defp parse_snmp_value(value_str) do
|
|
case Regex.run(~r/:\s*(.+)$/, String.trim(value_str)) do
|
|
[_, raw] ->
|
|
raw = String.trim(raw, "\"")
|
|
|
|
case Integer.parse(raw) do
|
|
{val, _} -> val
|
|
:error -> nil
|
|
end
|
|
|
|
nil ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
defp find_af11x_field(oid) do
|
|
Enum.find_value(@af11x_oids, fn {known_oid, field} ->
|
|
if oid == known_oid, do: field
|
|
end)
|
|
end
|
|
|
|
defp find_af60_static_field(oid) do
|
|
Enum.find_value(@af60_static_oids, fn {known_oid, field} ->
|
|
if oid == known_oid, do: field
|
|
end)
|
|
end
|
|
|
|
defp find_af60_station_field(oid) do
|
|
Enum.find_value(@af60_station_cols, fn {col, field_and_conv} ->
|
|
prefix = "#{@af60_base}.3.1.#{col}."
|
|
if String.starts_with?(oid, prefix), do: field_and_conv
|
|
end)
|
|
end
|
|
|
|
defp convert_value(value, :raw), do: value
|
|
defp convert_value(value, :kbps_to_mbps), do: div(value, 1000)
|
|
defp convert_value(value, :centiseconds_to_seconds), do: div(value, 100)
|
|
end
|