test improvements
This commit is contained in:
parent
49bc320a89
commit
fe7a44e5e2
17 changed files with 973 additions and 214 deletions
|
|
@ -190,28 +190,31 @@ defmodule Towerops.Devices do
|
|||
end
|
||||
|
||||
defp resolve_snmp_config(device) do
|
||||
cond do
|
||||
device.snmp_community != nil ->
|
||||
build_snmp_config(device.snmp_version, device.snmp_community, :device)
|
||||
# Resolve community and version independently with hierarchical fallback
|
||||
# This allows setting version at device level while inheriting community from org
|
||||
community =
|
||||
device.snmp_community ||
|
||||
device.site.snmp_community ||
|
||||
device.site.organization.snmp_community
|
||||
|
||||
device.site.snmp_community != nil ->
|
||||
build_snmp_config(device.site.snmp_version, device.site.snmp_community, :site)
|
||||
version =
|
||||
device.snmp_version ||
|
||||
device.site.snmp_version ||
|
||||
device.site.organization.snmp_version ||
|
||||
"2c"
|
||||
|
||||
device.site.organization.snmp_community != nil ->
|
||||
build_snmp_config(
|
||||
device.site.organization.snmp_version,
|
||||
device.site.organization.snmp_community,
|
||||
:organization
|
||||
)
|
||||
# Determine source based on where community string came from (primary credential)
|
||||
# If no community anywhere, source is :default
|
||||
source =
|
||||
cond do
|
||||
device.snmp_community != nil -> :device
|
||||
device.site.snmp_community != nil -> :site
|
||||
device.site.organization.snmp_community != nil -> :organization
|
||||
true -> :default
|
||||
end
|
||||
|
||||
true ->
|
||||
%{version: "2c", community: nil, source: :default}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_snmp_config(version, community, source) do
|
||||
%{
|
||||
version: version || "2c",
|
||||
version: version,
|
||||
community: community,
|
||||
source: source
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ defmodule Towerops.Snmp.Device do
|
|||
field :model, :string
|
||||
field :firmware_version, :string
|
||||
field :serial_number, :string
|
||||
field :raw_discovery_data, :map
|
||||
field :last_discovery_at, :utc_datetime
|
||||
|
||||
belongs_to :device, DeviceSchema
|
||||
|
||||
|
|
@ -48,6 +50,8 @@ defmodule Towerops.Snmp.Device do
|
|||
model: String.t() | nil,
|
||||
firmware_version: String.t() | nil,
|
||||
serial_number: String.t() | nil,
|
||||
raw_discovery_data: map() | nil,
|
||||
last_discovery_at: DateTime.t() | nil,
|
||||
device_id: Ecto.UUID.t(),
|
||||
device: NotLoaded.t() | Devices.t(),
|
||||
interfaces: NotLoaded.t() | [Interface.t()],
|
||||
|
|
@ -70,7 +74,9 @@ defmodule Towerops.Snmp.Device do
|
|||
:manufacturer,
|
||||
:model,
|
||||
:firmware_version,
|
||||
:serial_number
|
||||
:serial_number,
|
||||
:raw_discovery_data,
|
||||
:last_discovery_at
|
||||
])
|
||||
|> validate_required([:device_id])
|
||||
|> unique_constraint(:device_id)
|
||||
|
|
|
|||
|
|
@ -117,6 +117,13 @@ defmodule Towerops.Snmp.Discovery do
|
|||
_ = update_device_discovery_time(device)
|
||||
Logger.info("SNMP discovery completed successfully for: #{device.name}")
|
||||
|
||||
# Determine if this was first discovery or rediscovery
|
||||
# Check if device existed before (had last_discovery_at set)
|
||||
is_rediscovery = device.last_discovery_at != nil
|
||||
|
||||
# Log discovery event
|
||||
log_discovery_event(device, discovered_device, is_rediscovery)
|
||||
|
||||
# Broadcast discovery completion for real-time updates
|
||||
_ =
|
||||
Phoenix.PubSub.broadcast(
|
||||
|
|
@ -236,7 +243,8 @@ defmodule Towerops.Snmp.Discovery do
|
|||
Cisco
|
||||
|
||||
# Ubiquiti devices (check before generic Linux since they run Linux)
|
||||
String.contains?(sys_object_id, "41112") or
|
||||
# OID 10002 = old Ubiquiti/UniFi devices, 41112 = newer airOS devices
|
||||
String.contains?(sys_object_id, ["10002", "41112"]) or
|
||||
String.contains?(sys_descr, ["airOS", "AirFiber", "airMAX", "Ubiquiti"]) ->
|
||||
Logger.debug("Selected Ubiquiti profile")
|
||||
Ubiquiti
|
||||
|
|
@ -263,14 +271,53 @@ defmodule Towerops.Snmp.Discovery do
|
|||
{:ok, system_info}
|
||||
end
|
||||
|
||||
defp build_device_info(_client_opts, system_info, profile) when is_atom(profile) do
|
||||
defp build_device_info(client_opts, system_info, profile) when is_atom(profile) do
|
||||
Logger.info(
|
||||
"build_device_info: system_info has _raw_discovery_data: #{inspect(Map.has_key?(system_info, :_raw_discovery_data))}"
|
||||
)
|
||||
|
||||
# Get profile-specific system info if profile has discover_system_info/1
|
||||
enriched_system_info =
|
||||
if function_exported?(profile, :discover_system_info, 1) do
|
||||
case profile.discover_system_info(client_opts) do
|
||||
{:ok, profile_info} ->
|
||||
Logger.info("Enriched system_info with profile-specific data")
|
||||
Map.merge(system_info, profile_info)
|
||||
|
||||
{:error, _reason} ->
|
||||
Logger.warning("Failed to get profile-specific system info, using base info")
|
||||
system_info
|
||||
end
|
||||
else
|
||||
system_info
|
||||
end
|
||||
|
||||
# Let the hard-coded profile identify the device
|
||||
identified_info = profile.identify_device(system_info)
|
||||
{:ok, identified_info}
|
||||
identified_info = profile.identify_device(enriched_system_info)
|
||||
|
||||
# Collect raw debug data if profile supports it
|
||||
identified_info_with_debug =
|
||||
if function_exported?(profile, :collect_raw_debug_data, 1) do
|
||||
raw_data = profile.collect_raw_debug_data(client_opts)
|
||||
Logger.info("Collected raw debug data: #{map_size(raw_data)} keys")
|
||||
|
||||
identified_info
|
||||
|> Map.put(:_raw_discovery_data, raw_data)
|
||||
|> Map.put(:_last_discovery_at, DateTime.utc_now())
|
||||
else
|
||||
identified_info
|
||||
end
|
||||
|
||||
Logger.info(
|
||||
"build_device_info: identified_info has _raw_discovery_data: #{inspect(Map.has_key?(identified_info_with_debug, :_raw_discovery_data))}"
|
||||
)
|
||||
|
||||
{:ok, identified_info_with_debug}
|
||||
rescue
|
||||
_error ->
|
||||
Logger.error("Failed to identify device")
|
||||
{:ok, system_info}
|
||||
Logger.error("Failed to identify device with profile, falling back to Base profile")
|
||||
# Fall back to Base profile identification to ensure manufacturer/model are set
|
||||
{:ok, Base.identify_device(system_info)}
|
||||
end
|
||||
|
||||
@spec discover_interfaces(Client.connection_opts(), profile()) ::
|
||||
|
|
@ -342,15 +389,34 @@ defmodule Towerops.Snmp.Discovery do
|
|||
|
||||
@spec upsert_device(DeviceSchema.t(), device_info()) :: Device.t()
|
||||
defp upsert_device(device, device_info) do
|
||||
Logger.info("upsert_device called with device_info keys: #{inspect(Map.keys(device_info))}")
|
||||
|
||||
# Extract raw discovery data if present (using underscore prefix to distinguish from regular fields)
|
||||
raw_discovery_data = Map.get(device_info, :_raw_discovery_data)
|
||||
last_discovery_at = Map.get(device_info, :_last_discovery_at, DateTime.utc_now())
|
||||
|
||||
Logger.info("Extracted raw_discovery_data: #{inspect(raw_discovery_data != nil)}")
|
||||
|
||||
# Remove internal keys before saving
|
||||
device_attrs =
|
||||
device_info
|
||||
|> Map.delete(:_raw_discovery_data)
|
||||
|> Map.delete(:_last_discovery_at)
|
||||
|> Map.put(:device_id, device.id)
|
||||
|> Map.put(:raw_discovery_data, raw_discovery_data)
|
||||
|> Map.put(:last_discovery_at, last_discovery_at)
|
||||
|
||||
Logger.info("device_attrs has raw_discovery_data: #{inspect(Map.has_key?(device_attrs, :raw_discovery_data))}")
|
||||
|
||||
case Repo.get_by(Device, device_id: device.id) do
|
||||
nil ->
|
||||
%Device{}
|
||||
|> Device.changeset(Map.put(device_info, :device_id, device.id))
|
||||
|> Device.changeset(device_attrs)
|
||||
|> Repo.insert!()
|
||||
|
||||
existing_device ->
|
||||
existing_device
|
||||
|> Device.changeset(device_info)
|
||||
|> Device.changeset(device_attrs)
|
||||
|> Repo.update!()
|
||||
end
|
||||
end
|
||||
|
|
@ -427,4 +493,44 @@ defmodule Towerops.Snmp.Discovery do
|
|||
{:ok, device}
|
||||
end
|
||||
end
|
||||
|
||||
defp log_discovery_event(device, discovered_device, is_rediscovery) do
|
||||
event_type = if is_rediscovery, do: "device_rediscovered", else: "device_discovered"
|
||||
|
||||
message =
|
||||
if is_rediscovery do
|
||||
"Device rediscovered: #{device.name} (#{discovered_device.manufacturer} #{discovered_device.model})"
|
||||
else
|
||||
"Device discovered: #{device.name} (#{discovered_device.manufacturer} #{discovered_device.model})"
|
||||
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 || []),
|
||||
sensor_count:
|
||||
Repo.aggregate(
|
||||
from(s in Sensor, where: s.snmp_device_id == ^discovered_device.id),
|
||||
:count
|
||||
)
|
||||
}
|
||||
|
||||
event_attrs = %{
|
||||
device_id: device.id,
|
||||
event_type: event_type,
|
||||
severity: "info",
|
||||
message: message,
|
||||
metadata: metadata,
|
||||
occurred_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
# Publish event for EventLogger to persist
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"device:events",
|
||||
{:device_event, event_attrs}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -58,7 +58,9 @@ defmodule Towerops.Snmp.MibTranslator do
|
|||
defp translate_mib_name(mib_name) do
|
||||
# Get MIB directories from config or use default
|
||||
mib_dirs = Application.get_env(:towerops, :mib_dirs, ["/app/mibs", "/usr/share/snmp/mibs"])
|
||||
mib_dir_args = Enum.flat_map(mib_dirs, fn dir -> ["-M", "+#{dir}"] end)
|
||||
# snmptranslate requires colon-separated paths with + prefix for each directory
|
||||
mib_path = Enum.map_join(mib_dirs, ":", fn dir -> "+#{dir}" end)
|
||||
mib_dir_args = if mib_path == "", do: [], else: ["-M", mib_path]
|
||||
|
||||
# Use snmptranslate to convert MIB name to numeric OID
|
||||
# -On: Output numeric OID
|
||||
|
|
|
|||
|
|
@ -335,4 +335,50 @@ defmodule Towerops.Snmp.Profiles.Base do
|
|||
defp parse_sensor_status(2), do: "unavailable"
|
||||
defp parse_sensor_status(3), do: "nonoperational"
|
||||
defp parse_sensor_status(_), do: "unknown"
|
||||
|
||||
@doc """
|
||||
Collects raw debug data for troubleshooting.
|
||||
Includes system OIDs and interface/sensor tables.
|
||||
"""
|
||||
def collect_raw_debug_data(client_opts) do
|
||||
# Collect system OID values
|
||||
system_oids =
|
||||
Map.new(@system_oids, fn {key, oid} ->
|
||||
value =
|
||||
case Client.get(client_opts, oid) do
|
||||
{:ok, val} -> val
|
||||
{:error, reason} -> "ERROR: #{inspect(reason)}"
|
||||
end
|
||||
|
||||
{key, %{oid: oid, value: value}}
|
||||
end)
|
||||
|
||||
# Walk interface tables
|
||||
if_table =
|
||||
case Client.walk(client_opts, "1.3.6.1.2.1.2.2") do
|
||||
{:ok, results} -> results
|
||||
{:error, _} -> %{}
|
||||
end
|
||||
|
||||
ifx_table =
|
||||
case Client.walk(client_opts, "1.3.6.1.2.1.31.1.1") do
|
||||
{:ok, results} -> results
|
||||
{:error, _} -> %{}
|
||||
end
|
||||
|
||||
# Walk entity sensor table
|
||||
entity_sensor_table =
|
||||
case Client.walk(client_opts, "1.3.6.1.2.1.99.1.1") do
|
||||
{:ok, results} -> results
|
||||
{:error, _} -> %{}
|
||||
end
|
||||
|
||||
%{
|
||||
timestamp: DateTime.utc_now(),
|
||||
system_oids: system_oids,
|
||||
if_table: if_table,
|
||||
ifx_table: ifx_table,
|
||||
entity_sensor_table: entity_sensor_table
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -30,12 +30,14 @@ defmodule Towerops.Snmp.Profiles.Cisco do
|
|||
}
|
||||
|
||||
@doc """
|
||||
Discovers system information using Base profile.
|
||||
Discovers system information.
|
||||
Cisco profile doesn't query additional system OIDs beyond Base profile.
|
||||
"""
|
||||
@spec discover_system_info(Client.connection_opts()) ::
|
||||
{:ok, Discovery.system_info()} | {:error, term()}
|
||||
def discover_system_info(client_opts) do
|
||||
Base.discover_system_info(client_opts)
|
||||
def discover_system_info(_client_opts) do
|
||||
# No additional system info beyond Base profile
|
||||
{:ok, %{}}
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -244,4 +246,31 @@ defmodule Towerops.Snmp.Profiles.Cisco do
|
|||
defp parse_cisco_sensor_status(2), do: "unavailable"
|
||||
defp parse_cisco_sensor_status(3), do: "nonoperational"
|
||||
defp parse_cisco_sensor_status(_), do: "unknown"
|
||||
|
||||
@doc """
|
||||
Collects raw debug data for troubleshooting.
|
||||
Includes Cisco-specific sensor OIDs and entity tables.
|
||||
"""
|
||||
def collect_raw_debug_data(client_opts) do
|
||||
# Walk Cisco entity sensor table
|
||||
cisco_sensor_table =
|
||||
case Client.walk(client_opts, "1.3.6.1.4.1.9.9.91.1.1.1") do
|
||||
{:ok, results} -> results
|
||||
{:error, _} -> %{}
|
||||
end
|
||||
|
||||
# Walk entity physical table for descriptions
|
||||
entity_phys_table =
|
||||
case Client.walk(client_opts, "1.3.6.1.2.1.47.1.1.1") do
|
||||
{:ok, results} -> results
|
||||
{:error, _} -> %{}
|
||||
end
|
||||
|
||||
%{
|
||||
timestamp: DateTime.utc_now(),
|
||||
cisco_sensor_oids: @cisco_sensor_oids,
|
||||
cisco_sensor_table: cisco_sensor_table,
|
||||
entity_phys_table: entity_phys_table
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -60,10 +60,18 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do
|
|||
total_memory: "1.3.6.1.2.1.25.2.2.0",
|
||||
used_memory: "1.3.6.1.4.1.14988.1.1.1.2.0",
|
||||
total_hdd: "1.3.6.1.4.1.14988.1.1.1.6.0",
|
||||
used_hdd: "1.3.6.1.4.1.14988.1.1.1.7.0"
|
||||
used_hdd: "1.3.6.1.4.1.14988.1.1.1.7.0",
|
||||
|
||||
# Network Services
|
||||
dhcp_lease_count: "1.3.6.1.4.1.14988.1.1.1.3.0",
|
||||
|
||||
# Connection Tracking (mtxrFWConnection)
|
||||
fw_connection_count: "1.3.6.1.4.1.14988.1.1.13.1.1.1.0",
|
||||
fw_connection_max: "1.3.6.1.4.1.14988.1.1.13.1.1.2.0"
|
||||
}
|
||||
|
||||
# Table OID bases for walking
|
||||
@health_sensor_table "1.3.6.1.4.1.14988.1.1.3.100.1"
|
||||
@poe_table "1.3.6.1.4.1.14988.1.1.15.1"
|
||||
@optical_table "1.3.6.1.4.1.14988.1.1.16.1"
|
||||
|
||||
|
|
@ -129,17 +137,20 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do
|
|||
_ -> "MikroTik RouterOS"
|
||||
end
|
||||
|
||||
# Build formatted firmware version like LibreNMS: "Mikrotik RouterOS 7.20.6 (Level 6)"
|
||||
# Build formatted firmware version
|
||||
firmware_version = Map.get(system_info, :firmware_version)
|
||||
license_level = Map.get(system_info, :license_version)
|
||||
|
||||
# Treat empty string as nil for license level
|
||||
license_level = if license_level == "", do: nil, else: license_level
|
||||
|
||||
formatted_firmware =
|
||||
cond do
|
||||
firmware_version && license_level ->
|
||||
"Mikrotik RouterOS #{firmware_version} (Level #{license_level})"
|
||||
"RouterOS #{firmware_version} (Level #{license_level})"
|
||||
|
||||
firmware_version ->
|
||||
"Mikrotik RouterOS #{firmware_version}"
|
||||
"RouterOS #{firmware_version}"
|
||||
|
||||
true ->
|
||||
# Fallback to extracting from sysDescr
|
||||
|
|
@ -206,13 +217,92 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do
|
|||
license_version: license_level
|
||||
}}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, reason} ->
|
||||
# MikroTik-specific OIDs not available or partially available
|
||||
Logger.warning("Failed to get MikroTik device info: #{inspect(reason)}")
|
||||
{:ok, %{}}
|
||||
end
|
||||
end
|
||||
|
||||
defp discover_health_sensors(client_opts) do
|
||||
# First try the dynamic health sensor table (available on newer devices like CCR2004)
|
||||
table_sensors = discover_health_table_sensors(client_opts)
|
||||
|
||||
# Also try individual health OIDs for older devices
|
||||
oid_sensors = discover_health_oid_sensors(client_opts)
|
||||
|
||||
# Combine both, preferring table sensors (more accurate)
|
||||
table_sensors ++ oid_sensors
|
||||
end
|
||||
|
||||
defp discover_health_table_sensors(client_opts) do
|
||||
# Walk health sensor table: mtxrHealthTable (1.3.6.1.4.1.14988.1.1.3.100.1)
|
||||
# Column 2: Name, Column 3: Value, Column 4: Type
|
||||
with {:ok, name_results} <- Client.walk(client_opts, "#{@health_sensor_table}.2"),
|
||||
{:ok, value_results} <- Client.walk(client_opts, "#{@health_sensor_table}.3"),
|
||||
{:ok, type_results} <- Client.walk(client_opts, "#{@health_sensor_table}.4") do
|
||||
indices =
|
||||
name_results
|
||||
|> Map.keys()
|
||||
|> Enum.map(fn oid -> oid |> String.split(".") |> List.last() end)
|
||||
|
||||
indices
|
||||
|> Enum.map(fn index ->
|
||||
name_oid = "#{@health_sensor_table}.2.#{index}"
|
||||
value_oid = "#{@health_sensor_table}.3.#{index}"
|
||||
type_oid = "#{@health_sensor_table}.4.#{index}"
|
||||
|
||||
name = Map.get(name_results, name_oid, "unknown")
|
||||
value = Map.get(value_results, value_oid, 0)
|
||||
type_code = Map.get(type_results, type_oid, 0)
|
||||
|
||||
{sensor_type, unit, divisor} = decode_health_sensor_type(type_code, name)
|
||||
|
||||
# Skip sensors with value 0 (disabled PSUs, etc.)
|
||||
if value > 0 do
|
||||
%{
|
||||
sensor_type: sensor_type,
|
||||
sensor_index: "health_#{index}",
|
||||
sensor_oid: value_oid,
|
||||
sensor_descr: format_health_sensor_name(name),
|
||||
sensor_unit: unit,
|
||||
sensor_divisor: divisor,
|
||||
last_value: value / divisor,
|
||||
status: "ok"
|
||||
}
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
else
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp decode_health_sensor_type(type_code, name) do
|
||||
cond do
|
||||
# Type 1 = Temperature (Celsius)
|
||||
type_code == 1 -> {"temperature", "°C", 1}
|
||||
# Type 3 = Voltage (V, value in decivolts)
|
||||
type_code == 3 -> {"voltage", "V", 10}
|
||||
# Type 2 = Current (mA)
|
||||
type_code == 2 -> {"current", "mA", 1}
|
||||
# Fallback: infer from name
|
||||
String.contains?(name, "temperature") -> {"temperature", "°C", 1}
|
||||
String.contains?(name, "voltage") -> {"voltage", "V", 10}
|
||||
String.contains?(name, "current") -> {"current", "mA", 1}
|
||||
String.contains?(name, "power") -> {"power", "W", 10}
|
||||
true -> {"unknown", "", 1}
|
||||
end
|
||||
end
|
||||
|
||||
defp format_health_sensor_name(name) do
|
||||
name
|
||||
|> String.replace("-", " ")
|
||||
|> String.split()
|
||||
|> Enum.map_join(" ", &String.capitalize/1)
|
||||
end
|
||||
|
||||
defp discover_health_oid_sensors(client_opts) do
|
||||
# Try to get all health OIDs - voltages, temperatures, power, fans
|
||||
health_oids = [
|
||||
# Voltages
|
||||
|
|
@ -262,10 +352,11 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do
|
|||
defp discover_resource_sensors(client_opts) do
|
||||
cpu_sensors = discover_cpu_sensors(client_opts)
|
||||
storage_sensors = discover_storage_sensors(client_opts)
|
||||
network_sensors = discover_network_sensors(client_opts)
|
||||
poe_sensors = discover_poe_sensors(client_opts)
|
||||
optical_sensors = discover_optical_sensors(client_opts)
|
||||
|
||||
cpu_sensors ++ storage_sensors ++ poe_sensors ++ optical_sensors
|
||||
cpu_sensors ++ storage_sensors ++ network_sensors ++ poe_sensors ++ optical_sensors
|
||||
end
|
||||
|
||||
defp discover_cpu_sensors(client_opts) do
|
||||
|
|
@ -292,6 +383,35 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do
|
|||
end
|
||||
end
|
||||
|
||||
defp discover_network_sensors(client_opts) do
|
||||
# Get DHCP lease count and connection tracking stats
|
||||
network_oids = [
|
||||
{@mikrotik_oids.dhcp_lease_count, "dhcp_leases", "count", 1},
|
||||
{@mikrotik_oids.fw_connection_count, "connection_count", "count", 1}
|
||||
]
|
||||
|
||||
network_oids
|
||||
|> Enum.map(fn {oid, type, unit, divisor} ->
|
||||
case Client.get(client_opts, oid) do
|
||||
{:ok, value} when is_integer(value) and value >= 0 ->
|
||||
%{
|
||||
sensor_type: type,
|
||||
sensor_index: type,
|
||||
sensor_oid: oid,
|
||||
sensor_descr: format_network_sensor_name(type),
|
||||
sensor_unit: unit,
|
||||
sensor_divisor: divisor,
|
||||
last_value: value / divisor,
|
||||
status: "ok"
|
||||
}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
defp discover_storage_sensors(client_opts) do
|
||||
with {:ok, descr_results} <- Client.walk(client_opts, "1.3.6.1.2.1.25.2.3.1.3"),
|
||||
{:ok, size_results} <- Client.walk(client_opts, "1.3.6.1.2.1.25.2.3.1.5"),
|
||||
|
|
@ -544,6 +664,10 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do
|
|||
defp format_sensor_name("processor_frequency"), do: "Processor Frequency"
|
||||
defp format_sensor_name(other), do: String.capitalize(other)
|
||||
|
||||
defp format_network_sensor_name("dhcp_leases"), do: "DHCP Leases"
|
||||
defp format_network_sensor_name("connection_count"), do: "Active Connections"
|
||||
defp format_network_sensor_name(other), do: String.capitalize(other)
|
||||
|
||||
defp cpu_status(load) when load < 70, do: "ok"
|
||||
defp cpu_status(load) when load < 90, do: "warning"
|
||||
defp cpu_status(_), do: "critical"
|
||||
|
|
@ -568,4 +692,34 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do
|
|||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Collects raw SNMP OID data for debugging purposes.
|
||||
"""
|
||||
def collect_raw_debug_data(client_opts) do
|
||||
# Collect raw SNMP data for debugging
|
||||
system_oids =
|
||||
Map.new(@mikrotik_oids, fn {key, oid} ->
|
||||
value =
|
||||
case Client.get(client_opts, oid) do
|
||||
{:ok, val} -> val
|
||||
{:error, reason} -> "ERROR: #{inspect(reason)}"
|
||||
end
|
||||
|
||||
{key, %{oid: oid, value: value}}
|
||||
end)
|
||||
|
||||
# Walk health sensor table
|
||||
health_table =
|
||||
case Client.walk(client_opts, @health_sensor_table) do
|
||||
{:ok, results} -> results
|
||||
{:error, _} -> %{}
|
||||
end
|
||||
|
||||
%{
|
||||
timestamp: DateTime.utc_now(),
|
||||
system_oids: system_oids,
|
||||
health_sensor_table: health_table
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -38,12 +38,14 @@ defmodule Towerops.Snmp.Profiles.NetSnmp do
|
|||
}
|
||||
|
||||
@doc """
|
||||
Discovers system information using Base profile.
|
||||
Discovers system information.
|
||||
NetSnmp profile doesn't query additional system OIDs beyond Base profile.
|
||||
"""
|
||||
@spec discover_system_info(Client.connection_opts()) ::
|
||||
{:ok, Discovery.system_info()} | {:error, term()}
|
||||
def discover_system_info(client_opts) do
|
||||
Base.discover_system_info(client_opts)
|
||||
def discover_system_info(_client_opts) do
|
||||
# No additional system info beyond Base profile
|
||||
{:ok, %{}}
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -313,4 +315,53 @@ defmodule Towerops.Snmp.Profiles.NetSnmp do
|
|||
defp parse_float(value) when is_integer(value), do: value / 1.0
|
||||
defp parse_float(value) when is_binary(value), do: String.to_float(value)
|
||||
defp parse_float(_), do: nil
|
||||
|
||||
@doc """
|
||||
Collects raw debug data for troubleshooting.
|
||||
Includes LM-SENSORS and UCD-SNMP tables.
|
||||
"""
|
||||
def collect_raw_debug_data(client_opts) do
|
||||
# Walk LM-SENSORS tables
|
||||
lm_temp_table =
|
||||
case Client.walk(client_opts, "1.3.6.1.4.1.2021.13.16.2") do
|
||||
{:ok, results} -> results
|
||||
{:error, _} -> %{}
|
||||
end
|
||||
|
||||
lm_fan_table =
|
||||
case Client.walk(client_opts, "1.3.6.1.4.1.2021.13.16.3") do
|
||||
{:ok, results} -> results
|
||||
{:error, _} -> %{}
|
||||
end
|
||||
|
||||
lm_voltage_table =
|
||||
case Client.walk(client_opts, "1.3.6.1.4.1.2021.13.16.4") do
|
||||
{:ok, results} -> results
|
||||
{:error, _} -> %{}
|
||||
end
|
||||
|
||||
# Walk UCD-SNMP tables
|
||||
ucd_load_table =
|
||||
case Client.walk(client_opts, "1.3.6.1.4.1.2021.10") do
|
||||
{:ok, results} -> results
|
||||
{:error, _} -> %{}
|
||||
end
|
||||
|
||||
ucd_memory_table =
|
||||
case Client.walk(client_opts, "1.3.6.1.4.1.2021.4") do
|
||||
{:ok, results} -> results
|
||||
{:error, _} -> %{}
|
||||
end
|
||||
|
||||
%{
|
||||
timestamp: DateTime.utc_now(),
|
||||
lm_sensors_oids: @lm_sensors_oids,
|
||||
ucd_snmp_oids: @ucd_snmp_oids,
|
||||
lm_temp_table: lm_temp_table,
|
||||
lm_fan_table: lm_fan_table,
|
||||
lm_voltage_table: lm_voltage_table,
|
||||
ucd_load_table: ucd_load_table,
|
||||
ucd_memory_table: ucd_memory_table
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ defmodule Towerops.Snmp.Profiles.Ubiquiti do
|
|||
# Ubiquiti enterprise MIB base: 1.3.6.1.4.1.41112
|
||||
@ubnt_base "1.3.6.1.4.1.41112"
|
||||
|
||||
# UniFi specific OIDs (.1.6.3)
|
||||
@unifi_system_model "#{@ubnt_base}.1.6.3.3.0"
|
||||
@unifi_system_version "#{@ubnt_base}.1.6.3.6.0"
|
||||
|
||||
# Radio configuration OIDs (.1.3.1.1)
|
||||
@radio_config_base "#{@ubnt_base}.1.3.1.1"
|
||||
@radio_tx_freq "#{@radio_config_base}.5"
|
||||
|
|
@ -36,10 +40,27 @@ defmodule Towerops.Snmp.Profiles.Ubiquiti do
|
|||
@radio_remote_rx_power "#{@radio_stats_base}.14"
|
||||
|
||||
@doc """
|
||||
Discovers system information using base profile.
|
||||
Discovers system information, extending Base with UniFi-specific OIDs.
|
||||
"""
|
||||
def discover_system_info(client_opts) do
|
||||
Base.discover_system_info(client_opts)
|
||||
with {:ok, base_info} <- Base.discover_system_info(client_opts) do
|
||||
# Try to get UniFi-specific model and version
|
||||
unifi_info = discover_unifi_system_info(client_opts)
|
||||
{:ok, Map.merge(base_info, unifi_info)}
|
||||
end
|
||||
end
|
||||
|
||||
defp discover_unifi_system_info(client_opts) do
|
||||
case Client.get_multiple(client_opts, [@unifi_system_model, @unifi_system_version]) do
|
||||
{:ok, [model, version]} when is_binary(model) or is_binary(version) ->
|
||||
%{
|
||||
unifi_model: model,
|
||||
unifi_version: version
|
||||
}
|
||||
|
||||
_ ->
|
||||
%{}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -70,21 +91,93 @@ defmodule Towerops.Snmp.Profiles.Ubiquiti do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Identifies Ubiquiti device from sysDescr.
|
||||
Identifies Ubiquiti device from sysDescr and UniFi-specific OIDs.
|
||||
"""
|
||||
def identify_device(system_info) do
|
||||
sys_descr = Map.get(system_info, :sys_descr, "")
|
||||
unifi_model = Map.get(system_info, :unifi_model)
|
||||
unifi_version = Map.get(system_info, :unifi_version)
|
||||
|
||||
# Extract airOS version if present
|
||||
firmware = extract_airos_version(sys_descr)
|
||||
# Prefer UniFi-specific model/version if available, otherwise detect from sysDescr
|
||||
{model, firmware} =
|
||||
if unifi_model || unifi_version do
|
||||
{unifi_model || "UniFi Device", unifi_version}
|
||||
else
|
||||
{detect_model(sys_descr), extract_airos_version(sys_descr)}
|
||||
end
|
||||
|
||||
Map.merge(system_info, %{
|
||||
manufacturer: "Ubiquiti",
|
||||
model: detect_model(sys_descr),
|
||||
model: model,
|
||||
firmware_version: firmware
|
||||
})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Collects raw debug data for troubleshooting.
|
||||
Includes UniFi system OIDs, radio configuration and statistics.
|
||||
"""
|
||||
def collect_raw_debug_data(client_opts) do
|
||||
# UniFi system OIDs
|
||||
unifi_system_oids = %{
|
||||
unifi_model: @unifi_system_model,
|
||||
unifi_version: @unifi_system_version
|
||||
}
|
||||
|
||||
# Radio configuration OIDs
|
||||
radio_config_oids = %{
|
||||
radio_tx_freq: @radio_tx_freq,
|
||||
radio_rx_freq: @radio_rx_freq,
|
||||
radio_tx_power: @radio_tx_power
|
||||
}
|
||||
|
||||
# Radio statistics OIDs
|
||||
radio_stats_oids = %{
|
||||
radio_tx_throughput: @radio_tx_throughput,
|
||||
radio_rx_throughput: @radio_rx_throughput,
|
||||
radio_tx_capacity: @radio_tx_capacity_bps,
|
||||
radio_rx_capacity: @radio_rx_capacity_bps,
|
||||
radio_tx_signal: @radio_tx_signal,
|
||||
radio_rx_signal: @radio_rx_signal,
|
||||
radio_remote_tx_power: @radio_remote_tx_power,
|
||||
radio_remote_rx_power: @radio_remote_rx_power
|
||||
}
|
||||
|
||||
# Get UniFi system values
|
||||
unifi_system_values =
|
||||
Map.new(unifi_system_oids, fn {key, oid} ->
|
||||
value =
|
||||
case Client.get(client_opts, oid) do
|
||||
{:ok, val} -> val
|
||||
{:error, reason} -> "ERROR: #{inspect(reason)}"
|
||||
end
|
||||
|
||||
{key, %{oid: oid, value: value}}
|
||||
end)
|
||||
|
||||
# Walk radio config and stats tables
|
||||
radio_config_table =
|
||||
case Client.walk(client_opts, @radio_config_base) do
|
||||
{:ok, results} -> results
|
||||
{:error, _} -> %{}
|
||||
end
|
||||
|
||||
radio_stats_table =
|
||||
case Client.walk(client_opts, @radio_stats_base) do
|
||||
{:ok, results} -> results
|
||||
{:error, _} -> %{}
|
||||
end
|
||||
|
||||
%{
|
||||
timestamp: DateTime.utc_now(),
|
||||
unifi_system_oids: unifi_system_values,
|
||||
radio_config_oids: radio_config_oids,
|
||||
radio_stats_oids: radio_stats_oids,
|
||||
radio_config_table: radio_config_table,
|
||||
radio_stats_table: radio_stats_table
|
||||
}
|
||||
end
|
||||
|
||||
# Private functions
|
||||
|
||||
defp discover_radio_sensors(client_opts) do
|
||||
|
|
|
|||
|
|
@ -97,6 +97,22 @@
|
|||
>
|
||||
Logs
|
||||
</.link>
|
||||
|
||||
<%= if @snmp_device && @snmp_device.raw_discovery_data do %>
|
||||
<.link
|
||||
patch={~p"/devices/#{@device.id}?tab=debug"}
|
||||
class={[
|
||||
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
|
||||
if @active_tab == "debug" do
|
||||
"border-blue-500 text-blue-600 dark:text-blue-400"
|
||||
else
|
||||
"border-transparent text-zinc-500 hover:text-zinc-700 hover:border-zinc-300 dark:text-zinc-400 dark:hover:text-zinc-300"
|
||||
end
|
||||
]}
|
||||
>
|
||||
Debug
|
||||
</.link>
|
||||
<% end %>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -746,6 +762,116 @@
|
|||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% "debug" -> %>
|
||||
<div class="bg-white dark:bg-zinc-800 shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-zinc-900 dark:text-zinc-100">
|
||||
Raw Discovery Data
|
||||
</h3>
|
||||
<div class="mt-2 max-w-xl text-sm text-zinc-500 dark:text-zinc-400">
|
||||
<p>
|
||||
Raw SNMP OID values collected during the last discovery run at
|
||||
<%= if @snmp_device.last_discovery_at do %>
|
||||
{Calendar.strftime(@snmp_device.last_discovery_at, "%Y-%m-%d %H:%M:%S UTC")}
|
||||
<% else %>
|
||||
unknown time
|
||||
<% end %>.
|
||||
</p>
|
||||
</div>
|
||||
<%= if @snmp_device.raw_discovery_data do %>
|
||||
<div class="mt-5">
|
||||
<h4 class="text-sm font-medium text-zinc-900 dark:text-zinc-100 mb-3">
|
||||
System OIDs
|
||||
</h4>
|
||||
<div class="overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-700">
|
||||
<table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-700">
|
||||
<thead class="bg-zinc-50 dark:bg-zinc-900">
|
||||
<tr>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
|
||||
Field
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
|
||||
OID
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
|
||||
Value
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-zinc-800 divide-y divide-zinc-200 dark:divide-zinc-700">
|
||||
<%= for {key, data} <- Map.get(@snmp_device.raw_discovery_data, "system_oids", %{}) do %>
|
||||
<tr>
|
||||
<td class="px-3 py-4 whitespace-nowrap text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{key}
|
||||
</td>
|
||||
<td class="px-3 py-4 whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400 font-mono">
|
||||
{data["oid"]}
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm text-zinc-900 dark:text-zinc-100">
|
||||
<code class="text-xs">{inspect(data["value"])}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<%= if Map.get(@snmp_device.raw_discovery_data, "health_sensor_table") do %>
|
||||
<h4 class="text-sm font-medium text-zinc-900 dark:text-zinc-100 mt-6 mb-3">
|
||||
Health Sensor Table Walk
|
||||
</h4>
|
||||
<div class="overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-700">
|
||||
<table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-700">
|
||||
<thead class="bg-zinc-50 dark:bg-zinc-900">
|
||||
<tr>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
|
||||
OID
|
||||
</th>
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
|
||||
Value
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-zinc-800 divide-y divide-zinc-200 dark:divide-zinc-700">
|
||||
<%= for {oid, value} <- Map.get(@snmp_device.raw_discovery_data, "health_sensor_table", %{}) do %>
|
||||
<tr>
|
||||
<td class="px-3 py-4 whitespace-nowrap text-sm font-mono text-zinc-500 dark:text-zinc-400">
|
||||
{oid}
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm text-zinc-900 dark:text-zinc-100">
|
||||
<code class="text-xs">{inspect(value)}</code>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<h4 class="text-sm font-medium text-zinc-900 dark:text-zinc-100 mt-6 mb-3">
|
||||
Copyable Debug Data
|
||||
</h4>
|
||||
<div class="rounded-lg border border-zinc-200 dark:border-zinc-700">
|
||||
<textarea
|
||||
readonly
|
||||
rows="10"
|
||||
class="w-full p-3 font-mono text-xs bg-zinc-50 dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 border-0 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
onclick="this.select()"
|
||||
><%= Jason.encode!(@snmp_device.raw_discovery_data, pretty: true) %></textarea>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-zinc-500 dark:text-zinc-400">
|
||||
Click the text box to select all data, then copy with Cmd+C or Ctrl+C
|
||||
</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="mt-5 p-4 bg-zinc-50 dark:bg-zinc-900 rounded-md">
|
||||
<p class="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
No raw discovery data available. Run discovery again to collect debug data.
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% _ -> %>
|
||||
<div class="text-center py-12">
|
||||
<p class="text-zinc-500 dark:text-zinc-400">Tab not found</p>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
defmodule Towerops.Repo.Migrations.AddMikrotikResourceSensors do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
# Disable the database MikroTik profile to use the hardcoded profile instead
|
||||
# The hardcoded profile has comprehensive sensor discovery including:
|
||||
# - CPU load (per core)
|
||||
# - Memory and storage (via HOST-RESOURCES-MIB walks)
|
||||
# - DHCP leases
|
||||
# - Connection tracking
|
||||
# - POE sensors
|
||||
# - Optical/SFP sensors
|
||||
#
|
||||
# The database profile system doesn't yet support table walks,
|
||||
# so we fall back to the hardcoded profile for full discovery.
|
||||
execute("UPDATE device_profiles SET enabled = false WHERE name = 'mikrotik'")
|
||||
end
|
||||
|
||||
def down do
|
||||
# Re-enable the database profile
|
||||
execute("UPDATE device_profiles SET enabled = true WHERE name = 'mikrotik'")
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
defmodule Towerops.Repo.Migrations.AddRawDiscoveryDataToSnmpDevices do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:snmp_devices) do
|
||||
add :raw_discovery_data, :map
|
||||
add :last_discovery_at, :utc_datetime
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -51,14 +51,10 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
snmp_port: 161
|
||||
})
|
||||
|
||||
# Mock successful connection test
|
||||
expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
|
||||
{:ok, {:timeticks, 12_345}}
|
||||
end)
|
||||
|
||||
# Mock system info discovery
|
||||
expect(SnmpMock, :get, 6, fn _, oid, _ ->
|
||||
# Mock all GET calls (connection test + system info + interfaces + health + network sensors)
|
||||
stub(SnmpMock, :get, fn _, oid, _ ->
|
||||
case oid do
|
||||
# Connection test + Base system info
|
||||
"1.3.6.1.2.1.1.1.0" ->
|
||||
{:ok, {:octet_string, "RouterOS RB750"}}
|
||||
|
||||
|
|
@ -77,77 +73,114 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
"1.3.6.1.2.1.1.6.0" ->
|
||||
{:ok, {:octet_string, "Data Center"}}
|
||||
|
||||
# MikroTik-specific system info
|
||||
"1.3.6.1.4.1.14988.1.1.7.3.0" ->
|
||||
{:ok, {:octet_string, "1234567890"}}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.7.4.0" ->
|
||||
{:ok, {:octet_string, "7.13.2"}}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.7.9.0" ->
|
||||
{:ok, {:octet_string, "RB750Gr3"}}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.7.8.0" ->
|
||||
{:ok, {:octet_string, "Main Router"}}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.7.5.0" ->
|
||||
{:ok, {:integer, 6}}
|
||||
|
||||
# Network sensors (MikroTik-specific)
|
||||
"1.3.6.1.4.1.14988.1.1.1.3.0" ->
|
||||
{:ok, {:integer, 10}}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.13.1.1.1.0" ->
|
||||
{:ok, {:integer, 500}}
|
||||
|
||||
# Catch-all for interface data and health sensors
|
||||
_ ->
|
||||
{:error, :no_such_object}
|
||||
cond do
|
||||
# Health sensor OIDs (all return error for this test)
|
||||
String.starts_with?(oid, "1.3.6.1.4.1.14988.1.1.3.") and
|
||||
not String.contains?(oid, "1.3.6.1.4.1.14988.1.1.13") ->
|
||||
{:error, :no_such_object}
|
||||
|
||||
# Interface 1 data
|
||||
String.ends_with?(oid, ".1") ->
|
||||
parts = String.split(oid, ".")
|
||||
second_to_last = Enum.at(parts, -2)
|
||||
|
||||
cond do
|
||||
second_to_last == "2" -> {:ok, {:octet_string, "ether1"}}
|
||||
second_to_last == "3" -> {:ok, {:integer, 6}}
|
||||
second_to_last == "5" -> {:ok, {:gauge32, 1_000_000_000}}
|
||||
second_to_last == "6" -> {:ok, {:octet_string, <<0x00, 0x11, 0x22, 0x33, 0x44, 0x55>>}}
|
||||
second_to_last == "7" -> {:ok, {:integer, 1}}
|
||||
second_to_last == "8" -> {:ok, {:integer, 1}}
|
||||
second_to_last == "1" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "ether1"}}
|
||||
second_to_last == "18" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "WAN"}}
|
||||
true -> {:error, :no_such_object}
|
||||
end
|
||||
|
||||
# Interface 2 data
|
||||
String.ends_with?(oid, ".2") ->
|
||||
parts = String.split(oid, ".")
|
||||
second_to_last = Enum.at(parts, -2)
|
||||
|
||||
cond do
|
||||
second_to_last == "2" -> {:ok, {:octet_string, "ether2"}}
|
||||
second_to_last == "3" -> {:ok, {:integer, 6}}
|
||||
second_to_last == "5" -> {:ok, {:gauge32, 1_000_000_000}}
|
||||
second_to_last == "6" -> {:ok, {:octet_string, <<0x00, 0x11, 0x22, 0x33, 0x44, 0x56>>}}
|
||||
second_to_last == "7" -> {:ok, {:integer, 1}}
|
||||
second_to_last == "8" -> {:ok, {:integer, 1}}
|
||||
second_to_last == "1" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "ether2"}}
|
||||
second_to_last == "18" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "LAN"}}
|
||||
true -> {:error, :no_such_object}
|
||||
end
|
||||
|
||||
true ->
|
||||
{:error, :no_such_object}
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
# Mock interface discovery
|
||||
expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.2.2.1.1", _ ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.2.1.2.2.1.1.1", value: {:integer, 1}},
|
||||
%{oid: "1.3.6.1.2.1.2.2.1.1.2", value: {:integer, 2}}
|
||||
]}
|
||||
end)
|
||||
|
||||
# Mock interface data fetches for each interface
|
||||
expect(SnmpMock, :get, 16, fn _, oid, _ ->
|
||||
cond do
|
||||
String.ends_with?(oid, ".1") ->
|
||||
parts = String.split(oid, ".")
|
||||
second_to_last = Enum.at(parts, -2)
|
||||
|
||||
cond do
|
||||
second_to_last == "2" -> {:ok, {:octet_string, "ether1"}}
|
||||
second_to_last == "3" -> {:ok, {:integer, 6}}
|
||||
second_to_last == "5" -> {:ok, {:gauge32, 1_000_000_000}}
|
||||
second_to_last == "6" -> {:ok, {:octet_string, <<0x00, 0x11, 0x22, 0x33, 0x44, 0x55>>}}
|
||||
second_to_last == "7" -> {:ok, {:integer, 1}}
|
||||
second_to_last == "8" -> {:ok, {:integer, 1}}
|
||||
second_to_last == "1" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "ether1"}}
|
||||
second_to_last == "18" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "WAN"}}
|
||||
true -> {:error, :no_such_object}
|
||||
end
|
||||
|
||||
String.ends_with?(oid, ".2") ->
|
||||
parts = String.split(oid, ".")
|
||||
second_to_last = Enum.at(parts, -2)
|
||||
|
||||
cond do
|
||||
second_to_last == "2" -> {:ok, {:octet_string, "ether2"}}
|
||||
second_to_last == "3" -> {:ok, {:integer, 6}}
|
||||
second_to_last == "5" -> {:ok, {:gauge32, 1_000_000_000}}
|
||||
second_to_last == "6" -> {:ok, {:octet_string, <<0x00, 0x11, 0x22, 0x33, 0x44, 0x56>>}}
|
||||
second_to_last == "7" -> {:ok, {:integer, 1}}
|
||||
second_to_last == "8" -> {:ok, {:integer, 1}}
|
||||
second_to_last == "1" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "ether2"}}
|
||||
second_to_last == "18" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "LAN"}}
|
||||
true -> {:error, :no_such_object}
|
||||
end
|
||||
|
||||
true ->
|
||||
{:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
# Note: Dynamic profile would try to discover device OIDs and sensor OIDs from database,
|
||||
# but MibTranslator fails in tests (MIKROTIK-MIB not installed), so no get calls are made
|
||||
|
||||
# Mock sensor discovery - MikroTik profiles make many walk calls for sensors
|
||||
# CPU (1), storage (3), POE (4), optical (5), ENTITY-SENSOR-MIB (1) = 14 walks
|
||||
# Mock all walk calls
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
if String.starts_with?(oid, "1.3.6.1.2.1.99") do
|
||||
{:ok, []}
|
||||
else
|
||||
{:error, :no_such_object}
|
||||
cond do
|
||||
# Interface discovery - return 2 interfaces
|
||||
oid == "1.3.6.1.2.1.2.2.1.1" ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.2.1.2.2.1.1.1", value: {:integer, 1}},
|
||||
%{oid: "1.3.6.1.2.1.2.2.1.1.2", value: {:integer, 2}}
|
||||
]}
|
||||
|
||||
# ENTITY-SENSOR-MIB
|
||||
String.starts_with?(oid, "1.3.6.1.2.1.99") ->
|
||||
{:ok, []}
|
||||
|
||||
# LLDP neighbor discovery
|
||||
String.starts_with?(oid, "1.0.8802") ->
|
||||
{:ok, []}
|
||||
|
||||
# CDP neighbor discovery
|
||||
String.starts_with?(oid, "1.3.6.1.4.1.9.9.23") ->
|
||||
{:ok, []}
|
||||
|
||||
# MikroTik health sensor table
|
||||
String.starts_with?(oid, "1.3.6.1.4.1.14988.1.1.3.100") ->
|
||||
{:ok, []}
|
||||
|
||||
# Other walks (debug data collection, sensors, etc.)
|
||||
true ->
|
||||
{:ok, []}
|
||||
end
|
||||
end)
|
||||
|
||||
assert {:ok, device} = Discovery.discover_device(device)
|
||||
|
||||
assert device.manufacturer == "MikroTik"
|
||||
assert device.model == "RouterOS RB750"
|
||||
assert device.model == "RB750"
|
||||
assert device.sys_name == "router1"
|
||||
|
||||
# Verify interfaces were created
|
||||
|
|
@ -172,23 +205,18 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
snmp_port: 161
|
||||
})
|
||||
|
||||
# Mock successful connection test
|
||||
expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
|
||||
{:ok, {:timeticks, 54_321}}
|
||||
end)
|
||||
|
||||
# Mock system info discovery
|
||||
expect(SnmpMock, :get, 6, fn _, oid, _ ->
|
||||
# Mock all GET calls (connection test + system info + debug data)
|
||||
stub(SnmpMock, :get, fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.1.3.0" ->
|
||||
{:ok, {:timeticks, 54_321}}
|
||||
|
||||
"1.3.6.1.2.1.1.1.0" ->
|
||||
{:ok, {:octet_string, "Cisco IOS Software, C2960 Software"}}
|
||||
|
||||
"1.3.6.1.2.1.1.2.0" ->
|
||||
{:ok, {:object_identifier, [1, 3, 6, 1, 4, 1, 9, 1, 1208]}}
|
||||
|
||||
"1.3.6.1.2.1.1.3.0" ->
|
||||
{:ok, {:timeticks, 54_321}}
|
||||
|
||||
"1.3.6.1.2.1.1.4.0" ->
|
||||
{:ok, {:octet_string, "netadmin@cisco.com"}}
|
||||
|
||||
|
|
@ -203,10 +231,8 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
end
|
||||
end)
|
||||
|
||||
# Mock interface discovery - no interfaces for simplicity
|
||||
# Walk called 5 times: interfaces (1), cisco sensors (1), base sensors fallback (1),
|
||||
# LLDP neighbors (1), CDP neighbors (1)
|
||||
expect(SnmpMock, :walk, 5, fn _, _, _ ->
|
||||
# Mock all walk calls (debug data collection, interfaces, sensors, neighbors)
|
||||
stub(SnmpMock, :walk, fn _, _, _ ->
|
||||
{:ok, []}
|
||||
end)
|
||||
|
||||
|
|
@ -249,14 +275,13 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
snmp_port: 161
|
||||
})
|
||||
|
||||
# Mock successful connection test
|
||||
expect(SnmpMock, :get, fn _, "1.3.6.1.2.1.1.3.0", _ ->
|
||||
{:ok, {:timeticks, 999}}
|
||||
end)
|
||||
|
||||
# Mock system info discovery
|
||||
expect(SnmpMock, :get, 6, fn _, oid, _ ->
|
||||
# Mock all GET calls (connection + system info + debug data)
|
||||
# Connection: 1, System info: 6, Debug data: 6 = 13 total
|
||||
stub(SnmpMock, :get, fn _, oid, _ ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.1.3.0" ->
|
||||
{:ok, {:timeticks, 999}}
|
||||
|
||||
"1.3.6.1.2.1.1.1.0" ->
|
||||
{:ok, {:octet_string, "Generic Device"}}
|
||||
|
||||
|
|
@ -265,23 +290,31 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
end
|
||||
end)
|
||||
|
||||
# Mock failed interface discovery
|
||||
expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.2.2.1.1", _ ->
|
||||
{:error, :timeout}
|
||||
end)
|
||||
|
||||
# Mock failed sensor discovery and neighbor discovery (LLDP + CDP)
|
||||
expect(SnmpMock, :walk, 3, fn _, oid, _ ->
|
||||
# Mock all walk calls in execution order:
|
||||
# 1. Debug data collection happens first (inside build_device_info)
|
||||
# 2. Then interface discovery
|
||||
# 3. Then sensor discovery
|
||||
# 4. Then neighbor discovery (LLDP + CDP)
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
cond do
|
||||
# Entity sensor queries (sensor discovery + debug entity_sensor_table)
|
||||
String.starts_with?(oid, "1.3.6.1.2.1.99") ->
|
||||
{:error, :timeout}
|
||||
|
||||
# Interface ifIndex
|
||||
oid == "1.3.6.1.2.1.2.2.1.1" ->
|
||||
{:error, :timeout}
|
||||
|
||||
# LLDP - return empty
|
||||
String.starts_with?(oid, "1.0.8802") ->
|
||||
# LLDP - return empty
|
||||
{:ok, []}
|
||||
|
||||
# CDP - return empty
|
||||
String.starts_with?(oid, "1.3.6.1.4.1.9.9.23") ->
|
||||
# CDP - return empty
|
||||
{:ok, []}
|
||||
|
||||
# Debug data collection (if_table, ifx_table)
|
||||
String.starts_with?(oid, "1.3.6.1.2.1") ->
|
||||
{:ok, []}
|
||||
|
||||
true ->
|
||||
|
|
@ -533,13 +566,14 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
end)
|
||||
|
||||
# Mock interface and sensor discovery - no interfaces/sensors for simplicity
|
||||
# Walk called 10 times:
|
||||
# Walk called 15 times:
|
||||
# - LM sensors: temp devices (1), temp values (1), fan devices (1), fan values (1),
|
||||
# voltage devices (1), voltage values (1)
|
||||
# - Base sensors fallback (1)
|
||||
# - Interfaces (1)
|
||||
# - Neighbors: LLDP (1), CDP (1)
|
||||
expect(SnmpMock, :walk, 10, fn _, _, _ ->
|
||||
# - Debug data: lm_temp (1), lm_fan (1), lm_voltage (1), ucd_load (1), ucd_memory (1)
|
||||
expect(SnmpMock, :walk, 15, fn _, _, _ ->
|
||||
{:ok, []}
|
||||
end)
|
||||
|
||||
|
|
@ -599,13 +633,14 @@ defmodule Towerops.Snmp.DiscoveryTest do
|
|||
end)
|
||||
|
||||
# Mock interface and sensor discovery
|
||||
# Walk called 10 times:
|
||||
# Walk called 15 times:
|
||||
# - LM sensors: temp devices (1), temp values (1), fan devices (1), fan values (1),
|
||||
# voltage devices (1), voltage values (1)
|
||||
# - Base sensors fallback (1)
|
||||
# - Interfaces (1)
|
||||
# - Neighbors: LLDP (1), CDP (1)
|
||||
expect(SnmpMock, :walk, 10, fn _, _, _ ->
|
||||
# - Debug data: lm_temp (1), lm_fan (1), lm_voltage (1), ucd_load (1), ucd_memory (1)
|
||||
expect(SnmpMock, :walk, 15, fn _, _, _ ->
|
||||
{:ok, []}
|
||||
end)
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,9 @@ defmodule Towerops.Snmp.ProfileBehaviourTest do
|
|||
case profile.discover_system_info(@test_opts) do
|
||||
{:ok, info} ->
|
||||
assert is_map(info)
|
||||
assert map_size(info) > 0
|
||||
|
||||
# Some profiles return empty map (no additional info beyond Base)
|
||||
# Others return profile-specific data (e.g., MikroTik, Ubiquiti)
|
||||
|
||||
{:error, _reason} ->
|
||||
# Some profiles might fail if required data is missing
|
||||
|
|
|
|||
|
|
@ -11,33 +11,13 @@ defmodule Towerops.Snmp.Profiles.CiscoTest do
|
|||
@client_opts [ip: "192.168.1.1", community: "public", version: :v2c, port: 161]
|
||||
|
||||
describe "discover_system_info/1" do
|
||||
test "delegates to Base profile" do
|
||||
# Mock the SNMP calls that Base.discover_system_info makes
|
||||
# Base profile queries 6 system OIDs - match by OID string
|
||||
expect(SnmpMock, :get, 6, fn _target, oid, _opts ->
|
||||
case oid do
|
||||
# sysDescr
|
||||
"1.3.6.1.2.1.1.1.0" -> {:ok, "Cisco IOS Software"}
|
||||
# sysObjectID
|
||||
"1.3.6.1.2.1.1.2.0" -> {:ok, [1, 3, 6, 1, 4, 1, 9]}
|
||||
# sysUpTime
|
||||
"1.3.6.1.2.1.1.3.0" -> {:ok, 12_345}
|
||||
# sysContact
|
||||
"1.3.6.1.2.1.1.4.0" -> {:ok, "admin@example.com"}
|
||||
# sysName
|
||||
"1.3.6.1.2.1.1.5.0" -> {:ok, "test-device"}
|
||||
# sysLocation
|
||||
"1.3.6.1.2.1.1.6.0" -> {:ok, "Test Location"}
|
||||
_ -> {:error, :unknown_oid}
|
||||
end
|
||||
end)
|
||||
|
||||
test "returns empty map (no additional system info beyond Base)" do
|
||||
# Cisco profile doesn't query additional system OIDs
|
||||
# Base profile handles all standard system info discovery
|
||||
result = Cisco.discover_system_info(@client_opts)
|
||||
|
||||
# Should delegate to Base and return its result
|
||||
assert {:ok, system_info} = result
|
||||
assert is_map(system_info)
|
||||
assert system_info.sys_name == "test-device"
|
||||
assert system_info == %{}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -143,8 +143,9 @@ defmodule Towerops.Snmp.Profiles.MikrotikTest do
|
|||
|
||||
describe "discover_sensors/1" do
|
||||
test "discovers health sensors (temperature, voltage, fan)" do
|
||||
# Mock health sensor OIDs
|
||||
expect(SnmpMock, :get, 15, fn _, oid, _ ->
|
||||
# Mock health sensor OIDs + network sensors (DHCP leases, connection count)
|
||||
# Health: 15 OIDs, Network: 2 OIDs = 17 total
|
||||
expect(SnmpMock, :get, 17, fn _, oid, _ ->
|
||||
case oid do
|
||||
# Voltages
|
||||
"1.3.6.1.4.1.14988.1.1.3.1.0" -> {:ok, {:integer, 12}}
|
||||
|
|
@ -166,31 +167,84 @@ defmodule Towerops.Snmp.Profiles.MikrotikTest do
|
|||
"1.3.6.1.4.1.14988.1.1.3.18.0" -> {:ok, {:integer, 0}}
|
||||
# Processor frequency
|
||||
"1.3.6.1.4.1.14988.1.1.3.14.0" -> {:ok, {:integer, 1400}}
|
||||
# Network sensors
|
||||
"1.3.6.1.4.1.14988.1.1.1.3.0" -> {:ok, {:integer, 42}}
|
||||
"1.3.6.1.4.1.14988.1.1.13.1.1.1.0" -> {:ok, {:integer, 1500}}
|
||||
end
|
||||
end)
|
||||
|
||||
# Mock all walk calls - use stub to allow any number of calls (code may short-circuit)
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
case oid do
|
||||
# Health sensor table (new dynamic sensor discovery)
|
||||
# SNMP adapter returns list of %{oid:, value:} maps
|
||||
"1.3.6.1.4.1.14988.1.1.3.100.1.2" ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.4.1.14988.1.1.3.100.1.2.1", value: "CPU Temperature"},
|
||||
%{oid: "1.3.6.1.4.1.14988.1.1.3.100.1.2.2", value: "Board Temperature"}
|
||||
]}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.3.100.1.3" ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.4.1.14988.1.1.3.100.1.3.1", value: 450},
|
||||
%{oid: "1.3.6.1.4.1.14988.1.1.3.100.1.3.2", value: 480}
|
||||
]}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.3.100.1.4" ->
|
||||
{:ok,
|
||||
[
|
||||
%{oid: "1.3.6.1.4.1.14988.1.1.3.100.1.4.1", value: 1},
|
||||
%{oid: "1.3.6.1.4.1.14988.1.1.3.100.1.4.2", value: 1}
|
||||
]}
|
||||
|
||||
# CPU sensors
|
||||
"1.3.6.1.2.1.25.3.3.1.2" -> {:ok, [%{oid: "1.3.6.1.2.1.25.3.3.1.2.1", value: 45}]}
|
||||
"1.3.6.1.2.1.25.3.3.1.2" ->
|
||||
{:ok, [%{oid: "1.3.6.1.2.1.25.3.3.1.2.1", value: 45}]}
|
||||
|
||||
# Storage sensors
|
||||
"1.3.6.1.2.1.25.2.3.1.3" -> {:ok, [%{oid: "1.3.6.1.2.1.25.2.3.1.3.1", value: "RAM"}]}
|
||||
"1.3.6.1.2.1.25.2.3.1.5" -> {:ok, [%{oid: "1.3.6.1.2.1.25.2.3.1.5.1", value: 1000}]}
|
||||
"1.3.6.1.2.1.25.2.3.1.6" -> {:ok, [%{oid: "1.3.6.1.2.1.25.2.3.1.6.1", value: 750}]}
|
||||
"1.3.6.1.2.1.25.2.3.1.3" ->
|
||||
{:ok, [%{oid: "1.3.6.1.2.1.25.2.3.1.3.1", value: "RAM"}]}
|
||||
|
||||
"1.3.6.1.2.1.25.2.3.1.5" ->
|
||||
{:ok, [%{oid: "1.3.6.1.2.1.25.2.3.1.5.1", value: 1000}]}
|
||||
|
||||
"1.3.6.1.2.1.25.2.3.1.6" ->
|
||||
{:ok, [%{oid: "1.3.6.1.2.1.25.2.3.1.6.1", value: 750}]}
|
||||
|
||||
# POE sensors (empty)
|
||||
"1.3.6.1.4.1.14988.1.1.15.1.1.3" -> {:error, :no_such_object}
|
||||
"1.3.6.1.4.1.14988.1.1.15.1.1.5" -> {:error, :no_such_object}
|
||||
"1.3.6.1.4.1.14988.1.1.15.1.1.6" -> {:error, :no_such_object}
|
||||
"1.3.6.1.4.1.14988.1.1.15.1.1.7" -> {:error, :no_such_object}
|
||||
"1.3.6.1.4.1.14988.1.1.15.1.1.3" ->
|
||||
{:error, :no_such_object}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.15.1.1.5" ->
|
||||
{:error, :no_such_object}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.15.1.1.6" ->
|
||||
{:error, :no_such_object}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.15.1.1.7" ->
|
||||
{:error, :no_such_object}
|
||||
|
||||
# Optical sensors (empty)
|
||||
"1.3.6.1.4.1.14988.1.1.16.1.1.2" -> {:error, :no_such_object}
|
||||
"1.3.6.1.4.1.14988.1.1.16.1.1.3" -> {:error, :no_such_object}
|
||||
"1.3.6.1.4.1.14988.1.1.16.1.1.5" -> {:error, :no_such_object}
|
||||
"1.3.6.1.4.1.14988.1.1.16.1.1.7" -> {:error, :no_such_object}
|
||||
"1.3.6.1.4.1.14988.1.1.16.1.1.8" -> {:error, :no_such_object}
|
||||
"1.3.6.1.4.1.14988.1.1.16.1.1.2" ->
|
||||
{:error, :no_such_object}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.16.1.1.3" ->
|
||||
{:error, :no_such_object}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.16.1.1.5" ->
|
||||
{:error, :no_such_object}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.16.1.1.7" ->
|
||||
{:error, :no_such_object}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.16.1.1.8" ->
|
||||
{:error, :no_such_object}
|
||||
|
||||
# Entity sensors from Base
|
||||
"1.3.6.1.2.1.99.1.1.1.1" -> {:ok, []}
|
||||
"1.3.6.1.2.1.99.1.1.1.1" ->
|
||||
{:ok, []}
|
||||
end
|
||||
end)
|
||||
|
||||
|
|
@ -217,8 +271,16 @@ defmodule Towerops.Snmp.Profiles.MikrotikTest do
|
|||
end
|
||||
|
||||
test "discovers CPU load sensors" do
|
||||
# Mock health sensors (none available)
|
||||
expect(SnmpMock, :get, 15, fn _, _, _ -> {:error, :no_such_object} end)
|
||||
# Mock health sensors (15, all return error) + network sensors (2)
|
||||
expect(SnmpMock, :get, 17, fn _, oid, _ ->
|
||||
case oid do
|
||||
# Network sensors
|
||||
"1.3.6.1.4.1.14988.1.1.1.3.0" -> {:ok, {:integer, 10}}
|
||||
"1.3.6.1.4.1.14988.1.1.13.1.1.1.0" -> {:ok, {:integer, 500}}
|
||||
# All health sensors return error
|
||||
_ -> {:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
# Mock all walk calls
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
|
|
@ -255,8 +317,16 @@ defmodule Towerops.Snmp.Profiles.MikrotikTest do
|
|||
end
|
||||
|
||||
test "discovers storage sensors with proper status" do
|
||||
# Mock health sensors (none)
|
||||
expect(SnmpMock, :get, 15, fn _, _, _ -> {:error, :no_such_object} end)
|
||||
# Mock health sensors (15, all return error) + network sensors (2)
|
||||
expect(SnmpMock, :get, 17, fn _, oid, _ ->
|
||||
case oid do
|
||||
# Network sensors
|
||||
"1.3.6.1.4.1.14988.1.1.1.3.0" -> {:ok, {:integer, 10}}
|
||||
"1.3.6.1.4.1.14988.1.1.13.1.1.1.0" -> {:ok, {:integer, 500}}
|
||||
# All health sensors return error
|
||||
_ -> {:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
# Mock all walk calls
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
|
|
@ -307,12 +377,30 @@ defmodule Towerops.Snmp.Profiles.MikrotikTest do
|
|||
end
|
||||
|
||||
test "discovers POE sensors per port" do
|
||||
# Mock health, cpu, storage as empty
|
||||
expect(SnmpMock, :get, 15, fn _, _, _ -> {:error, :no_such_object} end)
|
||||
# Mock health sensors (15, all return error) + network sensors (2)
|
||||
expect(SnmpMock, :get, 17, fn _, oid, _ ->
|
||||
case oid do
|
||||
# Network sensors
|
||||
"1.3.6.1.4.1.14988.1.1.1.3.0" -> {:ok, {:integer, 10}}
|
||||
"1.3.6.1.4.1.14988.1.1.13.1.1.1.0" -> {:ok, {:integer, 500}}
|
||||
# All health sensors return error
|
||||
_ -> {:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
# Mock all walk calls
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
case oid do
|
||||
# Health sensor table (checked before POE)
|
||||
"1.3.6.1.4.1.14988.1.1.3.100.1.2" ->
|
||||
{:ok, []}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.3.100.1.3" ->
|
||||
{:ok, []}
|
||||
|
||||
"1.3.6.1.4.1.14988.1.1.3.100.1.4" ->
|
||||
{:ok, []}
|
||||
|
||||
# CPU sensors
|
||||
"1.3.6.1.2.1.25.3.3.1.2" ->
|
||||
{:ok, []}
|
||||
|
|
@ -385,7 +473,7 @@ defmodule Towerops.Snmp.Profiles.MikrotikTest do
|
|||
|
||||
assert result.manufacturer == "MikroTik"
|
||||
assert result.model == "RB5009UG+S+"
|
||||
assert result.firmware_version == "Mikrotik RouterOS 7.13.2"
|
||||
assert result.firmware_version == "RouterOS 7.13.2"
|
||||
end
|
||||
|
||||
test "identifies generic RouterOS device when model not found" do
|
||||
|
|
@ -426,7 +514,16 @@ defmodule Towerops.Snmp.Profiles.MikrotikTest do
|
|||
|
||||
describe "sensor status determination" do
|
||||
test "CPU status changes with load" do
|
||||
expect(SnmpMock, :get, 15, fn _, _, _ -> {:error, :no_such_object} end)
|
||||
# Mock health sensors (15, all return error) + network sensors (2)
|
||||
expect(SnmpMock, :get, 17, fn _, oid, _ ->
|
||||
case oid do
|
||||
# Network sensors
|
||||
"1.3.6.1.4.1.14988.1.1.1.3.0" -> {:ok, {:integer, 10}}
|
||||
"1.3.6.1.4.1.14988.1.1.13.1.1.1.0" -> {:ok, {:integer, 500}}
|
||||
# All health sensors return error
|
||||
_ -> {:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
case oid do
|
||||
|
|
@ -455,7 +552,16 @@ defmodule Towerops.Snmp.Profiles.MikrotikTest do
|
|||
end
|
||||
|
||||
test "memory status changes with usage percentage" do
|
||||
expect(SnmpMock, :get, 15, fn _, _, _ -> {:error, :no_such_object} end)
|
||||
# Mock health sensors (15, all return error) + network sensors (2)
|
||||
expect(SnmpMock, :get, 17, fn _, oid, _ ->
|
||||
case oid do
|
||||
# Network sensors
|
||||
"1.3.6.1.4.1.14988.1.1.1.3.0" -> {:ok, {:integer, 10}}
|
||||
"1.3.6.1.4.1.14988.1.1.13.1.1.1.0" -> {:ok, {:integer, 500}}
|
||||
# All health sensors return error
|
||||
_ -> {:error, :no_such_object}
|
||||
end
|
||||
end)
|
||||
|
||||
stub(SnmpMock, :walk, fn _, oid, _ ->
|
||||
case oid do
|
||||
|
|
|
|||
|
|
@ -11,26 +11,13 @@ defmodule Towerops.Snmp.Profiles.NetSnmpTest do
|
|||
@client_opts [ip: "192.168.1.100", community: "public", version: :v2c, port: 161]
|
||||
|
||||
describe "discover_system_info/1" do
|
||||
test "delegates to Base profile" do
|
||||
# Mock the SNMP calls that Base.discover_system_info makes
|
||||
# Base profile queries 6 system OIDs - match by OID string
|
||||
expect(SnmpMock, :get, 6, fn _target, oid, _opts ->
|
||||
case oid do
|
||||
"1.3.6.1.2.1.1.1.0" -> {:ok, "Linux ubuntu 5.4.0-42-generic #46-Ubuntu"}
|
||||
"1.3.6.1.2.1.1.2.0" -> {:ok, [1, 3, 6, 1, 4, 1, 8072]}
|
||||
"1.3.6.1.2.1.1.3.0" -> {:ok, 123_456}
|
||||
"1.3.6.1.2.1.1.4.0" -> {:ok, "root@localhost"}
|
||||
"1.3.6.1.2.1.1.5.0" -> {:ok, "ubuntu-server"}
|
||||
"1.3.6.1.2.1.1.6.0" -> {:ok, "Data Center"}
|
||||
_ -> {:error, :unknown_oid}
|
||||
end
|
||||
end)
|
||||
|
||||
test "returns empty map (no additional system info beyond Base)" do
|
||||
# NetSnmp profile doesn't query additional system OIDs
|
||||
# Base profile handles all standard system info discovery
|
||||
result = NetSnmp.discover_system_info(@client_opts)
|
||||
|
||||
assert {:ok, system_info} = result
|
||||
assert is_map(system_info)
|
||||
assert system_info.sys_name == "ubuntu-server"
|
||||
assert system_info == %{}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue