mikrotik improvements
This commit is contained in:
parent
218b99972e
commit
d401e0b036
11 changed files with 437 additions and 33 deletions
|
|
@ -142,6 +142,31 @@ defmodule Towerops.Devices do
|
|||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Finds a device by IP address within a site, optionally excluding a specific device ID.
|
||||
Returns nil if no device found.
|
||||
"""
|
||||
def get_device_by_ip(ip_address, site_id, exclude_id \\ nil)
|
||||
|
||||
def get_device_by_ip(_ip_address, nil, _exclude_id), do: nil
|
||||
|
||||
def get_device_by_ip(ip_address, site_id, exclude_id) do
|
||||
query =
|
||||
from(d in DeviceSchema,
|
||||
where: d.ip_address == ^ip_address and d.site_id == ^site_id,
|
||||
preload: [:site]
|
||||
)
|
||||
|
||||
query =
|
||||
if exclude_id do
|
||||
from(d in query, where: d.id != ^exclude_id)
|
||||
else
|
||||
query
|
||||
end
|
||||
|
||||
Repo.one(query)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single device belonging to a specific site.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -148,6 +148,8 @@ defmodule Towerops.Profiles.YamlProfiles do
|
|||
# Extract all detection blocks with their conditions for per-block matching
|
||||
detection_blocks: extract_detection_blocks(detection),
|
||||
device_oids: extract_device_oids(discovery),
|
||||
# Regex to extract hardware model from sysDescr (e.g., "/RouterOS (?<hardware>.*)/")
|
||||
hardware_regex: extract_hardware_regex(discovery),
|
||||
# Scalar sensors (direct OID polling)
|
||||
sensor_oids: extract_sensor_oids(discovery),
|
||||
# Table-based sensors (OID walks) - all standard sensor types
|
||||
|
|
@ -321,6 +323,7 @@ defmodule Towerops.Profiles.YamlProfiles do
|
|||
field_mappings = [
|
||||
{"serial", :serial_number},
|
||||
{"version", :firmware_version},
|
||||
{"features", :license_version},
|
||||
{"hardware", :hardware},
|
||||
{"lat", :latitude},
|
||||
{"long", :longitude}
|
||||
|
|
@ -338,6 +341,18 @@ defmodule Towerops.Profiles.YamlProfiles do
|
|||
|> Map.new()
|
||||
end
|
||||
|
||||
# Extract sysDescr_regex from os module for hardware extraction
|
||||
# Returns a compiled regex if present, nil otherwise
|
||||
defp extract_hardware_regex(yaml) do
|
||||
case get_in(yaml, ["modules", "os", "sysDescr_regex"]) do
|
||||
pattern when is_binary(pattern) ->
|
||||
parse_regex_pattern(pattern)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -519,7 +519,12 @@ defmodule Towerops.Snmp.Discovery do
|
|||
|
||||
@spec sync_sensors(Device.t(), [sensor_data()]) :: [Sensor.t()]
|
||||
defp sync_sensors(device, discovered_sensors) do
|
||||
# Deduplicate sensors by sensor_index (keep first occurrence)
|
||||
# Deduplicate sensors by OID first - when multiple sensors share the same OID,
|
||||
# prefer the one with a proper description (not containing "MIB::")
|
||||
# This handles cases where YAML profiles and vendor profiles discover the same sensor
|
||||
discovered_sensors = deduplicate_sensors_by_oid(discovered_sensors)
|
||||
|
||||
# Then deduplicate by sensor_index (keep first occurrence)
|
||||
# This prevents duplicate key errors when profiles generate duplicate indices
|
||||
discovered_sensors = Enum.uniq_by(discovered_sensors, & &1.sensor_index)
|
||||
|
||||
|
|
@ -744,4 +749,17 @@ defmodule Towerops.Snmp.Discovery do
|
|||
end
|
||||
|
||||
defp sanitize_for_json(other), do: other
|
||||
|
||||
# Deduplicate sensors by OID - when multiple sensors share the same OID,
|
||||
# prefer the one with a proper description (not containing "MIB::")
|
||||
# This handles cases where YAML profiles and vendor profiles discover the same sensor
|
||||
defp deduplicate_sensors_by_oid(sensors) do
|
||||
sensors
|
||||
|> Enum.group_by(& &1.sensor_oid)
|
||||
|> Enum.map(fn {_oid, group} ->
|
||||
# Prefer sensor with proper description (no MIB name)
|
||||
Enum.find(group, fn s -> not String.contains?(s.sensor_descr, "MIB::") end) ||
|
||||
List.last(group)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ defmodule Towerops.Snmp.MibTranslator do
|
|||
"MIKROTIK-MIB::mtxrSystemDisplayName.0" => "1.3.6.1.4.1.14988.1.1.7.8.0",
|
||||
"MIKROTIK-MIB::mtxrBoardName.0" => "1.3.6.1.4.1.14988.1.1.7.9.0",
|
||||
"MIKROTIK-MIB::mtxrLicenseVersion.0" => "1.3.6.1.4.1.14988.1.1.7.5.0",
|
||||
# MikroTik License info (short names used in YAML profiles)
|
||||
"MIKROTIK-MIB::mtxrLicVersion.0" => "1.3.6.1.4.1.14988.1.1.4.3.0",
|
||||
"MIKROTIK-MIB::mtxrLicLevel.0" => "1.3.6.1.4.1.14988.1.1.4.4.0",
|
||||
# MikroTik Health sensors
|
||||
"MIKROTIK-MIB::mtxrHlCoreVoltage.0" => "1.3.6.1.4.1.14988.1.1.3.1.0",
|
||||
"MIKROTIK-MIB::mtxrHlThreeDotThreeVoltage.0" => "1.3.6.1.4.1.14988.1.1.3.2.0",
|
||||
|
|
|
|||
|
|
@ -29,10 +29,11 @@ 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
|
||||
# Get hardware model - use YAML hardware OID, then sysDescr_regex capture, then vendor-specific, then sysDescr
|
||||
model =
|
||||
detect_vendor_hardware(profile, client_opts) ||
|
||||
Map.get(device_data, :hardware) ||
|
||||
Map.get(device_data, :hardware) ||
|
||||
extract_hardware_from_regex(profile, system_info[:sys_descr]) ||
|
||||
detect_vendor_hardware(profile, client_opts) ||
|
||||
system_info[:sys_descr]
|
||||
|
||||
# Merge with system_info
|
||||
|
|
@ -93,7 +94,11 @@ defmodule Towerops.Snmp.Profiles.Dynamic do
|
|||
count_results ++
|
||||
state_results ++ wireless_results ++ base_sensors
|
||||
|
||||
{:ok, all_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)
|
||||
|
||||
{:ok, deduped_sensors}
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -128,6 +133,18 @@ defmodule Towerops.Snmp.Profiles.Dynamic do
|
|||
|
||||
defp detect_vendor_hardware(_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
|
||||
|
||||
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) ->
|
||||
|
|
@ -344,4 +361,16 @@ defmodule Towerops.Snmp.Profiles.Dynamic do
|
|||
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::")
|
||||
defp deduplicate_by_oid(sensors) do
|
||||
sensors
|
||||
|> Enum.group_by(& &1.sensor_oid)
|
||||
|> Enum.map(fn {_oid, group} ->
|
||||
# Prefer sensor with proper description (no MIB name)
|
||||
Enum.find(group, fn s -> not String.contains?(s.sensor_descr, "MIB::") end) ||
|
||||
List.last(group)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
200
lib/towerops/snmp/profiles/vendors/routeros.ex
vendored
200
lib/towerops/snmp/profiles/vendors/routeros.ex
vendored
|
|
@ -27,6 +27,23 @@ defmodule Towerops.Snmp.Profiles.Vendors.Routeros do
|
|||
@wl_60g_table "1.3.6.1.4.1.14988.1.1.1.8.1"
|
||||
@lte_modem_table "1.3.6.1.4.1.14988.1.1.16.1.1"
|
||||
|
||||
# mtxrGaugeTable - System health sensors (temperature, voltage, current, power, fan speed)
|
||||
# .2.{idx} = mtxrGaugeName, .3.{idx} = mtxrGaugeValue, .4.{idx} = mtxrGaugeUnit
|
||||
@gauge_table "1.3.6.1.4.1.14988.1.1.3.100.1"
|
||||
|
||||
# mtxrOpticalTable - SFP/optical transceiver sensors
|
||||
# .2.{idx} = mtxrOpticalName, .6.{idx} = Temperature, .7.{idx} = Supply Voltage
|
||||
# .8.{idx} = Tx Bias Current, .9.{idx} = Tx Power, .10.{idx} = Rx Power
|
||||
@optical_table "1.3.6.1.4.1.14988.1.1.19.1.1"
|
||||
|
||||
# mtxrGaugeUnit values
|
||||
@gauge_unit_celsius 1
|
||||
@gauge_unit_rpm 2
|
||||
@gauge_unit_dv 3
|
||||
@gauge_unit_da 4
|
||||
@gauge_unit_dw 5
|
||||
@gauge_unit_status 6
|
||||
|
||||
@impl true
|
||||
def profile_names, do: ["routeros"]
|
||||
|
||||
|
|
@ -51,6 +68,8 @@ defmodule Towerops.Snmp.Profiles.Vendors.Routeros do
|
|||
discover_station_sensors(client_opts) ++
|
||||
discover_60g_sensors(client_opts) ++
|
||||
discover_lte_sensors(client_opts) ++
|
||||
discover_gauge_sensors(client_opts) ++
|
||||
discover_optical_sensors(client_opts) ++
|
||||
discover_system_sensors(client_opts)
|
||||
|
||||
Logger.debug("RouterOS: Discovered #{length(sensors)} wireless sensors")
|
||||
|
|
@ -537,6 +556,187 @@ defmodule Towerops.Snmp.Profiles.Vendors.Routeros do
|
|||
sensors
|
||||
end
|
||||
|
||||
# Discover sensors from mtxrGaugeTable (system health: temperature, voltage, current, power, fan speed)
|
||||
defp discover_gauge_sensors(client_opts) do
|
||||
case Client.walk(client_opts, @gauge_table) do
|
||||
{:ok, results} when is_map(results) and map_size(results) > 0 ->
|
||||
parse_gauge_table(results)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_gauge_table(results) do
|
||||
# Group by index (last OID component)
|
||||
grouped = group_by_index(results)
|
||||
|
||||
grouped
|
||||
|> Enum.flat_map(fn {idx, values} ->
|
||||
build_gauge_sensor(idx, values)
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
defp build_gauge_sensor(idx, values) do
|
||||
# .2 = mtxrGaugeName, .3 = mtxrGaugeValue, .4 = mtxrGaugeUnit
|
||||
name = get_string_value(values, 2)
|
||||
value = get_int_value(values, 3)
|
||||
unit_type = get_int_value(values, 4)
|
||||
|
||||
if name && value do
|
||||
{sensor_type, sensor_unit, divisor} = gauge_unit_to_sensor_type(unit_type)
|
||||
|
||||
[
|
||||
%{
|
||||
sensor_type: sensor_type,
|
||||
sensor_index: "mikrotik_gauge_#{idx}",
|
||||
sensor_oid: "#{@gauge_table}.3.#{idx}",
|
||||
sensor_descr: name,
|
||||
sensor_unit: sensor_unit,
|
||||
sensor_divisor: divisor,
|
||||
last_value: value / divisor
|
||||
}
|
||||
]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
# Map mtxrGaugeUnit value to sensor type, unit string, and divisor
|
||||
defp gauge_unit_to_sensor_type(@gauge_unit_celsius), do: {"temperature", "°C", 10}
|
||||
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_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}
|
||||
|
||||
# Discover sensors from mtxrOpticalTable (SFP/optical transceiver sensors)
|
||||
defp discover_optical_sensors(client_opts) do
|
||||
case Client.walk(client_opts, @optical_table) do
|
||||
{:ok, results} when is_map(results) and map_size(results) > 0 ->
|
||||
parse_optical_table(results)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_optical_table(results) do
|
||||
# Group by index (last OID component)
|
||||
grouped = group_by_index(results)
|
||||
|
||||
Enum.flat_map(grouped, fn {idx, values} ->
|
||||
build_optical_sensors(idx, values)
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_optical_sensors(idx, values) do
|
||||
# .2 = mtxrOpticalName, .6 = Temperature, .7 = Supply Voltage
|
||||
# .8 = Tx Bias Current, .9 = Tx Power, .10 = Rx Power
|
||||
name = get_string_value(values, 2) || "SFP #{idx}"
|
||||
|
||||
sensors = []
|
||||
|
||||
# Temperature (.6)
|
||||
sensors =
|
||||
if temp = get_int_value(values, 6) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "temperature",
|
||||
sensor_index: "mikrotik_optical_temp_#{idx}",
|
||||
sensor_oid: "#{@optical_table}.6.#{idx}",
|
||||
sensor_descr: "#{name} Temperature",
|
||||
sensor_unit: "°C",
|
||||
sensor_divisor: 1,
|
||||
last_value: temp / 1
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# Supply Voltage (.7) - in mV, convert to V
|
||||
sensors =
|
||||
if voltage = get_int_value(values, 7) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "voltage",
|
||||
sensor_index: "mikrotik_optical_voltage_#{idx}",
|
||||
sensor_oid: "#{@optical_table}.7.#{idx}",
|
||||
sensor_descr: "#{name} Supply Voltage",
|
||||
sensor_unit: "V",
|
||||
sensor_divisor: 1000,
|
||||
last_value: voltage / 1000
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# Tx Bias Current (.8) - in uA, convert to mA
|
||||
sensors =
|
||||
if bias = get_int_value(values, 8) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "current",
|
||||
sensor_index: "mikrotik_optical_tx_bias_#{idx}",
|
||||
sensor_oid: "#{@optical_table}.8.#{idx}",
|
||||
sensor_descr: "#{name} Tx Bias",
|
||||
sensor_unit: "mA",
|
||||
sensor_divisor: 1000,
|
||||
last_value: bias / 1000
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# Tx Power (.9) - in thousandths of dBm
|
||||
sensors =
|
||||
if tx_power = get_int_value(values, 9) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "dbm",
|
||||
sensor_index: "mikrotik_optical_tx_power_#{idx}",
|
||||
sensor_oid: "#{@optical_table}.9.#{idx}",
|
||||
sensor_descr: "#{name} Tx Power",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1000,
|
||||
last_value: tx_power / 1000
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# Rx Power (.10) - in thousandths of dBm
|
||||
sensors =
|
||||
if rx_power = get_int_value(values, 10) do
|
||||
[
|
||||
%{
|
||||
sensor_type: "dbm",
|
||||
sensor_index: "mikrotik_optical_rx_power_#{idx}",
|
||||
sensor_oid: "#{@optical_table}.10.#{idx}",
|
||||
sensor_descr: "#{name} Rx Power",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1000,
|
||||
last_value: rx_power / 1000
|
||||
}
|
||||
| sensors
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
sensors
|
||||
end
|
||||
|
||||
# Discover system-level sensors (DHCP leases, PPPoE sessions)
|
||||
defp discover_system_sensors(client_opts) do
|
||||
Vendor.fetch_sensors(wireless_oid_defs(), client_opts)
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ defmodule ToweropsWeb.CoreComponents do
|
|||
"text-sm text-gray-500 dark:text-gray-400",
|
||||
@title && "mt-1"
|
||||
]}>
|
||||
{msg}
|
||||
{render_flash_message(msg)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="ml-4 flex shrink-0">
|
||||
|
|
@ -105,6 +105,24 @@ defmodule ToweropsWeb.CoreComponents do
|
|||
"""
|
||||
end
|
||||
|
||||
# Renders flash message content, supporting both plain strings and maps with links
|
||||
# Map format: %{text: "Message", link_text: "Link", link_url: "/path"}
|
||||
defp render_flash_message(%{text: text, link_text: link_text, link_url: link_url}) do
|
||||
assigns = %{text: text, link_text: link_text, link_url: link_url}
|
||||
|
||||
~H"""
|
||||
{@text}
|
||||
<.link
|
||||
navigate={@link_url}
|
||||
class="font-medium text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
|
||||
>
|
||||
{@link_text}
|
||||
</.link>
|
||||
"""
|
||||
end
|
||||
|
||||
defp render_flash_message(msg), do: msg
|
||||
|
||||
@doc """
|
||||
Renders a button with navigation support.
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ defmodule ToweropsWeb.DeviceLive.Form do
|
|||
|> assign(:available_sites, sites)
|
||||
|> assign(:available_agents, agents)
|
||||
|> assign(:preselected_site_id, params["site_id"])
|
||||
|> assign(:snmp_test_result, nil)}
|
||||
|> assign(:snmp_test_result, nil)
|
||||
|> assign(:duplicate_device, nil)}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -143,16 +144,32 @@ defmodule ToweropsWeb.DeviceLive.Form do
|
|||
|
||||
@impl true
|
||||
def handle_event("validate", %{"device" => device_params}, socket) do
|
||||
device_params = sanitize_device_params(device_params)
|
||||
|
||||
changeset =
|
||||
socket.assigns.device
|
||||
|> Devices.change_device(device_params)
|
||||
|> Map.put(:action, :validate)
|
||||
|
||||
{:noreply, assign(socket, :form, to_form(changeset))}
|
||||
# Check for duplicate IP address within the same site
|
||||
ip_address = device_params["ip_address"]
|
||||
site_id = device_params["site_id"]
|
||||
exclude_id = if socket.assigns.live_action == :edit, do: socket.assigns.device.id
|
||||
|
||||
duplicate_device =
|
||||
if ip_address && String.trim(ip_address) != "" && site_id do
|
||||
Devices.get_device_by_ip(String.trim(ip_address), site_id, exclude_id)
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:form, to_form(changeset))
|
||||
|> assign(:duplicate_device, duplicate_device)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("save", %{"device" => device_params}, socket) do
|
||||
device_params = sanitize_device_params(device_params)
|
||||
save_device(socket, socket.assigns.live_action, device_params)
|
||||
end
|
||||
|
||||
|
|
@ -279,12 +296,24 @@ defmodule ToweropsWeb.DeviceLive.Form do
|
|||
end
|
||||
|
||||
defp handle_device_creation(device) do
|
||||
device_label = device.name || device.ip_address
|
||||
|
||||
base_message =
|
||||
if device.snmp_enabled do
|
||||
"Device created. SNMP discovery started. "
|
||||
else
|
||||
"Device created. "
|
||||
end
|
||||
|
||||
if device.snmp_enabled do
|
||||
_ = enqueue_discovery(device.id)
|
||||
"Device created successfully. SNMP discovery started in background."
|
||||
else
|
||||
"Device created successfully"
|
||||
end
|
||||
|
||||
%{
|
||||
text: base_message,
|
||||
link_text: device_label,
|
||||
link_url: "/devices/#{device.id}"
|
||||
}
|
||||
end
|
||||
|
||||
defp handle_device_update(old_device, device) do
|
||||
|
|
@ -335,7 +364,7 @@ defmodule ToweropsWeb.DeviceLive.Form do
|
|||
# If device community is nil or empty, resolve from site/org hierarchy
|
||||
effective_community =
|
||||
if is_nil(device_community) or device_community == "" do
|
||||
resolve_inherited_community(assigns)
|
||||
resolve_inherited_community(params, assigns)
|
||||
else
|
||||
device_community
|
||||
end
|
||||
|
|
@ -348,27 +377,45 @@ defmodule ToweropsWeb.DeviceLive.Form do
|
|||
}
|
||||
end
|
||||
|
||||
defp resolve_inherited_community(assigns) do
|
||||
cond do
|
||||
# For new devices, check preselected site
|
||||
assigns[:preselected_site_id] ->
|
||||
site = Enum.find(assigns.available_sites, &(&1.id == assigns.preselected_site_id))
|
||||
site && site.snmp_community
|
||||
|
||||
# For edit mode, use effective SNMP community
|
||||
assigns[:effective_snmp_community] && assigns.effective_snmp_community != "(not set)" ->
|
||||
assigns.effective_snmp_community
|
||||
|
||||
# Fallback: check if there's only one site
|
||||
length(assigns[:available_sites] || []) == 1 ->
|
||||
site = List.first(assigns.available_sites)
|
||||
site && site.snmp_community
|
||||
|
||||
true ->
|
||||
nil
|
||||
defp resolve_inherited_community(params, assigns) do
|
||||
# For edit mode, use effective SNMP community (already resolved)
|
||||
if has_effective_community?(assigns) do
|
||||
assigns.effective_snmp_community
|
||||
else
|
||||
resolve_community_from_hierarchy(params, assigns)
|
||||
end
|
||||
end
|
||||
|
||||
defp has_effective_community?(assigns) do
|
||||
assigns[:effective_snmp_community] && assigns.effective_snmp_community != "(not set)"
|
||||
end
|
||||
|
||||
defp resolve_community_from_hierarchy(params, assigns) do
|
||||
selected_site_id = get_selected_site_id(params, assigns)
|
||||
site_community = get_site_community(selected_site_id, assigns)
|
||||
site_community || get_org_community(assigns)
|
||||
end
|
||||
|
||||
defp get_selected_site_id(params, assigns) do
|
||||
sites = assigns[:available_sites] || []
|
||||
|
||||
params["site_id"] ||
|
||||
assigns[:preselected_site_id] ||
|
||||
(length(sites) == 1 && List.first(sites).id)
|
||||
end
|
||||
|
||||
defp get_site_community(nil, _assigns), do: nil
|
||||
|
||||
defp get_site_community(site_id, assigns) do
|
||||
site = Enum.find(assigns.available_sites, &(&1.id == site_id))
|
||||
if site && site.snmp_community && site.snmp_community != "", do: site.snmp_community
|
||||
end
|
||||
|
||||
defp get_org_community(assigns) do
|
||||
org = assigns[:organization]
|
||||
org && org.snmp_community
|
||||
end
|
||||
|
||||
@spec normalize_port(integer() | String.t() | any()) :: integer()
|
||||
defp normalize_port(port) when is_integer(port) and port >= 1 and port <= 65_535, do: port
|
||||
defp normalize_port(port) when is_integer(port), do: 161
|
||||
|
|
@ -451,4 +498,15 @@ defmodule ToweropsWeb.DeviceLive.Form do
|
|||
end
|
||||
|
||||
defp handle_agent_assignment(_device_id, _), do: :ok
|
||||
|
||||
# Sanitize device params - trim whitespace from IP address
|
||||
defp sanitize_device_params(params) do
|
||||
case Map.get(params, "ip_address") do
|
||||
ip when is_binary(ip) ->
|
||||
Map.put(params, "ip_address", String.trim(ip))
|
||||
|
||||
_ ->
|
||||
params
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -44,6 +44,24 @@
|
|||
|
||||
<.input field={@form[:ip_address]} type="text" label="IP Address" required />
|
||||
|
||||
<div
|
||||
:if={@duplicate_device}
|
||||
class="rounded-md bg-amber-50 dark:bg-amber-900/20 p-3 -mt-2 mb-4"
|
||||
>
|
||||
<div class="flex items-center gap-2 text-amber-800 dark:text-amber-200">
|
||||
<.icon name="hero-exclamation-triangle" class="h-5 w-5 flex-shrink-0" />
|
||||
<p class="text-sm">
|
||||
A device with this IP address already exists:
|
||||
<.link
|
||||
navigate={~p"/devices/#{@duplicate_device.id}"}
|
||||
class="font-medium underline hover:text-amber-900 dark:hover:text-amber-100"
|
||||
>
|
||||
{@duplicate_device.name || @duplicate_device.ip_address}
|
||||
</.link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.input
|
||||
field={@form[:site_id]}
|
||||
type="select"
|
||||
|
|
@ -240,7 +258,13 @@
|
|||
</div>
|
||||
|
||||
<div class="flex gap-3 mt-6">
|
||||
<.button phx-disable-with="Saving..." variant="primary">Save Device</.button>
|
||||
<.button
|
||||
phx-disable-with="Saving..."
|
||||
variant="primary"
|
||||
disabled={@duplicate_device != nil}
|
||||
>
|
||||
Save Device
|
||||
</.button>
|
||||
<.button navigate={~p"/devices"}>Cancel</.button>
|
||||
</div>
|
||||
</.form>
|
||||
|
|
|
|||
|
|
@ -190,9 +190,9 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
assert {:ok, device} = Discovery.discover_device(device)
|
||||
|
||||
# YAML profile "routeros" returns "Mikrotik RouterOS" as vendor
|
||||
# Model comes from sysDescr when using YAML profiles
|
||||
# Model comes from sysDescr_regex capture (hardware group extracts model from sysDescr)
|
||||
assert device.manufacturer == "Mikrotik RouterOS"
|
||||
assert device.model == "RouterOS RB750"
|
||||
assert device.model == "RB750"
|
||||
assert device.sys_name == "router1"
|
||||
|
||||
# Verify interfaces were created
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ defmodule Towerops.Snmp.Profiles.Vendors.RouterosTest do
|
|||
@wl_stat_table "1.3.6.1.4.1.14988.1.1.1.1.1"
|
||||
@wl_60g_table "1.3.6.1.4.1.14988.1.1.1.8.1"
|
||||
@lte_modem_table "1.3.6.1.4.1.14988.1.1.16.1.1"
|
||||
@gauge_table "1.3.6.1.4.1.14988.1.1.3.100.1"
|
||||
@optical_table "1.3.6.1.4.1.14988.1.1.19.1.1"
|
||||
|
||||
describe "profile_names/0" do
|
||||
test "returns expected profile names" do
|
||||
|
|
@ -113,6 +115,8 @@ defmodule Towerops.Snmp.Profiles.Vendors.RouterosTest do
|
|||
expect(SnmpMock, :walk, fn _, @wl_stat_table, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, @wl_60g_table, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, @lte_modem_table, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, @gauge_table, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, @optical_table, _ -> {:ok, []} end)
|
||||
|
||||
# System sensors (DHCP leases and PPPoE)
|
||||
expect(SnmpMock, :get, 2, fn _, _oid, _ -> {:ok, 100} end)
|
||||
|
|
@ -150,6 +154,8 @@ defmodule Towerops.Snmp.Profiles.Vendors.RouterosTest do
|
|||
|
||||
expect(SnmpMock, :walk, fn _, @wl_60g_table, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, @lte_modem_table, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, @gauge_table, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, @optical_table, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :get, 2, fn _, _oid, _ -> {:ok, 50} end)
|
||||
|
||||
sensors = Routeros.discover_wireless_sensors(@client_opts)
|
||||
|
|
@ -180,6 +186,8 @@ defmodule Towerops.Snmp.Profiles.Vendors.RouterosTest do
|
|||
end)
|
||||
|
||||
expect(SnmpMock, :walk, fn _, @lte_modem_table, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, @gauge_table, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, @optical_table, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :get, 2, fn _, _oid, _ -> {:ok, 25} end)
|
||||
|
||||
sensors = Routeros.discover_wireless_sensors(@client_opts)
|
||||
|
|
@ -209,6 +217,8 @@ defmodule Towerops.Snmp.Profiles.Vendors.RouterosTest do
|
|||
]}
|
||||
end)
|
||||
|
||||
expect(SnmpMock, :walk, fn _, @gauge_table, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, @optical_table, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :get, 2, fn _, _oid, _ -> {:ok, 10} end)
|
||||
|
||||
sensors = Routeros.discover_wireless_sensors(@client_opts)
|
||||
|
|
@ -228,6 +238,8 @@ defmodule Towerops.Snmp.Profiles.Vendors.RouterosTest do
|
|||
expect(SnmpMock, :walk, fn _, @wl_stat_table, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, @wl_60g_table, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, @lte_modem_table, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, @gauge_table, _ -> {:ok, []} end)
|
||||
expect(SnmpMock, :walk, fn _, @optical_table, _ -> {:ok, []} end)
|
||||
|
||||
# System sensors (DHCP leases and PPPoE)
|
||||
expect(SnmpMock, :get, 2, fn _, _oid, _ -> {:ok, 100} end)
|
||||
|
|
@ -247,6 +259,8 @@ defmodule Towerops.Snmp.Profiles.Vendors.RouterosTest do
|
|||
expect(SnmpMock, :walk, fn _, @wl_stat_table, _ -> {:error, :timeout} end)
|
||||
expect(SnmpMock, :walk, fn _, @wl_60g_table, _ -> {:error, :timeout} end)
|
||||
expect(SnmpMock, :walk, fn _, @lte_modem_table, _ -> {:error, :timeout} end)
|
||||
expect(SnmpMock, :walk, fn _, @gauge_table, _ -> {:error, :timeout} end)
|
||||
expect(SnmpMock, :walk, fn _, @optical_table, _ -> {:error, :timeout} end)
|
||||
expect(SnmpMock, :get, 2, fn _, _, _ -> {:error, :timeout} end)
|
||||
|
||||
sensors = Routeros.discover_wireless_sensors(@client_opts)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue