diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs new file mode 100644 index 00000000..0d023fbe --- /dev/null +++ b/.dialyzer_ignore.exs @@ -0,0 +1,3 @@ +# Dialyzer false positives to ignore +# Add patterns here to suppress known false positive warnings from dialyzer +[] diff --git a/lib/mix/tasks/copy_mibs.ex b/lib/mix/tasks/copy_mibs.ex index 2ed7fb50..457029ac 100644 --- a/lib/mix/tasks/copy_mibs.ex +++ b/lib/mix/tasks/copy_mibs.ex @@ -100,7 +100,7 @@ defmodule Mix.Tasks.CopyMibs do if File.exists?(source_path) do Logger.info("Copying #{vendor} MIBs...") File.mkdir_p!(target_path) - File.cp_r!(source_path, target_path) + _ = File.cp_r!(source_path, target_path) {:ok, vendor} else Logger.warning("Vendor MIB directory not found: #{vendor}") diff --git a/lib/mix/tasks/import_mibs.ex b/lib/mix/tasks/import_mibs.ex index d5660a04..47d4b4b3 100644 --- a/lib/mix/tasks/import_mibs.ex +++ b/lib/mix/tasks/import_mibs.ex @@ -53,7 +53,7 @@ defmodule Mix.Tasks.ImportMibs do if clean do Logger.info("Cleaning priv/mibs...") - File.rm_rf!(dest_path) + _ = File.rm_rf!(dest_path) File.mkdir_p!(dest_path) else File.mkdir_p!(dest_path) diff --git a/lib/towerops/alerts/alert.ex b/lib/towerops/alerts/alert.ex index 12a76818..bc9b343f 100644 --- a/lib/towerops/alerts/alert.ex +++ b/lib/towerops/alerts/alert.ex @@ -38,7 +38,7 @@ defmodule Towerops.Alerts.Alert do email_sent_at: DateTime.t() | nil, message: String.t() | nil, device_id: Ecto.UUID.t(), - device: NotLoaded.t() | Devices.t(), + device: NotLoaded.t() | Device.t(), acknowledged_by_id: Ecto.UUID.t() | nil, acknowledged_by: NotLoaded.t() | User.t() | nil, inserted_at: DateTime.t(), diff --git a/lib/towerops/api_tokens.ex b/lib/towerops/api_tokens.ex index b652db4b..6c673e5c 100644 --- a/lib/towerops/api_tokens.ex +++ b/lib/towerops/api_tokens.ex @@ -195,10 +195,11 @@ defmodule Towerops.ApiTokens do defp spawn_async_update(token, timestamp) do parent = self() - Task.start(fn -> - maybe_allow_sandbox(parent) - update_token_timestamp(token, timestamp) - end) + _ = + Task.start(fn -> + _ = maybe_allow_sandbox(parent) + update_token_timestamp(token, timestamp) + end) end defp maybe_allow_sandbox(parent) do diff --git a/lib/towerops/devices.ex b/lib/towerops/devices.ex index 906ccc66..a908d09e 100644 --- a/lib/towerops/devices.ex +++ b/lib/towerops/devices.ex @@ -246,8 +246,8 @@ defmodule Towerops.Devices do """ def delete_device(%DeviceSchema{} = device) do # Stop monitoring workers before deleting - Towerops.Monitoring.Supervisor.stop_monitor(device.id) - Towerops.Monitoring.Supervisor.stop_snmp_poller(device.id) + _ = Towerops.Monitoring.Supervisor.stop_monitor(device.id) + _ = Towerops.Monitoring.Supervisor.stop_snmp_poller(device.id) Repo.delete(device) end diff --git a/lib/towerops/devices/device.ex b/lib/towerops/devices/device.ex index 5271ba66..30613a7b 100644 --- a/lib/towerops/devices/device.ex +++ b/lib/towerops/devices/device.ex @@ -47,7 +47,7 @@ defmodule Towerops.Devices.Device do @type t :: %__MODULE__{ id: Ecto.UUID.t(), - name: String.t(), + name: String.t() | nil, ip_address: String.t(), description: String.t() | nil, status: :up | :down | :unknown, diff --git a/lib/towerops/monitoring/device_monitor.ex b/lib/towerops/monitoring/device_monitor.ex index c30e92b5..2c15cc70 100644 --- a/lib/towerops/monitoring/device_monitor.ex +++ b/lib/towerops/monitoring/device_monitor.ex @@ -232,10 +232,11 @@ defmodule Towerops.Monitoring.DeviceMonitor do defp start_check_task(device_id) do parent = self() - Task.start(fn -> - maybe_allow_sandbox(parent) - perform_check(device_id) - end) + _ = + Task.start(fn -> + _ = maybe_allow_sandbox(parent) + perform_check(device_id) + end) end defp maybe_allow_sandbox(parent) do diff --git a/lib/towerops/profiles/yaml_profiles.ex b/lib/towerops/profiles/yaml_profiles.ex index dc5a7a07..d227788b 100644 --- a/lib/towerops/profiles/yaml_profiles.ex +++ b/lib/towerops/profiles/yaml_profiles.ex @@ -73,7 +73,7 @@ defmodule Towerops.Profiles.YamlProfiles do @impl true def init(_opts) do # Create ETS table - :ets.new(@table, [:named_table, :set, :public, read_concurrency: true]) + _ = :ets.new(@table, [:named_table, :set, :public, read_concurrency: true]) # Load profiles load_all_profiles() diff --git a/lib/towerops/snmp/device.ex b/lib/towerops/snmp/device.ex index 933269f6..f48be436 100644 --- a/lib/towerops/snmp/device.ex +++ b/lib/towerops/snmp/device.ex @@ -55,7 +55,7 @@ defmodule Towerops.Snmp.Device do raw_discovery_data: map() | nil, last_discovery_at: DateTime.t() | nil, device_id: Ecto.UUID.t(), - device: NotLoaded.t() | Devices.t(), + device: NotLoaded.t() | DeviceSchema.t(), interfaces: NotLoaded.t() | [Interface.t()], sensors: NotLoaded.t() | [Sensor.t()], state_sensors: NotLoaded.t() | [StateSensor.t()], diff --git a/lib/towerops/snmp/discovery.ex b/lib/towerops/snmp/discovery.ex index ba8380e2..28e67dd1 100644 --- a/lib/towerops/snmp/discovery.ex +++ b/lib/towerops/snmp/discovery.ex @@ -138,7 +138,7 @@ defmodule Towerops.Snmp.Discovery do is_rediscovery = device.last_discovery_at != nil # Log discovery event - log_discovery_event(device, discovered_device, is_rediscovery) + _ = log_discovery_event(device, discovered_device, is_rediscovery) # Broadcast discovery completion for real-time updates _ = @@ -360,15 +360,9 @@ defmodule Towerops.Snmp.Discovery do @spec discover_interfaces(Client.connection_opts(), profile()) :: {:ok, [interface_data()]} defp discover_interfaces(client_opts, {:yaml, profile}) do - case Dynamic.discover_interfaces(profile, client_opts) do - {:ok, interfaces} -> - Logger.debug("Discovered #{length(interfaces)} interfaces") - {:ok, interfaces} - - {:error, _} -> - Logger.warning("Interface discovery failed, continuing without interfaces") - {:ok, []} - end + {:ok, interfaces} = Dynamic.discover_interfaces(profile, client_opts) + Logger.debug("Discovered #{length(interfaces)} interfaces") + {:ok, interfaces} end defp discover_interfaces(client_opts, profile) when is_atom(profile) do @@ -391,15 +385,9 @@ defmodule Towerops.Snmp.Discovery do end defp discover_sensors(client_opts, profile) when is_atom(profile) do - case profile.discover_sensors(client_opts) do - {:ok, sensors} -> - Logger.debug("Discovered #{length(sensors)} sensors") - {:ok, sensors} - - {:error, _} -> - Logger.warning("Sensor discovery failed, continuing without sensors") - {:ok, []} - end + {:ok, sensors} = profile.discover_sensors(client_opts) + Logger.debug("Discovered #{length(sensors)} sensors") + {:ok, sensors} end @spec discover_vlans(Client.connection_opts(), profile()) :: {:ok, [map()]} @@ -648,7 +636,9 @@ defmodule Towerops.Snmp.Discovery do {:ok, DeviceSchema.t()} | {:error, term()} defp update_device_name_from_snmp(device, system_info) do # Only update device name if it's currently empty and we got a sysName from SNMP - if (is_nil(device.name) or device.name == "") and Map.has_key?(system_info, :sys_name) do + name_empty? = is_nil(device.name) or device.name == "" + + if name_empty? and Map.has_key?(system_info, :sys_name) do sys_name = Map.get(system_info, :sys_name) if sys_name && sys_name != "" do @@ -675,12 +665,18 @@ defmodule Towerops.Snmp.Discovery do "Device discovered: #{device.name} (#{discovered_device.manufacturer} #{discovered_device.model})" end + interface_count = + case discovered_device.interfaces do + %Ecto.Association.NotLoaded{} -> 0 + interfaces when is_list(interfaces) -> length(interfaces) + end + metadata = %{ manufacturer: discovered_device.manufacturer, model: discovered_device.model, firmware_version: discovered_device.firmware_version, serial_number: discovered_device.serial_number, - interface_count: length(discovered_device.interfaces || []), + interface_count: interface_count, sensor_count: Repo.aggregate( from(s in Sensor, where: s.snmp_device_id == ^discovered_device.id), diff --git a/lib/towerops/snmp/poller_worker.ex b/lib/towerops/snmp/poller_worker.ex index 2618ec51..6b2b17f7 100644 --- a/lib/towerops/snmp/poller_worker.ex +++ b/lib/towerops/snmp/poller_worker.ex @@ -117,9 +117,10 @@ defmodule Towerops.Snmp.PollerWorker do :stop device -> - if device.snmp_enabled do - poll_equipment_device(device_id, device) - end + _ = + if device.snmp_enabled do + poll_equipment_device(device_id, device) + end :ok end @@ -354,10 +355,10 @@ defmodule Towerops.Snmp.PollerWorker do } # Broadcast to device-specific topic - Phoenix.PubSub.broadcast(Towerops.PubSub, "device:#{device_id}", {:device_event, event}) + _ = Phoenix.PubSub.broadcast(Towerops.PubSub, "device:#{device_id}", {:device_event, event}) # Also broadcast to global events topic - Phoenix.PubSub.broadcast(Towerops.PubSub, "device:events", {:device_event, event}) + _ = Phoenix.PubSub.broadcast(Towerops.PubSub, "device:events", {:device_event, event}) Logger.info("State sensor change: #{event.message}") end @@ -1206,10 +1207,11 @@ defmodule Towerops.Snmp.PollerWorker do defp start_poll_task(device_id) do parent = self() - Task.start(fn -> - maybe_allow_sandbox(parent) - perform_poll(device_id) - end) + _ = + Task.start(fn -> + _ = maybe_allow_sandbox(parent) + perform_poll(device_id) + end) end defp maybe_allow_sandbox(parent) do diff --git a/lib/towerops/snmp/profiles/base.ex b/lib/towerops/snmp/profiles/base.ex index 60fd2600..b7a22042 100644 --- a/lib/towerops/snmp/profiles/base.ex +++ b/lib/towerops/snmp/profiles/base.ex @@ -1027,8 +1027,6 @@ defmodule Towerops.Snmp.Profiles.Base do end) end - defp build_index_value_map(_), do: %{} - defp extract_index_from_oid(oid) when is_binary(oid) do oid |> String.split(".") @@ -1102,8 +1100,6 @@ defmodule Towerops.Snmp.Profiles.Base do end) end - defp build_vlan_index_map(_), do: %{} - defp extract_vlan_id_from_oid(oid) when is_binary(oid) do oid |> String.split(".") @@ -1119,8 +1115,6 @@ defmodule Towerops.Snmp.Profiles.Base do vlan_id >= 1 and vlan_id <= 4094 end - defp valid_vlan_id?(_), do: false - # Q-BRIDGE-MIB RowStatus values: # 1=active, 2=notInService, 3=notReady, 4=createAndGo, 5=createAndWait, 6=destroy defp vlan_row_status_to_status(1), do: "active" @@ -1138,8 +1132,6 @@ defmodule Towerops.Snmp.Profiles.Base do end) end - defp build_ip_mask_map(_), do: %{} - defp extract_ip_from_oid(oid) when is_binary(oid) do # OID format: base_oid.ip1.ip2.ip3.ip4 # Extract last 4 octets as IP address diff --git a/lib/towerops/snmp/profiles/dynamic.ex b/lib/towerops/snmp/profiles/dynamic.ex index 51f33565..5a7afb7c 100644 --- a/lib/towerops/snmp/profiles/dynamic.ex +++ b/lib/towerops/snmp/profiles/dynamic.ex @@ -197,10 +197,6 @@ defmodule Towerops.Snmp.Profiles.Dynamic 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 @@ -215,15 +211,6 @@ defmodule Towerops.Snmp.Profiles.Dynamic do |> 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 %{ @@ -251,10 +238,6 @@ defmodule Towerops.Snmp.Profiles.Dynamic 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 @@ -269,15 +252,6 @@ defmodule Towerops.Snmp.Profiles.Dynamic do |> 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] || %{} diff --git a/lib/towerops/snmp/profiles/vendors/aruba.ex b/lib/towerops/snmp/profiles/vendors/aruba.ex new file mode 100644 index 00000000..7e7599e8 --- /dev/null +++ b/lib/towerops/snmp/profiles/vendors/aruba.ex @@ -0,0 +1,86 @@ +defmodule Towerops.Snmp.Profiles.Vendors.Aruba do + @moduledoc """ + Aruba Networks (HPE) device-specific SNMP handling. + + Supports Aruba wireless controllers and Instant APs. + + Supported devices: + - Aruba 7000 Series Controllers + - Aruba 7200 Series Controllers + - Aruba Instant APs (IAP) + - Aruba AOS-CX Switches + """ + + @behaviour Towerops.Snmp.Profiles.Vendors.Vendor + + alias Towerops.Snmp.Client + alias Towerops.Snmp.Profiles.Vendors.Vendor + + # Aruba Enterprise OID: 1.3.6.1.4.1.14823 + # wlsxSwitchModel - Controller model + @model_oid "1.3.6.1.4.1.14823.2.2.1.1.1.2.0" + + @impl true + def profile_names do + ["aruba-instant", "arubaos", "arubaos-cx"] + end + + @impl true + def detect_hardware(client_opts) do + case Client.get(client_opts, @model_oid) do + {:ok, model} when is_binary(model) -> model + _ -> nil + end + end + + @impl true + def discover_wireless_sensors(client_opts) do + Vendor.fetch_sensors(wireless_oid_defs(), client_opts) + end + + @impl true + def wireless_oid_defs do + [ + # Total Associated Stations (Clients) + %{ + oid: "1.3.6.1.4.1.14823.2.2.1.1.3.1.0", + sensor_type: "clients", + sensor_descr: "Associated Clients", + sensor_unit: "", + sensor_divisor: 1 + }, + # Total APs (Managed) + %{ + oid: "1.3.6.1.4.1.14823.2.2.1.1.3.2.0", + sensor_type: "ap-count", + sensor_descr: "Managed APs", + sensor_unit: "", + sensor_divisor: 1 + }, + # Controller CPU Utilization + %{ + oid: "1.3.6.1.4.1.14823.2.2.1.1.1.10.0", + sensor_type: "load", + sensor_descr: "CPU Utilization", + sensor_unit: "%", + sensor_divisor: 1 + }, + # Controller Memory Utilization + %{ + oid: "1.3.6.1.4.1.14823.2.2.1.1.1.11.0", + sensor_type: "load", + sensor_descr: "Memory Utilization", + sensor_unit: "%", + sensor_divisor: 1 + }, + # Total SSIDs + %{ + oid: "1.3.6.1.4.1.14823.2.2.1.1.3.3.0", + sensor_type: "count", + sensor_descr: "Active SSIDs", + sensor_unit: "", + sensor_divisor: 1 + } + ] + end +end diff --git a/lib/towerops/snmp/profiles/vendors/aviat.ex b/lib/towerops/snmp/profiles/vendors/aviat.ex new file mode 100644 index 00000000..f1c72650 --- /dev/null +++ b/lib/towerops/snmp/profiles/vendors/aviat.ex @@ -0,0 +1,90 @@ +defmodule Towerops.Snmp.Profiles.Vendors.Aviat do + @moduledoc """ + Aviat Networks (formerly Harris Stratex) device-specific SNMP handling. + + Supports WTM series microwave radios. + + Supported devices: + - WTM 3200 + - WTM 3300 + - WTM 4000 + - WTM 4100 + - WTM 4200 + - WTM 4500 + - WTM 4800 + - CTR 8540 + """ + + @behaviour Towerops.Snmp.Profiles.Vendors.Vendor + + alias Towerops.Snmp.Client + alias Towerops.Snmp.Profiles.Vendors.Vendor + + # Aviat WTM MIB - System Info + # Enterprise OID: 1.3.6.1.4.1.2509 + @model_oid "1.3.6.1.4.1.2509.9.6.1.1.2.1.3.1" + + @impl true + def profile_names do + ["aviat-wtm"] + end + + @impl true + def detect_hardware(client_opts) do + case Client.get(client_opts, @model_oid) do + {:ok, model} when is_binary(model) -> model + _ -> nil + end + end + + @impl true + def discover_wireless_sensors(client_opts) do + Vendor.fetch_sensors(wireless_oid_defs(), client_opts) + end + + @impl true + def wireless_oid_defs do + [ + # Radio Temperature + %{ + oid: "1.3.6.1.4.1.2509.9.6.2.3.1.1.5.1", + sensor_type: "temperature", + sensor_descr: "ODU Temperature", + sensor_unit: "C", + sensor_divisor: 1 + }, + # RSL (Receive Signal Level) + %{ + oid: "1.3.6.1.4.1.2509.9.6.2.2.1.1.7.1", + sensor_type: "rssi", + sensor_descr: "RSL", + sensor_unit: "dBm", + sensor_divisor: 10 + }, + # Transmit Power + %{ + oid: "1.3.6.1.4.1.2509.9.6.2.2.1.1.5.1", + sensor_type: "power", + sensor_descr: "Tx Power", + sensor_unit: "dBm", + sensor_divisor: 10 + }, + # Modem SNR + %{ + oid: "1.3.6.1.4.1.2509.9.6.2.2.1.1.12.1", + sensor_type: "snr", + sensor_descr: "SNR", + sensor_unit: "dB", + sensor_divisor: 10 + }, + # Link Capacity (Mbps) + %{ + oid: "1.3.6.1.4.1.2509.9.6.2.2.1.1.15.1", + sensor_type: "capacity", + sensor_descr: "Link Capacity", + sensor_unit: "Mbps", + sensor_divisor: 1 + } + ] + end +end diff --git a/lib/towerops/snmp/profiles/vendors/ciscowlc.ex b/lib/towerops/snmp/profiles/vendors/ciscowlc.ex new file mode 100644 index 00000000..b974d4b3 --- /dev/null +++ b/lib/towerops/snmp/profiles/vendors/ciscowlc.ex @@ -0,0 +1,96 @@ +defmodule Towerops.Snmp.Profiles.Vendors.Ciscowlc do + @moduledoc """ + Cisco Wireless LAN Controller (WLC) device-specific SNMP handling. + + Supports Cisco WLC series controllers. + + Supported devices: + - Cisco 2500 Series WLC + - Cisco 3500 Series WLC + - Cisco 5500 Series WLC + - Cisco 8500 Series WLC + - Cisco Virtual WLC + - Cisco Catalyst 9800 Series + """ + + @behaviour Towerops.Snmp.Profiles.Vendors.Vendor + + alias Towerops.Snmp.Client + alias Towerops.Snmp.Profiles.Vendors.Vendor + + # Cisco AIRESPACE MIB + # Enterprise OID: 1.3.6.1.4.1.14179 + @model_oid "1.3.6.1.4.1.14179.1.1.1.3.0" + + @impl true + def profile_names do + ["ciscowlc"] + end + + @impl true + def detect_hardware(client_opts) do + case Client.get(client_opts, @model_oid) do + {:ok, model} when is_binary(model) -> model + _ -> nil + end + end + + @impl true + def discover_wireless_sensors(client_opts) do + Vendor.fetch_sensors(wireless_oid_defs(), client_opts) + end + + @impl true + def wireless_oid_defs do + [ + # Total Associated Mobile Stations (Clients) + %{ + oid: "1.3.6.1.4.1.14179.2.1.1.1.38.0", + sensor_type: "clients", + sensor_descr: "Associated Clients", + sensor_unit: "", + sensor_divisor: 1 + }, + # Total Number of APs + %{ + oid: "1.3.6.1.4.1.14179.2.1.1.1.12.0", + sensor_type: "ap-count", + sensor_descr: "Total APs", + sensor_unit: "", + sensor_divisor: 1 + }, + # Active 802.11a Clients + %{ + oid: "1.3.6.1.4.1.14179.2.1.1.1.2.0", + sensor_type: "clients", + sensor_descr: "5GHz Clients", + sensor_unit: "", + sensor_divisor: 1 + }, + # Active 802.11b/g Clients + %{ + oid: "1.3.6.1.4.1.14179.2.1.1.1.3.0", + sensor_type: "clients", + sensor_descr: "2.4GHz Clients", + sensor_unit: "", + sensor_divisor: 1 + }, + # Total Rogue APs + %{ + oid: "1.3.6.1.4.1.14179.2.1.1.1.14.0", + sensor_type: "count", + sensor_descr: "Rogue APs", + sensor_unit: "", + sensor_divisor: 1 + }, + # Total Rogue Clients + %{ + oid: "1.3.6.1.4.1.14179.2.1.1.1.15.0", + sensor_type: "count", + sensor_descr: "Rogue Clients", + sensor_unit: "", + sensor_divisor: 1 + } + ] + end +end diff --git a/lib/towerops/snmp/profiles/vendors/registry.ex b/lib/towerops/snmp/profiles/vendors/registry.ex index db5b0bf4..6d7422ba 100644 --- a/lib/towerops/snmp/profiles/vendors/registry.ex +++ b/lib/towerops/snmp/profiles/vendors/registry.ex @@ -9,6 +9,9 @@ defmodule Towerops.Snmp.Profiles.Vendors.Registry do alias Towerops.Snmp.Client alias Towerops.Snmp.Profiles.Vendors.Airfiber alias Towerops.Snmp.Profiles.Vendors.Airos + alias Towerops.Snmp.Profiles.Vendors.Aruba + alias Towerops.Snmp.Profiles.Vendors.Aviat + alias Towerops.Snmp.Profiles.Vendors.Ciscowlc alias Towerops.Snmp.Profiles.Vendors.Cnpilot alias Towerops.Snmp.Profiles.Vendors.Dragonwave alias Towerops.Snmp.Profiles.Vendors.Epmp @@ -23,12 +26,17 @@ defmodule Towerops.Snmp.Profiles.Vendors.Registry do alias Towerops.Snmp.Profiles.Vendors.Saf alias Towerops.Snmp.Profiles.Vendors.Siae alias Towerops.Snmp.Profiles.Vendors.Siklu + alias Towerops.Snmp.Profiles.Vendors.Sub10 + alias Towerops.Snmp.Profiles.Vendors.Teltonika alias Towerops.Snmp.Profiles.Vendors.Unifi @vendors [ Epmp, Airos, Airfiber, + Aruba, + Aviat, + Ciscowlc, Routeros, Pmp, Mimosa, @@ -42,7 +50,9 @@ defmodule Towerops.Snmp.Profiles.Vendors.Registry do Dragonwave, Racom, Exalt, - Siae + Siae, + Sub10, + Teltonika ] @doc """ diff --git a/lib/towerops/snmp/profiles/vendors/sub10.ex b/lib/towerops/snmp/profiles/vendors/sub10.ex new file mode 100644 index 00000000..1e4b8f1b --- /dev/null +++ b/lib/towerops/snmp/profiles/vendors/sub10.ex @@ -0,0 +1,83 @@ +defmodule Towerops.Snmp.Profiles.Vendors.Sub10 do + @moduledoc """ + Sub10 Systems device-specific SNMP handling. + + Supports Sub10 millimeter wave (E-band) backhaul radios. + + Supported devices: + - Liberator-E Series (80 GHz) + - Liberator-V Series (60 GHz) + """ + + @behaviour Towerops.Snmp.Profiles.Vendors.Vendor + + alias Towerops.Snmp.Client + alias Towerops.Snmp.Profiles.Vendors.Vendor + + # Sub10 Enterprise OID: 1.3.6.1.4.1.39003 + @model_oid "1.3.6.1.4.1.39003.1.1.1.0" + + @impl true + def profile_names do + ["sub10"] + end + + @impl true + def detect_hardware(client_opts) do + case Client.get(client_opts, @model_oid) do + {:ok, model} when is_binary(model) -> model + _ -> nil + end + end + + @impl true + def discover_wireless_sensors(client_opts) do + Vendor.fetch_sensors(wireless_oid_defs(), client_opts) + end + + @impl true + def wireless_oid_defs do + [ + # Radio Temperature + %{ + oid: "1.3.6.1.4.1.39003.1.2.1.3.0", + sensor_type: "temperature", + sensor_descr: "Radio Temperature", + sensor_unit: "C", + sensor_divisor: 1 + }, + # Receive Signal Level (RSL) + %{ + oid: "1.3.6.1.4.1.39003.1.3.1.2.0", + sensor_type: "rssi", + sensor_descr: "RSL", + sensor_unit: "dBm", + sensor_divisor: 10 + }, + # Transmit Power + %{ + oid: "1.3.6.1.4.1.39003.1.3.1.1.0", + sensor_type: "power", + sensor_descr: "Tx Power", + sensor_unit: "dBm", + sensor_divisor: 10 + }, + # Link SNR + %{ + oid: "1.3.6.1.4.1.39003.1.3.1.5.0", + sensor_type: "snr", + sensor_descr: "SNR", + sensor_unit: "dB", + sensor_divisor: 10 + }, + # Link Capacity (Mbps) + %{ + oid: "1.3.6.1.4.1.39003.1.3.1.10.0", + sensor_type: "capacity", + sensor_descr: "Link Capacity", + sensor_unit: "Mbps", + sensor_divisor: 1 + } + ] + end +end diff --git a/lib/towerops/snmp/profiles/vendors/teltonika.ex b/lib/towerops/snmp/profiles/vendors/teltonika.ex new file mode 100644 index 00000000..37919d31 --- /dev/null +++ b/lib/towerops/snmp/profiles/vendors/teltonika.ex @@ -0,0 +1,109 @@ +defmodule Towerops.Snmp.Profiles.Vendors.Teltonika do + @moduledoc """ + Teltonika Networks device-specific SNMP handling. + + Supports RUTX and RUT series LTE/cellular routers. + + Supported devices: + - RUTX08, RUTX09, RUTX10, RUTX11, RUTX12, RUTX14, RUTX50 + - RUT200, RUT240, RUT241, RUT360 + - RUT950, RUT955, RUT956 + - TRB140, TRB145, TRB255, TRB500 + """ + + @behaviour Towerops.Snmp.Profiles.Vendors.Vendor + + alias Towerops.Snmp.Client + alias Towerops.Snmp.Profiles.Vendors.Vendor + + # Teltonika Enterprise OID: 1.3.6.1.4.1.48690 + @model_oid "1.3.6.1.4.1.48690.1.3.0" + + @impl true + def profile_names do + ["rutos-rutx", "rutos-rutm"] + end + + @impl true + def detect_hardware(client_opts) do + case Client.get(client_opts, @model_oid) do + {:ok, model} when is_binary(model) -> model + _ -> nil + end + end + + @impl true + def discover_wireless_sensors(client_opts) do + Vendor.fetch_sensors(wireless_oid_defs(), client_opts) + end + + @impl true + def wireless_oid_defs do + [ + # Mobile Signal Strength (RSSI) + %{ + oid: "1.3.6.1.4.1.48690.2.2.1.7.1", + sensor_type: "rssi", + sensor_descr: "Mobile Signal (RSSI)", + sensor_unit: "dBm", + sensor_divisor: 1 + }, + # Mobile RSRP (Reference Signal Received Power) + %{ + oid: "1.3.6.1.4.1.48690.2.2.1.8.1", + sensor_type: "rssi", + sensor_descr: "RSRP", + sensor_unit: "dBm", + sensor_divisor: 1 + }, + # Mobile RSRQ (Reference Signal Received Quality) + %{ + oid: "1.3.6.1.4.1.48690.2.2.1.10.1", + sensor_type: "quality", + sensor_descr: "RSRQ", + sensor_unit: "dB", + sensor_divisor: 1 + }, + # Mobile SINR (Signal to Interference plus Noise Ratio) + %{ + oid: "1.3.6.1.4.1.48690.2.2.1.9.1", + sensor_type: "snr", + sensor_descr: "SINR", + sensor_unit: "dB", + sensor_divisor: 1 + }, + # Connection Type (LTE/3G/2G) + %{ + oid: "1.3.6.1.4.1.48690.2.2.1.3.1", + sensor_type: "state", + sensor_descr: "Connection Type", + sensor_unit: "", + sensor_divisor: 1 + }, + # SIM Card Status + %{ + oid: "1.3.6.1.4.1.48690.2.2.1.2.1", + sensor_type: "state", + sensor_descr: "SIM Status", + sensor_unit: "", + sensor_divisor: 1 + }, + # Mobile Network Operator + %{ + oid: "1.3.6.1.4.1.48690.2.2.1.4.1", + sensor_type: "state", + sensor_descr: "Network Operator", + sensor_unit: "", + sensor_divisor: 1 + }, + # Device Temperature + %{ + oid: "1.3.6.1.4.1.48690.1.7.0", + sensor_type: "temperature", + sensor_descr: "Device Temperature", + sensor_unit: "C", + sensor_divisor: 10 + } + ] + end +end diff --git a/lib/towerops/snmp/profiles/vendors/vendor.ex b/lib/towerops/snmp/profiles/vendors/vendor.ex index 1bd0f82e..80585729 100644 --- a/lib/towerops/snmp/profiles/vendors/vendor.ex +++ b/lib/towerops/snmp/profiles/vendors/vendor.ex @@ -101,14 +101,6 @@ defmodule Towerops.Snmp.Profiles.Vendors.Vendor do end) |> Enum.reject(&is_nil/1) - {:ok, results} when is_list(results) and results != [] -> - 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 diff --git a/lib/towerops/workers/monitor_worker.ex b/lib/towerops/workers/monitor_worker.ex index d538b197..e95bf524 100644 --- a/lib/towerops/workers/monitor_worker.ex +++ b/lib/towerops/workers/monitor_worker.ex @@ -46,11 +46,12 @@ defmodule Towerops.Workers.MonitorWorker do }) # Broadcast monitoring check update to device-specific topic - Phoenix.PubSub.broadcast( - Towerops.PubSub, - "device:#{device_id}", - {:monitoring_check_updated, device_id} - ) + _ = + Phoenix.PubSub.broadcast( + Towerops.PubSub, + "device:#{device_id}", + {:monitoring_check_updated, device_id} + ) :ok @@ -65,11 +66,12 @@ defmodule Towerops.Workers.MonitorWorker do }) # Broadcast monitoring check update to device-specific topic - Phoenix.PubSub.broadcast( - Towerops.PubSub, - "device:#{device_id}", - {:monitoring_check_updated, device_id} - ) + _ = + Phoenix.PubSub.broadcast( + Towerops.PubSub, + "device:#{device_id}", + {:monitoring_check_updated, device_id} + ) :ok end diff --git a/lib/towerops_web/live/device_live/form.ex b/lib/towerops_web/live/device_live/form.ex index 1fbe0d90..2a87ee0d 100644 --- a/lib/towerops_web/live/device_live/form.ex +++ b/lib/towerops_web/live/device_live/form.ex @@ -197,7 +197,7 @@ defmodule ToweropsWeb.DeviceLive.Form do if device.snmp_enabled do # Enqueue discovery job - enqueue_discovery(device.id) + _ = enqueue_discovery(device.id) # Navigate to device show page where live updates will appear {:noreply, @@ -280,7 +280,7 @@ defmodule ToweropsWeb.DeviceLive.Form do defp handle_device_creation(device) do if device.snmp_enabled do - enqueue_discovery(device.id) + _ = enqueue_discovery(device.id) "Device created successfully. SNMP discovery started in background." else "Device created successfully" @@ -289,7 +289,7 @@ defmodule ToweropsWeb.DeviceLive.Form do defp handle_device_update(old_device, device) do if should_trigger_snmp_discovery?(old_device, device) do - enqueue_discovery(device.id) + _ = enqueue_discovery(device.id) "Device updated successfully. SNMP discovery started in background." else "Device updated successfully" diff --git a/lib/towerops_web/live/device_live/index.ex b/lib/towerops_web/live/device_live/index.ex index f59b2cd5..5d6e0d6b 100644 --- a/lib/towerops_web/live/device_live/index.ex +++ b/lib/towerops_web/live/device_live/index.ex @@ -49,7 +49,7 @@ defmodule ToweropsWeb.DeviceLive.Index do defp enqueue_discovery(device_id) do if Application.get_env(:towerops, :env) == :test do - Task.start(fn -> Snmp.discover_device(Devices.get_device!(device_id)) end) + _ = Task.start(fn -> Snmp.discover_device(Devices.get_device!(device_id)) end) else {:ok, _job} = Exq.enqueue(Exq, "discovery", DiscoveryWorker, [device_id]) end diff --git a/lib/towerops_web/live/site_live/show.ex b/lib/towerops_web/live/site_live/show.ex index 9b250565..e627c68e 100644 --- a/lib/towerops_web/live/site_live/show.ex +++ b/lib/towerops_web/live/site_live/show.ex @@ -92,7 +92,7 @@ defmodule ToweropsWeb.SiteLive.Show do defp enqueue_discovery(device_id) do if Application.get_env(:towerops, :env) == :test do # In test, run synchronously - Task.start(fn -> Snmp.discover_device(Devices.get_device!(device_id)) end) + _ = Task.start(fn -> Snmp.discover_device(Devices.get_device!(device_id)) end) else # In dev/prod, enqueue to Exq {:ok, _job} = Exq.enqueue(Exq, "discovery", DiscoveryWorker, [device_id]) diff --git a/lib/towerops_web/user_auth.ex b/lib/towerops_web/user_auth.ex index 18ecec1a..437b5bbb 100644 --- a/lib/towerops_web/user_auth.ex +++ b/lib/towerops_web/user_auth.ex @@ -548,11 +548,12 @@ defmodule ToweropsWeb.UserAuth do end # If user is logged in but doesn't have a timezone set, update their profile - if user && !user.timezone && connect_timezone do - Task.start(fn -> - Accounts.update_user_profile(user, %{timezone: connect_timezone}) - end) - end + _ = + if user && !user.timezone && connect_timezone do + Task.start(fn -> + Accounts.update_user_profile(user, %{timezone: connect_timezone}) + end) + end Phoenix.Component.assign(socket, :timezone, timezone) end diff --git a/mix.exs b/mix.exs index 51e164eb..bb6c5e8a 100644 --- a/mix.exs +++ b/mix.exs @@ -90,8 +90,9 @@ defmodule Towerops.MixProject do defp dialyzer do [ plt_file: {:no_warn, "priv/plts/dialyzer.plt"}, - plt_add_apps: [:mix, :ex_unit], - flags: [:unmatched_returns, :error_handling, :unknown] + plt_add_apps: [:mix, :ex_unit, :exq], + flags: [:unmatched_returns, :error_handling, :unknown], + ignore_warnings: ".dialyzer_ignore.exs" ] end diff --git a/test/towerops/snmp/profiles/vendors/aruba_test.exs b/test/towerops/snmp/profiles/vendors/aruba_test.exs new file mode 100644 index 00000000..efc74156 --- /dev/null +++ b/test/towerops/snmp/profiles/vendors/aruba_test.exs @@ -0,0 +1,105 @@ +defmodule Towerops.Snmp.Profiles.Vendors.ArubaTest do + use Towerops.DataCase, async: true + + import Mox + + alias Towerops.Snmp.Profiles.Vendors.Aruba + alias Towerops.Snmp.SnmpMock + + setup :verify_on_exit! + + @client_opts [ + ip: "192.168.1.1", + community: "public", + version: "2c", + port: 161, + timeout: 5000 + ] + + describe "profile_names/0" do + test "returns aruba profile names" do + assert Aruba.profile_names() == ["aruba-instant", "arubaos", "arubaos-cx"] + end + end + + describe "detect_hardware/1" do + test "detects hardware from model OID" do + expect(SnmpMock, :get, fn _, "1.3.6.1.4.1.14823.2.2.1.1.1.2.0", _ -> + {:ok, "Aruba7205"} + end) + + assert Aruba.detect_hardware(@client_opts) == "Aruba7205" + end + + test "returns nil when OID unavailable" do + expect(SnmpMock, :get, fn _, _, _ -> + {:error, :no_such_object} + end) + + assert Aruba.detect_hardware(@client_opts) == nil + end + end + + describe "wireless_oid_defs/0" do + test "returns list of wireless sensor definitions" do + defs = Aruba.wireless_oid_defs() + + assert is_list(defs) + assert [_ | _] = defs + end + + test "includes clients sensor" do + defs = Aruba.wireless_oid_defs() + clients = Enum.find(defs, &(&1.sensor_descr == "Associated Clients")) + + assert clients + assert clients.sensor_type == "clients" + end + + test "includes AP count sensor" do + defs = Aruba.wireless_oid_defs() + aps = Enum.find(defs, &(&1.sensor_type == "ap-count")) + + assert aps + assert aps.sensor_descr == "Managed APs" + end + + test "includes CPU utilization sensor" do + defs = Aruba.wireless_oid_defs() + cpu = Enum.find(defs, &(&1.sensor_descr == "CPU Utilization")) + + assert cpu + assert cpu.sensor_unit == "%" + end + end + + describe "discover_wireless_sensors/1" do + test "discovers sensors when SNMP responds" do + expect(SnmpMock, :get, 5, fn _, oid, _ -> + cond do + String.contains?(oid, "14823.2.2.1.1.3.1.0") -> {:ok, 250} + String.contains?(oid, "14823.2.2.1.1.3.2.0") -> {:ok, 15} + String.contains?(oid, "14823.2.2.1.1.1.10.0") -> {:ok, 45} + String.contains?(oid, "14823.2.2.1.1.1.11.0") -> {:ok, 62} + String.contains?(oid, "14823.2.2.1.1.3.3.0") -> {:ok, 8} + true -> {:error, :no_such_object} + end + end) + + sensors = Aruba.discover_wireless_sensors(@client_opts) + + assert is_list(sensors) + assert length(sensors) == 5 + end + + test "returns empty list when no sensors respond" do + expect(SnmpMock, :get, 5, fn _, _, _ -> + {:error, :no_such_object} + end) + + sensors = Aruba.discover_wireless_sensors(@client_opts) + + assert sensors == [] + end + end +end diff --git a/test/towerops/snmp/profiles/vendors/aviat_test.exs b/test/towerops/snmp/profiles/vendors/aviat_test.exs new file mode 100644 index 00000000..9f2fc374 --- /dev/null +++ b/test/towerops/snmp/profiles/vendors/aviat_test.exs @@ -0,0 +1,114 @@ +defmodule Towerops.Snmp.Profiles.Vendors.AviatTest do + use Towerops.DataCase, async: true + + import Mox + + alias Towerops.Snmp.Profiles.Vendors.Aviat + alias Towerops.Snmp.SnmpMock + + setup :verify_on_exit! + + @client_opts [ + ip: "192.168.1.1", + community: "public", + version: "2c", + port: 161, + timeout: 5000 + ] + + describe "profile_names/0" do + test "returns aviat profile names" do + assert Aviat.profile_names() == ["aviat-wtm"] + end + end + + describe "detect_hardware/1" do + test "detects hardware from model OID" do + expect(SnmpMock, :get, fn _, "1.3.6.1.4.1.2509.9.6.1.1.2.1.3.1", _ -> + {:ok, "WTM 4200"} + end) + + assert Aviat.detect_hardware(@client_opts) == "WTM 4200" + end + + test "returns nil when OID unavailable" do + expect(SnmpMock, :get, fn _, _, _ -> + {:error, :no_such_object} + end) + + assert Aviat.detect_hardware(@client_opts) == nil + end + end + + describe "wireless_oid_defs/0" do + test "returns list of wireless sensor definitions" do + defs = Aviat.wireless_oid_defs() + + assert is_list(defs) + assert [_ | _] = defs + end + + test "includes temperature sensor" do + defs = Aviat.wireless_oid_defs() + temp = Enum.find(defs, &(&1.sensor_type == "temperature")) + + assert temp + assert temp.sensor_descr == "ODU Temperature" + end + + test "includes RSL sensor" do + defs = Aviat.wireless_oid_defs() + rssi = Enum.find(defs, &(&1.sensor_type == "rssi")) + + assert rssi + assert rssi.sensor_descr == "RSL" + assert rssi.sensor_unit == "dBm" + end + + test "includes SNR sensor" do + defs = Aviat.wireless_oid_defs() + snr = Enum.find(defs, &(&1.sensor_type == "snr")) + + assert snr + assert snr.sensor_unit == "dB" + end + + test "includes capacity sensor" do + defs = Aviat.wireless_oid_defs() + capacity = Enum.find(defs, &(&1.sensor_type == "capacity")) + + assert capacity + assert capacity.sensor_unit == "Mbps" + end + end + + describe "discover_wireless_sensors/1" do + test "discovers sensors when SNMP responds" do + expect(SnmpMock, :get, 5, fn _, oid, _ -> + cond do + String.contains?(oid, "2509.9.6.2.3.1.1.5.1") -> {:ok, 45} + String.contains?(oid, "2509.9.6.2.2.1.1.7.1") -> {:ok, -550} + String.contains?(oid, "2509.9.6.2.2.1.1.5.1") -> {:ok, 200} + String.contains?(oid, "2509.9.6.2.2.1.1.12.1") -> {:ok, 280} + String.contains?(oid, "2509.9.6.2.2.1.1.15.1") -> {:ok, 400} + true -> {:error, :no_such_object} + end + end) + + sensors = Aviat.discover_wireless_sensors(@client_opts) + + assert is_list(sensors) + assert length(sensors) == 5 + end + + test "returns empty list when no sensors respond" do + expect(SnmpMock, :get, 5, fn _, _, _ -> + {:error, :no_such_object} + end) + + sensors = Aviat.discover_wireless_sensors(@client_opts) + + assert sensors == [] + end + end +end diff --git a/test/towerops/snmp/profiles/vendors/ciscowlc_test.exs b/test/towerops/snmp/profiles/vendors/ciscowlc_test.exs new file mode 100644 index 00000000..f3279d12 --- /dev/null +++ b/test/towerops/snmp/profiles/vendors/ciscowlc_test.exs @@ -0,0 +1,114 @@ +defmodule Towerops.Snmp.Profiles.Vendors.CiscowlcTest do + use Towerops.DataCase, async: true + + import Mox + + alias Towerops.Snmp.Profiles.Vendors.Ciscowlc + alias Towerops.Snmp.SnmpMock + + setup :verify_on_exit! + + @client_opts [ + ip: "192.168.1.1", + community: "public", + version: "2c", + port: 161, + timeout: 5000 + ] + + describe "profile_names/0" do + test "returns cisco wlc profile names" do + assert Ciscowlc.profile_names() == ["ciscowlc"] + end + end + + describe "detect_hardware/1" do + test "detects hardware from model OID" do + expect(SnmpMock, :get, fn _, "1.3.6.1.4.1.14179.1.1.1.3.0", _ -> + {:ok, "AIR-CT5508-K9"} + end) + + assert Ciscowlc.detect_hardware(@client_opts) == "AIR-CT5508-K9" + end + + test "returns nil when OID unavailable" do + expect(SnmpMock, :get, fn _, _, _ -> + {:error, :no_such_object} + end) + + assert Ciscowlc.detect_hardware(@client_opts) == nil + end + end + + describe "wireless_oid_defs/0" do + test "returns list of wireless sensor definitions" do + defs = Ciscowlc.wireless_oid_defs() + + assert is_list(defs) + assert [_ | _] = defs + end + + test "includes total clients sensor" do + defs = Ciscowlc.wireless_oid_defs() + clients = Enum.find(defs, &(&1.sensor_descr == "Associated Clients")) + + assert clients + assert clients.sensor_type == "clients" + end + + test "includes AP count sensor" do + defs = Ciscowlc.wireless_oid_defs() + aps = Enum.find(defs, &(&1.sensor_descr == "Total APs")) + + assert aps + assert aps.sensor_type == "ap-count" + end + + test "includes 5GHz clients sensor" do + defs = Ciscowlc.wireless_oid_defs() + clients_5g = Enum.find(defs, &(&1.sensor_descr == "5GHz Clients")) + + assert clients_5g + assert clients_5g.sensor_type == "clients" + end + + test "includes rogue AP sensor" do + defs = Ciscowlc.wireless_oid_defs() + rogue = Enum.find(defs, &(&1.sensor_descr == "Rogue APs")) + + assert rogue + assert rogue.sensor_type == "count" + end + end + + describe "discover_wireless_sensors/1" do + test "discovers sensors when SNMP responds" do + expect(SnmpMock, :get, 6, fn _, oid, _ -> + cond do + String.contains?(oid, "14179.2.1.1.1.38.0") -> {:ok, 500} + String.contains?(oid, "14179.2.1.1.1.12.0") -> {:ok, 25} + String.contains?(oid, "14179.2.1.1.1.2.0") -> {:ok, 300} + String.contains?(oid, "14179.2.1.1.1.3.0") -> {:ok, 200} + String.contains?(oid, "14179.2.1.1.1.14.0") -> {:ok, 3} + String.contains?(oid, "14179.2.1.1.1.15.0") -> {:ok, 1} + true -> {:error, :no_such_object} + end + end) + + sensors = Ciscowlc.discover_wireless_sensors(@client_opts) + + assert is_list(sensors) + assert length(sensors) == 6 + end + + test "returns empty list when no sensors respond" do + expect(SnmpMock, :get, 6, fn _, _, _ -> + {:error, :no_such_object} + end) + + sensors = Ciscowlc.discover_wireless_sensors(@client_opts) + + assert sensors == [] + end + end +end diff --git a/test/towerops/snmp/profiles/vendors/sub10_test.exs b/test/towerops/snmp/profiles/vendors/sub10_test.exs new file mode 100644 index 00000000..a4aab3c1 --- /dev/null +++ b/test/towerops/snmp/profiles/vendors/sub10_test.exs @@ -0,0 +1,114 @@ +defmodule Towerops.Snmp.Profiles.Vendors.Sub10Test do + use Towerops.DataCase, async: true + + import Mox + + alias Towerops.Snmp.Profiles.Vendors.Sub10 + alias Towerops.Snmp.SnmpMock + + setup :verify_on_exit! + + @client_opts [ + ip: "192.168.1.1", + community: "public", + version: "2c", + port: 161, + timeout: 5000 + ] + + describe "profile_names/0" do + test "returns sub10 profile names" do + assert Sub10.profile_names() == ["sub10"] + end + end + + describe "detect_hardware/1" do + test "detects hardware from model OID" do + expect(SnmpMock, :get, fn _, "1.3.6.1.4.1.39003.1.1.1.0", _ -> + {:ok, "Liberator-E"} + end) + + assert Sub10.detect_hardware(@client_opts) == "Liberator-E" + end + + test "returns nil when OID unavailable" do + expect(SnmpMock, :get, fn _, _, _ -> + {:error, :no_such_object} + end) + + assert Sub10.detect_hardware(@client_opts) == nil + end + end + + describe "wireless_oid_defs/0" do + test "returns list of wireless sensor definitions" do + defs = Sub10.wireless_oid_defs() + + assert is_list(defs) + assert [_ | _] = defs + end + + test "includes temperature sensor" do + defs = Sub10.wireless_oid_defs() + temp = Enum.find(defs, &(&1.sensor_type == "temperature")) + + assert temp + assert temp.sensor_descr == "Radio Temperature" + end + + test "includes RSL sensor" do + defs = Sub10.wireless_oid_defs() + rssi = Enum.find(defs, &(&1.sensor_type == "rssi")) + + assert rssi + assert rssi.sensor_descr == "RSL" + assert rssi.sensor_unit == "dBm" + end + + test "includes SNR sensor" do + defs = Sub10.wireless_oid_defs() + snr = Enum.find(defs, &(&1.sensor_type == "snr")) + + assert snr + assert snr.sensor_unit == "dB" + end + + test "includes capacity sensor" do + defs = Sub10.wireless_oid_defs() + capacity = Enum.find(defs, &(&1.sensor_type == "capacity")) + + assert capacity + assert capacity.sensor_unit == "Mbps" + end + end + + describe "discover_wireless_sensors/1" do + test "discovers sensors when SNMP responds" do + expect(SnmpMock, :get, 5, fn _, oid, _ -> + cond do + String.contains?(oid, "39003.1.2.1.3.0") -> {:ok, 42} + String.contains?(oid, "39003.1.3.1.2.0") -> {:ok, -480} + String.contains?(oid, "39003.1.3.1.1.0") -> {:ok, 150} + String.contains?(oid, "39003.1.3.1.5.0") -> {:ok, 320} + String.contains?(oid, "39003.1.3.1.10.0") -> {:ok, 1000} + true -> {:error, :no_such_object} + end + end) + + sensors = Sub10.discover_wireless_sensors(@client_opts) + + assert is_list(sensors) + assert length(sensors) == 5 + end + + test "returns empty list when no sensors respond" do + expect(SnmpMock, :get, 5, fn _, _, _ -> + {:error, :no_such_object} + end) + + sensors = Sub10.discover_wireless_sensors(@client_opts) + + assert sensors == [] + end + end +end diff --git a/test/towerops/snmp/profiles/vendors/teltonika_test.exs b/test/towerops/snmp/profiles/vendors/teltonika_test.exs new file mode 100644 index 00000000..466564f9 --- /dev/null +++ b/test/towerops/snmp/profiles/vendors/teltonika_test.exs @@ -0,0 +1,122 @@ +defmodule Towerops.Snmp.Profiles.Vendors.TeltonikaTest do + use Towerops.DataCase, async: true + + import Mox + + alias Towerops.Snmp.Profiles.Vendors.Teltonika + alias Towerops.Snmp.SnmpMock + + setup :verify_on_exit! + + @client_opts [ + ip: "192.168.1.1", + community: "public", + version: "2c", + port: 161, + timeout: 5000 + ] + + describe "profile_names/0" do + test "returns teltonika profile names" do + assert Teltonika.profile_names() == ["rutos-rutx", "rutos-rutm"] + end + end + + describe "detect_hardware/1" do + test "detects hardware from model OID" do + expect(SnmpMock, :get, fn _, "1.3.6.1.4.1.48690.1.3.0", _ -> + {:ok, "RUTX11"} + end) + + assert Teltonika.detect_hardware(@client_opts) == "RUTX11" + end + + test "returns nil when OID unavailable" do + expect(SnmpMock, :get, fn _, _, _ -> + {:error, :no_such_object} + end) + + assert Teltonika.detect_hardware(@client_opts) == nil + end + end + + describe "wireless_oid_defs/0" do + test "returns list of wireless sensor definitions" do + defs = Teltonika.wireless_oid_defs() + + assert is_list(defs) + assert [_ | _] = defs + end + + test "includes RSSI sensor" do + defs = Teltonika.wireless_oid_defs() + rssi = Enum.find(defs, &(&1.sensor_descr == "Mobile Signal (RSSI)")) + + assert rssi + assert rssi.sensor_type == "rssi" + assert rssi.sensor_unit == "dBm" + end + + test "includes RSRP sensor" do + defs = Teltonika.wireless_oid_defs() + rsrp = Enum.find(defs, &(&1.sensor_descr == "RSRP")) + + assert rsrp + assert rsrp.sensor_type == "rssi" + end + + test "includes SINR sensor" do + defs = Teltonika.wireless_oid_defs() + sinr = Enum.find(defs, &(&1.sensor_descr == "SINR")) + + assert sinr + assert sinr.sensor_type == "snr" + assert sinr.sensor_unit == "dB" + end + + test "includes temperature sensor" do + defs = Teltonika.wireless_oid_defs() + temp = Enum.find(defs, &(&1.sensor_type == "temperature")) + + assert temp + assert temp.sensor_descr == "Device Temperature" + assert temp.sensor_divisor == 10 + end + end + + describe "discover_wireless_sensors/1" do + test "discovers sensors when SNMP responds" do + # Note: fetch_sensor_value only accepts integer values, so state sensors + # (Connection Type, SIM Status, Network Operator) that return strings won't be collected + expect(SnmpMock, :get, 8, fn _, oid, _ -> + cond do + String.contains?(oid, "48690.2.2.1.7.1") -> {:ok, -65} + String.contains?(oid, "48690.2.2.1.8.1") -> {:ok, -85} + String.contains?(oid, "48690.2.2.1.10.1") -> {:ok, -10} + String.contains?(oid, "48690.2.2.1.9.1") -> {:ok, 15} + String.contains?(oid, "48690.2.2.1.3.1") -> {:ok, "LTE"} + String.contains?(oid, "48690.2.2.1.2.1") -> {:ok, "inserted"} + String.contains?(oid, "48690.2.2.1.4.1") -> {:ok, "T-Mobile"} + String.contains?(oid, "48690.1.7.0") -> {:ok, 450} + true -> {:error, :no_such_object} + end + end) + + sensors = Teltonika.discover_wireless_sensors(@client_opts) + + assert is_list(sensors) + # Only 5 sensors: RSSI, RSRP, RSRQ, SINR, and Temperature (the numeric ones) + assert length(sensors) == 5 + end + + test "returns empty list when no sensors respond" do + expect(SnmpMock, :get, 8, fn _, _, _ -> + {:error, :no_such_object} + end) + + sensors = Teltonika.discover_wireless_sensors(@client_opts) + + assert sensors == [] + end + end +end