allow nonroutable for me

This commit is contained in:
Graham McIntire 2026-01-23 13:10:12 -06:00
parent a0ba9285dd
commit a4335a047a
No known key found for this signature in database
4 changed files with 113 additions and 80 deletions

View file

@ -606,10 +606,11 @@ defmodule Towerops.Snmp.Profiles.Vendors.Routeros do
end
# Map mtxrGaugeUnit value to sensor type, unit string, and divisor
defp gauge_unit_to_sensor_type(@gauge_unit_celsius), do: {"temperature", "°C", 10}
# Based on LibreNMS routeros.yaml - temperature is in whole degrees, voltage/current/power in deci-units
defp gauge_unit_to_sensor_type(@gauge_unit_celsius), do: {"temperature", "°C", 1}
defp gauge_unit_to_sensor_type(@gauge_unit_rpm), do: {"fanspeed", "RPM", 1}
defp gauge_unit_to_sensor_type(@gauge_unit_dv), do: {"voltage", "V", 10}
defp gauge_unit_to_sensor_type(@gauge_unit_da), do: {"current", "A", 1000}
defp gauge_unit_to_sensor_type(@gauge_unit_da), do: {"current", "mA", 1}
defp gauge_unit_to_sensor_type(@gauge_unit_dw), do: {"power", "W", 10}
defp gauge_unit_to_sensor_type(@gauge_unit_status), do: {"state", "", 1}
defp gauge_unit_to_sensor_type(_), do: {"unknown", "", 1}
@ -643,10 +644,10 @@ defmodule Towerops.Snmp.Profiles.Vendors.Routeros do
end
end
# Sensor type configurations for overrides
# Sensor type configurations for overrides (must match gauge_unit_to_sensor_type divisors)
defp sensor_type_config("voltage"), do: {"voltage", "V", 10}
defp sensor_type_config("temperature"), do: {"temperature", "°C", 10}
defp sensor_type_config("current"), do: {"current", "A", 1000}
defp sensor_type_config("temperature"), do: {"temperature", "°C", 1}
defp sensor_type_config("current"), do: {"current", "mA", 1}
defp sensor_type_config("power"), do: {"power", "W", 10}
defp sensor_type_config("fanspeed"), do: {"fanspeed", "RPM", 1}

View file

@ -516,17 +516,34 @@ defmodule ToweropsWeb.DeviceLive.Form do
end
# Check if a non-routable IP is being used with cloud poller
# Skip this check in dev mode or for specific users
defp check_non_routable_ip_cloud_error(device_params, assigns) do
ip_address = device_params["ip_address"]
# Skip check in dev mode or for specific users
if skip_non_routable_check?(assigns) do
false
else
ip_address = device_params["ip_address"]
# Only check if we have a valid IP address
with true <- is_binary(ip_address) and String.trim(ip_address) != "",
trimmed_ip = String.trim(ip_address),
true <- non_routable_ip?(trimmed_ip),
true <- using_cloud_poller?(device_params, assigns) do
# Only check if we have a valid IP address
with true <- is_binary(ip_address) and String.trim(ip_address) != "",
trimmed_ip = String.trim(ip_address),
true <- non_routable_ip?(trimmed_ip),
true <- using_cloud_poller?(device_params, assigns) do
true
else
_ -> false
end
end
end
defp skip_non_routable_check?(assigns) do
# Skip in dev mode
if Mix.env() == :dev do
true
else
_ -> false
# Skip for specific users
user = assigns[:current_scope] && assigns.current_scope.user
user && user.email == "graham@mcintire.me"
end
end

View file

@ -179,9 +179,9 @@ defmodule ToweropsWeb.DeviceLive.Show do
end)
temperature_sensors =
Enum.filter(non_transceiver_sensors, &(&1.sensor_type in ["temperature", "cpu_temperature", "celsius"]))
Enum.filter(non_transceiver_sensors, &temperature_sensor?/1)
voltage_sensors = Enum.filter(non_transceiver_sensors, &(&1.sensor_type in ["voltage", "volts"]))
voltage_sensors = Enum.filter(non_transceiver_sensors, &voltage_sensor?/1)
storage_sensors = Enum.filter(non_transceiver_sensors, &(&1.sensor_type in ["disk_usage"]))
@ -626,34 +626,57 @@ defmodule ToweropsWeb.DeviceLive.Show do
end
end
# Calculate BPS per interface first, then aggregate by timestamp
# This avoids issues with misaligned timestamps between interfaces
defp aggregate_interface_traffic_stats(interfaces, since) do
interfaces
|> Enum.flat_map(fn interface ->
stats =
interface.id
|> Snmp.get_interface_stats(since: since, limit: 1000)
|> Enum.reverse()
Enum.map(stats, &interface_stat_to_tuple/1)
interface.id
|> Snmp.get_interface_stats(since: since, limit: 1000)
|> Enum.reverse()
|> calculate_interface_bps()
end)
|> Enum.group_by(&elem(&1, 0))
|> Enum.map(&sum_traffic_stats_by_timestamp/1)
|> Enum.sort_by(&elem(&1, 0), DateTime)
|> Enum.group_by(& &1.timestamp_ms)
|> Enum.map(fn {ts, bps_list} ->
total_in = Enum.reduce(bps_list, 0.0, fn x, acc -> acc + x.in_bps end)
total_out = Enum.reduce(bps_list, 0.0, fn x, acc -> acc + x.out_bps end)
%{timestamp_ms: ts, in_bps: total_in, out_bps: total_out}
end)
|> Enum.sort_by(& &1.timestamp_ms)
end
defp interface_stat_to_tuple(stat) do
{stat.checked_at, stat.if_in_octets, stat.if_out_octets}
end
# Calculate BPS for a single interface's stats
defp calculate_interface_bps(stats) do
stats
|> Enum.chunk_every(2, 1, :discard)
|> Enum.map(fn [s1, s2] ->
time_diff = s2.checked_at |> DateTime.diff(s1.checked_at, :second) |> max(1)
defp sum_traffic_stats_by_timestamp({timestamp, stats}) do
total_in = Enum.reduce(stats, 0, fn {_, in_octets, _}, acc -> acc + (in_octets || 0) end)
total_out = Enum.reduce(stats, 0, fn {_, _, out_octets}, acc -> acc + (out_octets || 0) end)
{timestamp, total_in, total_out}
in_bps =
if s2.if_in_octets && s1.if_in_octets do
max((s2.if_in_octets - s1.if_in_octets) * 8 / time_diff, 0)
else
0.0
end
out_bps =
if s2.if_out_octets && s1.if_out_octets do
max((s2.if_out_octets - s1.if_out_octets) * 8 / time_diff, 0)
else
0.0
end
# Round timestamp to nearest minute for aggregation (avoids microsecond misalignment)
ts_seconds = DateTime.to_unix(s2.checked_at)
rounded_ts_ms = div(ts_seconds, 60) * 60 * 1000
%{timestamp_ms: rounded_ts_ms, in_bps: in_bps, out_bps: out_bps}
end)
end
defp build_traffic_json_datasets(all_stats) do
in_data = calculate_bps_data(all_stats, :inbound)
out_data = calculate_bps_data(all_stats, :outbound)
in_data = Enum.map(all_stats, fn s -> %{x: s.timestamp_ms, y: Float.round(s.in_bps, 2)} end)
out_data = Enum.map(all_stats, fn s -> %{x: s.timestamp_ms, y: -Float.round(s.out_bps, 2)} end)
datasets = [
%{label: "Outbound", data: out_data},
@ -663,34 +686,6 @@ defmodule ToweropsWeb.DeviceLive.Show do
Jason.encode!(%{datasets: datasets})
end
defp calculate_bps_data(all_stats, :inbound) do
all_stats
|> Enum.chunk_every(2, 1, :discard)
|> Enum.map(fn [{t1, in1, _out1}, {t2, in2, _out2}] ->
time_diff = t2 |> DateTime.diff(t1, :second) |> max(1)
bps = ((in2 - in1) * 8 / time_diff) |> max(0) |> :erlang.float() |> Float.round(2)
%{
x: DateTime.to_unix(t2, :millisecond),
y: bps
}
end)
end
defp calculate_bps_data(all_stats, :outbound) do
all_stats
|> Enum.chunk_every(2, 1, :discard)
|> Enum.map(fn [{t1, _in1, out1}, {t2, _in2, out2}] ->
time_diff = t2 |> DateTime.diff(t1, :second) |> max(1)
bps = ((out2 - out1) * 8 / time_diff) |> max(0) |> :erlang.float() |> Float.round(2)
%{
x: DateTime.to_unix(t2, :millisecond),
y: -bps
}
end)
end
# Group interfaces by type for organized display
defp group_interfaces_by_type(interfaces) do
interfaces
@ -744,4 +739,33 @@ defmodule ToweropsWeb.DeviceLive.Show do
defp group_ip_addresses_by_interface(ip_addresses) do
Enum.group_by(ip_addresses, & &1.snmp_interface_id)
end
# Sensor type classification helpers
# These check multiple signals (type, unit, description) to correctly categorize sensors
# since some devices (especially Mikrotik) report incorrect unit types
defp temperature_sensor?(sensor) do
type_match = sensor.sensor_type in ["temperature", "cpu_temperature", "celsius"]
unit_match = sensor.sensor_unit in ["°C", "C", "celsius"]
descr_match =
sensor.sensor_descr &&
String.contains?(String.downcase(sensor.sensor_descr), ["temperature", "temp"]) &&
!String.contains?(String.downcase(sensor.sensor_descr), "voltage")
# Match by type OR unit, but exclude if description says "voltage"
(type_match || unit_match || descr_match) && !voltage_by_description?(sensor)
end
defp voltage_sensor?(sensor) do
type_match = sensor.sensor_type in ["voltage", "volts"]
unit_match = sensor.sensor_unit in ["V", "mV", "volts"]
descr_match = voltage_by_description?(sensor)
type_match || unit_match || descr_match
end
defp voltage_by_description?(sensor) do
sensor.sensor_descr &&
String.contains?(String.downcase(sensor.sensor_descr), "voltage")
end
end

View file

@ -5,7 +5,7 @@
active_page="devices"
timezone={@timezone}
>
<div class="mb-6">
<div class="mb-6 -mt-2">
<!-- Breadcrumbs -->
<nav class="flex mb-4" aria-label="Breadcrumb">
<ol class="inline-flex items-center space-x-1 text-sm text-gray-600 dark:text-gray-400">
@ -29,32 +29,23 @@
<div class="flex items-center justify-between mb-4">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{@device.name}</h1>
<p class="text-sm text-gray-600 dark:text-gray-400 font-mono">{@device.ip_address}</p>
<!-- Poller Indicator -->
<div class="mt-2 flex items-center gap-2">
<div class="flex items-center gap-3 mt-0.5">
<span class="text-sm text-gray-600 dark:text-gray-400 font-mono">
{@device.ip_address}
</span>
<span class="text-gray-300 dark:text-gray-600">•</span>
<%= if @agent_info.type == :cloud do %>
<span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300">
<.icon name="hero-cloud" class="h-3.5 w-3.5" /> Cloud Polling
<span class="inline-flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400">
<.icon name="hero-cloud" class="h-3.5 w-3.5" /> Cloud
</span>
<% else %>
<span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300">
<span class="inline-flex items-center gap-1 text-xs text-green-600 dark:text-green-400">
<.icon name="hero-server" class="h-3.5 w-3.5" />
{@agent_info.name}
</span>
<span class="text-xs text-gray-500 dark:text-gray-400">
<%= case @agent_info.source do %>
<% :device -> %>
(assigned directly)
<% :site -> %>
(via site default)
<% :organization -> %>
(via org default)
<% _ -> %>
<% end %>
</span>
<%= if @agent_info.last_seen_at do %>
<span class="text-xs text-gray-500 dark:text-gray-400">
&bull; Last seen: {format_relative_time(@agent_info.last_seen_at)}
<span class="text-xs text-gray-400 dark:text-gray-500">
({format_relative_time(@agent_info.last_seen_at)})
</span>
<% end %>
<% end %>